Merge branch 'release/v0.3.2' into feature/rag2

* release/v0.3.2: (245 commits)
  fix(conversation_schema): refine citations field type to Dict[str, Any]
  fix(tool_controller): re-raise HTTPException to preserve original status codes
  fix(workflow): add reasoning content, suggested questions, citations and audio status support
  feat(workflow): augment logging queries and ameliorate error handling
  fix(api_key): bypass publication check for SERVICE type API keys
  fix(multimodal_service): add '文档内容:' prefix to document text and simplify image placeholder text
  fix(api): convert config_id to string in write_router
  fix(api): convert end_user_id to string in write_router
  fix(multimodal_service): refactor image processing to use intermediate list before extending result
  fix(web): node status ui
  fix(api): correct import paths in memory_read and celery task command
  fix(api): correct import paths in memory_read and celery task command
  refactor(tool): flatten request body parameters for model exposure
  fix(api): correct import paths in memory_read and celery task command
  refactor(workflow): streamline node execution handling and log service logic
  feat(web): http request add process
  feat(web): workflow app logs
  fix(app_chat_service,draft_run_service): move system_prompt augmentation before LangChainAgent instantiation
  fix(app_chat_service,draft_run_service): move system_prompt augmentation before LangChainAgent instantiation
  refactor(http_request): simplify request handling and remove unused fields
  ...

# Conflicts:
#	api/app/controllers/file_controller.py
#	api/app/tasks.py
This commit is contained in:
Mark
2026-04-27 16:13:57 +08:00
342 changed files with 13546 additions and 4400 deletions

View File

@@ -15,8 +15,8 @@ class ApiKeyCreate(BaseModel):
type: ApiKeyType = Field(..., description="API Key 类型")
scopes: List[str] = Field(default_factory=list, description="权限范围列表")
resource_id: Optional[uuid.UUID] = Field(None, description="关联资源ID")
rate_limit: Optional[int] = Field(100, ge=1, le=1000, description="QPS限制请求/秒)")
daily_request_limit: Optional[int] = Field(10000, description="日请求限制", ge=1)
rate_limit: Optional[int] = Field(50, ge=1, le=1000, description="QPS限制请求/秒)")
daily_request_limit: Optional[int] = Field(100000, description="日请求限制", ge=1)
quota_limit: Optional[int] = Field(None, description="配额限制(总请求数)", ge=1)
expires_at: Optional[datetime.datetime] = Field(None, description="过期时间")
@@ -55,7 +55,7 @@ class ApiKeyUpdate(BaseModel):
description: Optional[str] = Field(None, description="描述")
scopes: Optional[List[str]] = Field(None, description="权限范围列表")
rate_limit: Optional[int] = Field(None, description="速率限制(请求/分钟)", ge=1)
daily_request_limit: Optional[int] = Field(10000, description="每日请求数限制", ge=1)
daily_request_limit: Optional[int] = Field(100000, description="每日请求数限制", ge=1)
quota_limit: Optional[int] = Field(None, description="配额限制(总请求数)", ge=1)
is_active: Optional[bool] = Field(None, description="是否激活")
expires_at: Optional[datetime.datetime] = Field(None, description="过期时间")

View File

@@ -14,6 +14,7 @@ class AppLogMessage(BaseModel):
conversation_id: uuid.UUID
role: str = Field(description="角色: user / assistant / system")
content: str
status: Optional[str] = Field(default=None, description="执行状态(工作流专用): completed / failed")
meta_data: Optional[Dict[str, Any]] = None
created_at: datetime.datetime
@@ -48,6 +49,22 @@ class AppLogConversation(BaseModel):
return int(dt.timestamp() * 1000) if dt else None
class AppLogNodeExecution(BaseModel):
"""工作流节点执行记录"""
node_id: str
node_type: str
node_name: Optional[str] = None
status: str = "pending"
error: Optional[str] = None
input: Optional[Any] = None
process: Optional[Any] = None
output: Optional[Any] = None
cycle_items: Optional[List[Any]] = None
elapsed_time: Optional[float] = None
token_usage: Optional[Dict[str, Any]] = None
class AppLogConversationDetail(AppLogConversation):
"""会话详情(包含消息列表)"""
messages: List[AppLogMessage] = Field(default_factory=list)
node_executions_map: Dict[str, List[AppLogNodeExecution]] = Field(default_factory=dict, description="按消息ID分组的节点执行记录")

View File

@@ -3,7 +3,7 @@ import uuid
from typing import Optional, Any, List, Dict, Union
from enum import Enum, StrEnum
from pydantic import BaseModel, Field, ConfigDict, field_serializer, field_validator
from pydantic import BaseModel, Field, ConfigDict, field_serializer, field_validator, model_serializer
from app.schemas.workflow_schema import WorkflowConfigCreate
@@ -44,6 +44,8 @@ class FileInput(BaseModel):
upload_file_id: Optional[uuid.UUID] = Field(None, description="已上传文件IDlocal_file时必填")
url: Optional[str] = Field(None, description="远程URLremote_url时必填")
file_type: Optional[str] = Field(None, description="具体文件格式如image/jpg、audio/wav、document/docx、video/mp4")
name: Optional[str] = Field(None, description="文件名")
size: Optional[int] = Field(None, description="文件大小(字节)")
_content = None
@@ -153,6 +155,10 @@ class FileUploadConfig(BaseModel):
document_allowed_extensions: List[str] = Field(
default=["pdf", "docx", "doc", "xlsx", "xls", "txt", "csv", "json", "md"]
)
document_image_recognition: bool = Field(
default=False,
description="是否识别文档中的图片(需配置视觉模型)"
)
# 视频文件MP4/MOV/AVI/WebM最大 500MB
video_enabled: bool = Field(default=False)
video_max_size_mb: int = Field(default=50)
@@ -194,6 +200,7 @@ class TextToSpeechConfig(BaseModel):
class CitationConfig(BaseModel):
"""引用和归属配置"""
enabled: bool = Field(default=False)
allow_download: bool = Field(default=False, description="是否允许下载引用文档")
class Citation(BaseModel):
@@ -201,6 +208,7 @@ class Citation(BaseModel):
file_name: str
knowledge_id: str
score: float
download_url: Optional[str] = Field(default=None, description="引用文档下载链接allow_download 开启时返回)")
class WebSearchConfig(BaseModel):
@@ -243,6 +251,7 @@ class ModelParameters(BaseModel):
stop: Optional[List[str]] = Field(default=None, description="停止序列")
deep_thinking: bool = Field(default=False, description="是否启用深度思考模式(需模型支持,如 DeepSeek-R1、QwQ 等)")
thinking_budget_tokens: Optional[int] = Field(default=None, ge=1024, le=131072, description="深度思考 token 预算(仅部分模型支持)")
json_output: bool = Field(default=False, description="是否强制 JSON 格式输出(需模型支持 json_output 能力)")
class VariableDefinition(BaseModel):
@@ -650,11 +659,13 @@ class DraftRunResponse(BaseModel):
usage: Optional[Dict[str, Any]] = Field(default=None, description="Token 使用情况")
elapsed_time: Optional[float] = Field(default=None, description="耗时(秒)")
suggested_questions: List[str] = Field(default_factory=list, description="下一步建议问题")
citations: List[CitationSource] = Field(default_factory=list, description="引用来源")
citations: List[Dict[str, Any]] = Field(default_factory=list, description="引用来源")
audio_url: Optional[str] = Field(default=None, description="TTS 语音URL")
audio_status: Optional[str] = Field(default=None, description="TTS 语音状态")
def model_dump(self, **kwargs):
data = super().model_dump(**kwargs)
@model_serializer(mode="wrap")
def _serialize(self, handler):
data = handler(self)
if not data.get("reasoning_content"):
data.pop("reasoning_content", None)
return data

View File

@@ -2,7 +2,7 @@
import uuid
import datetime
from typing import Optional, Dict, Any, List
from pydantic import BaseModel, Field, ConfigDict, field_serializer
from pydantic import BaseModel, Field, ConfigDict, field_serializer, model_serializer
# 导入 FileInput用于体验运行
from app.schemas.app_schema import FileInput
@@ -94,6 +94,18 @@ class ChatResponse(BaseModel):
message_id: str
usage: Optional[Dict[str, Any]] = None
elapsed_time: Optional[float] = None
reasoning_content: Optional[str] = None
suggested_questions: Optional[List[str]] = None
citations: Optional[List[Dict[str, Any]]] = None
audio_url: Optional[str] = None
audio_status: Optional[str] = None
@model_serializer(mode="wrap")
def _serialize(self, handler):
data = handler(self)
if not data.get("reasoning_content"):
data.pop("reasoning_content", None)
return data
# ---------- Conversation Summary Schemas ----------

View File

@@ -4,9 +4,10 @@ This module defines Pydantic schemas for the Memory API Service endpoints,
including request validation and response structures for read and write operations.
"""
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Literal, Optional
import uuid
from pydantic import BaseModel, Field, field_validator
from pydantic import BaseModel, ConfigDict, Field, field_validator
class MemoryWriteRequest(BaseModel):
@@ -110,6 +111,30 @@ class MemoryReadRequest(BaseModel):
class MemoryWriteResponse(BaseModel):
"""Response schema for memory write operation.
Attributes:
task_id: task ID for status polling
status: Initial task status (QUEUED)
end_user_id: End user ID the write was submitted for
"""
task_id: str = Field(..., description="task ID for polling")
status: str = Field(..., description="Task status: QUEUED")
end_user_id: str = Field(..., description="End user ID")
class TaskStatusResponse(BaseModel):
"""Response schema for task status check.
Attributes:
status: Task status (PENDING, STARTED, SUCCESS, FAILURE, SKIPPED)
result: Task result data (available when status is SUCCESS or FAILURE)
"""
status: str = Field(..., description="Task status")
result: Optional[Dict[str, Any]] = Field(None, description="Task result when completed")
class MemoryWriteSyncResponse(BaseModel):
"""Response schema for synchronous memory write.
Attributes:
status: Operation status (success or failed)
end_user_id: End user ID that was written to
@@ -118,8 +143,8 @@ class MemoryWriteResponse(BaseModel):
end_user_id: str = Field(..., description="End user ID")
class MemoryReadResponse(BaseModel):
"""Response schema for memory read operation.
class MemoryReadSyncResponse(BaseModel):
"""Response schema for synchronous memory read.
Attributes:
answer: Generated answer from memory retrieval
@@ -128,12 +153,25 @@ class MemoryReadResponse(BaseModel):
"""
answer: str = Field(..., description="Generated answer")
intermediate_outputs: List[Dict[str, Any]] = Field(
default_factory=list,
default_factory=list,
description="Intermediate retrieval outputs"
)
end_user_id: str = Field(..., description="End user ID")
class MemoryReadResponse(BaseModel):
"""Response schema for memory read operation.
Attributes:
task_id: Celery task ID for status polling
status: Initial task status (PENDING)
end_user_id: End user ID the read was submitted for
"""
task_id: str = Field(..., description="Celery task ID for polling")
status: str = Field(..., description="Task status: PENDING")
end_user_id: str = Field(..., description="End user ID")
class CreateEndUserRequest(BaseModel):
"""Request schema for creating an end user.
@@ -141,10 +179,12 @@ class CreateEndUserRequest(BaseModel):
other_id: External user identifier (required)
other_name: Display name for the end user
memory_config_id: Optional memory config ID. If not provided, uses workspace default.
app_id: Optional app ID to bind the end user to.
"""
other_id: str = Field(..., description="External user identifier (required)")
other_name: Optional[str] = Field("", description="Display name")
memory_config_id: Optional[str] = Field(None, description="Memory config ID. Falls back to workspace default if not provided.")
app_id: Optional[str] = Field(None, description="App ID to bind the end user to")
@field_validator("other_id")
@classmethod
@@ -192,6 +232,7 @@ class MemoryConfigItem(BaseModel):
created_at: Optional[str] = Field(None, description="Creation timestamp")
updated_at: Optional[str] = Field(None, description="Last update timestamp")
# ========== V1 记忆配置管理接口 Schema ==========
class ListConfigsResponse(BaseModel):
"""Response schema for listing memory configs.
@@ -202,3 +243,203 @@ class ListConfigsResponse(BaseModel):
"""
configs: List[MemoryConfigItem] = Field(default_factory=list, description="List of configs")
total: int = Field(0, description="Total number of configs")
class ConfigCreateRequest(BaseModel):
"""Request schema for creating a new memory config."""
config_name: str = Field(..., description="Configuration name")
config_desc: Optional[str] = Field("", description="Configuration description")
scene_id: uuid.UUID = Field(..., description="Associated ontology scene ID (UUID, required)")
llm_id: Optional[str] = Field(None, description="LLM model configuration ID")
embedding_id: Optional[str] = Field(None, description="Embedding model configuration ID")
rerank_id: Optional[str] = Field(None, description="Reranking model configuration ID")
reflection_model_id: Optional[str] = Field(None, description="Reflection model ID")
emotion_model_id: Optional[str] = Field(None, description="Emotion analysis model ID")
@field_validator("config_name")
@classmethod
def validate_config_name(cls, v: str) -> str:
if not v or not v.strip():
raise ValueError("config_name is required and cannot be empty")
return v.strip()
class ConfigUpdateRequest(BaseModel):
"""Request schema for updating memory config basic info.
Attributes:
config_id: Configuration UUID to update (required)
config_name: New configuration name
config_desc: New configuration description
scene_id: New associated ontology scene ID
"""
config_id: str = Field(..., description="Configuration ID to update")
config_name: Optional[str] = Field(None, description="Configuration name")
config_desc: Optional[str] = Field(None, description="Configuration description")
scene_id: Optional[uuid.UUID] = Field(None, description="Associated ontology scene ID")
@field_validator("config_id")
@classmethod
def validate_config_id(cls, v: str) -> str:
"""Validate that config_id is not empty."""
if not v or not v.strip():
raise ValueError("config_id is required and cannot be empty")
return v.strip()
class ConfigUpdateExtractedRequest(BaseModel):
"""Request schema for updating memory config extracted parameters.
Attributes:
config_id: Configuration UUID to update (required)
llm_id: Optional LLM model configuration ID
audio_id: Optional audio model configuration ID
vision_id: Optional vision model configuration ID
video_id: Optional video model configuration ID
embedding_id: Optional embedding model configuration ID
rerank_id: Optional reranking model configuration ID
enable_llm_dedup_blockwise: Optional toggle for LLM decision deduplication
enable_llm_disambiguation: Optional toggle for LLM decision disambiguation
deep_retrieval: Optional toggle for deep retrieval
t_type_strict: Optional float (0-1) for type strictness threshold
t_name_strict: Optional float (0-1) for name strictness threshold
t_overall: Optional float (0-1) for overall strictness threshold
state: Optional boolean for config active state
chunker_strategy: Optional string for memory chunking strategy
statement_granularity: Optional int (1-3) for statement extraction granularity
include_dialogue_context: Optional boolean for including dialogue context in retrieval
max_context: Optional int for maximum dialogue context length in characters
pruning_enabled: Optional boolean to enable intelligent semantic pruning
pruning_scene: Optional string for semantic pruning scene
pruning_threshold: Optional float (0-0.9) for semantic pruning threshold
enable_self_reflexion: Optional boolean to enable self-reflexion
iteration_period: Optional string for reflexion iteration period in hours (1, 3, 6, 12, 24)
reflexion_range: Optional string for reflexion range (partial or all)
baseline: Optional string for baseline (TIME/FACT/TIME-FACT)
"""
config_id: str = Field(..., description="Configuration ID (UUID)")
llm_id: Optional[str] = Field(None, description="LLM model configuration ID")
audio_id: Optional[str] = Field(None, description="Audio model ID")
vision_id: Optional[str] = Field(None, description="Vision model ID")
video_id: Optional[str] = Field(None, description="Video model ID")
embedding_id: Optional[str] = Field(None, description="Embedding model configuration ID")
rerank_id: Optional[str] = Field(None, description="Reranking model configuration ID")
enable_llm_dedup_blockwise: Optional[bool] = Field(None, description="Enable LLM decision deduplication")
enable_llm_disambiguation: Optional[bool] = Field(None, description="Enable LLM decision disambiguation")
deep_retrieval: Optional[bool] = Field(None, description="Deep retrieval toggle")
t_type_strict: Optional[float] = Field(None, ge=0.0, le=1.0, description="type strictness threshold")
t_name_strict: Optional[float] = Field(None, ge=0.0, le=1.0, description="name strictness threshold")
t_overall: Optional[float] = Field(None, ge=0.0, le=1.0, description="overall strictness threshold")
state: Optional[bool] = Field(None, description="config active state")
# 句子提取
chunker_strategy: Optional[str] = Field(None, description="memory chunking strategy")
statement_granularity: Optional[int] = Field(None, ge=1, le=3, description="statement extraction granularity")
include_dialogue_context: Optional[bool] = Field(None, description="whether to include dialogue context in retrieval")
max_context: Optional[int] = Field(None, gt=100, description="maximum dialogue context length in characters")
# 剪枝配置:与 runtime.json 中 pruning 段对应
pruning_enabled: Optional[bool] = Field(None, description="whether to enable intelligent semantic pruning")
pruning_scene: Optional[str] = Field(None, description="semantic pruning scene")
pruning_threshold: Optional[float] = Field(None, ge=0.0, le=0.9, description="semantic pruning threshold (0-0.9)")
enable_self_reflexion: Optional[bool] = Field(None, description="whether to enable self-reflexion")
iteration_period: Optional[Literal["1", "3", "6", "12", "24"]] = Field(None, description="reflexion iteration period in hours (1, 3, 6, 12, 24)")
reflexion_range: Optional[Literal["partial", "all"]] = Field(None, description="reflexion range: partial/all")
baseline: Optional[Literal["TIME", "FACT", "TIME-FACT"]] = Field(None, description="baseline: TIME/FACT/TIME-FACT")
@field_validator("config_id")
@classmethod
def validate_config_id(cls, v: str) -> str:
if not v or not v.strip():
raise ValueError("config_id is required and cannot be empty")
return v.strip()
class ConfigUpdateForgettingRequest(BaseModel):
"""Request schema for updating memory config forgetting parameters.
Attributes:
config_id: Configuration UUID to update (required)
decay_constant: Decay constant for forgetting
lambda_time: Time decay parameter
lambda_mem: Memory decay parameter
offset: Offset for forgetting curve
max_history_length: Maximum history length to consider for forgetting
forgetting_threshold: Threshold for forgetting
min_days_since_access: Minimum days since last access to trigger forgetting
enable_llm_summary: Whether to use LLM-generated summaries for forgetting
max_merge_batch_size: Maximum batch size for merging nodes during forgetting
forgetting_interval_hours: Interval in hours for periodic forgetting
"""
model_config = ConfigDict(populate_by_name=True, extra="forbid")
config_id: str = Field(..., description="Configuration ID (UUID)")
decay_constant: Optional[float] = Field(None, ge=0.0, le=1.0, description="Decay constant for forgetting")
lambda_time: Optional[float] = Field(None, ge=0.0, le=1.0, description="Time decay parameter")
lambda_mem: Optional[float] = Field(None, ge=0.0, le=1.0, description="Memory decay parameter")
offset: Optional[float] = Field(None, ge=0.0, le=1.0, description="Offset for forgetting curve")
max_history_length: Optional[int] = Field(None, ge=10, le=1000, description="Maximum history length to consider for forgetting")
forgetting_threshold: Optional[float] = Field(None, ge=0.0, le=1.0, description="Forgetting threshold")
min_days_since_access: Optional[int] = Field(None, ge=1, le=365, description="Minimum days since last access to trigger forgetting")
enable_llm_summary: Optional[bool] = Field(None, description="Whether to use LLM-generated summaries for forgetting")
max_merge_batch_size: Optional[int] = Field(None, ge=1, le=1000, description="Maximum batch size for merging nodes during forgetting")
forgetting_interval_hours: Optional[int] = Field(None, ge=1, le=168, description="Interval in hours for periodic forgetting")
@field_validator("config_id")
@classmethod
def validate_config_id(cls, v: str) -> str:
if not v or not v.strip():
raise ValueError("config_id is required and cannot be empty")
return v.strip()
class EmotionConfigUpdateRequest(BaseModel):
"""Request schema for updating memory config emotion parameters.
Attributes:
config_id: Configuration UUID to update (required)
emotion_enabled: Whether to enable emotion extraction
emotion_model_id: Emotion analysis model ID
emotion_extract_keywords: Whether to extract emotion keywords
emotion_min_intensity: Minimum emotion intensity threshold (0.0-1.0)
emotion_enable_subject: Whether to enable subject classification for emotions
"""
config_id: str = Field(..., description="Configuration ID (UUID)")
emotion_enabled: bool = Field(..., description="Whether to enable emotion extraction")
emotion_model_id: Optional[str] = Field(None, description="Emotion analysis model ID")
emotion_extract_keywords: bool = Field(..., description="Whether to extract emotion keywords")
emotion_min_intensity: float = Field(..., ge=0.0, le=1.0, description="Minimum emotion intensity threshold")
emotion_enable_subject: bool = Field(..., description="Whether to enable subject classification for emotions")
@field_validator("config_id")
@classmethod
def validate_config_id(cls, v: str) -> str:
if not v or not v.strip():
raise ValueError("config_id is required and cannot be empty")
return v.strip()
class ReflectionConfigUpdateRequest(BaseModel):
"""Request schema for updating memory config reflection parameters.
Attributes:
config_id: Configuration UUID to update (required)
reflection_enabled: Whether to enable self-reflection
reflection_period_in_hours: Reflection iteration period in hours
reflexion_range: Reflection range (partial or all)
baseline: Baseline for reflection (TIME/FACT/TIME-FACT)
reflection_model_id: Reflection model ID
memory_verify: Whether to enable memory verification
quality_assessment: Whether to enable quality assessment
"""
config_id: str = Field(..., description="Configuration ID (UUID)")
reflection_enabled: bool = Field(..., description="Whether to enable self-reflection")
reflection_period_in_hours: str = Field(..., description="Reflection iteration period in hours")
reflexion_range: Literal["partial", "all"] = Field(..., description="Reflection range: partial/all")
baseline: Literal["TIME", "FACT", "TIME-FACT"] = Field(..., description="Baseline: TIME/FACT/TIME-FACT")
reflection_model_id: str = Field(..., description="Reflection model ID")
memory_verify: bool = Field(..., description="Whether to enable memory verification")
quality_assessment: bool = Field(..., description="Whether to enable quality assessment")
@field_validator("config_id")
@classmethod
def validate_config_id(cls, v: str) -> str:
if not v or not v.strip():
raise ValueError("config_id is required and cannot be empty")
return v.strip()

View File

@@ -291,7 +291,7 @@ class ConfigUpdateExtracted(BaseModel): # 更新记忆萃取引擎配置参数
pruning_threshold: Optional[float] = Field(
None, ge=0.0, le=0.9, description="智能语义剪枝阈值0-0.9"
)
#TODO:萃取引擎的更新的更新会带有反思引擎的参数,需判断业务是否需要,不需要可以重构
# 反思配置
enable_self_reflexion: Optional[bool] = Field(None, description="是否启用自我反思")
iteration_period: Optional[Literal["1", "3", "6", "12", "24"]] = Field(