2026-01-22 00:28:43 +08:00
|
|
|
|
import asyncio
|
|
|
|
|
|
import json
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
from collections.abc import AsyncIterator
|
2026-02-25 16:30:36 +08:00
|
|
|
|
from datetime import UTC, datetime
|
2026-03-07 23:28:25 +08:00
|
|
|
|
from typing import Any
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
|
|
|
|
|
from langchain.messages import AIMessage, AIMessageChunk, HumanMessage
|
|
|
|
|
|
from langgraph.types import Command
|
2026-03-17 10:16:44 +08:00
|
|
|
|
from yuxi import config as conf
|
2026-03-20 12:08:00 +08:00
|
|
|
|
from yuxi.agents.buildin import agent_manager
|
2026-06-02 18:47:28 +08:00
|
|
|
|
from yuxi.agents.context import build_agent_input_context, normalize_agent_context_config
|
2026-03-29 10:55:00 +08:00
|
|
|
|
from yuxi.agents.state import AgentStatePayload
|
2026-05-24 00:46:07 +08:00
|
|
|
|
from yuxi.repositories.agent_repository import AgentRepository
|
2026-06-02 18:47:28 +08:00
|
|
|
|
from yuxi.repositories.agent_run_repository import AgentRunRepository
|
2026-03-17 10:16:44 +08:00
|
|
|
|
from yuxi.repositories.conversation_repository import ConversationRepository
|
2026-05-24 00:46:07 +08:00
|
|
|
|
from yuxi.services.conversation_service import serialize_attachment
|
2026-03-31 09:58:16 +08:00
|
|
|
|
from yuxi.services.langfuse_service import (
|
|
|
|
|
|
LangfuseRunContext,
|
|
|
|
|
|
build_run_context,
|
|
|
|
|
|
flush_langfuse,
|
|
|
|
|
|
get_trace_info,
|
|
|
|
|
|
)
|
2026-03-17 10:16:44 +08:00
|
|
|
|
from yuxi.storage.postgres.manager import pg_manager
|
2026-05-24 00:46:07 +08:00
|
|
|
|
from yuxi.storage.postgres.models_business import Agent, User
|
2026-05-31 13:40:15 +08:00
|
|
|
|
from yuxi.utils.guard import content_guard
|
2026-03-17 10:16:44 +08:00
|
|
|
|
from yuxi.utils.logging_config import logger
|
2026-03-17 19:46:09 +08:00
|
|
|
|
from yuxi.utils.question_utils import (
|
2026-03-17 03:47:20 +08:00
|
|
|
|
normalize_questions as _normalize_interrupt_questions,
|
2026-03-17 03:39:48 +08:00
|
|
|
|
)
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
2026-06-03 18:55:34 +08:00
|
|
|
|
|
2026-02-13 22:16:11 +08:00
|
|
|
|
def _build_state_files(attachments: list[dict]) -> dict:
|
|
|
|
|
|
"""将附件列表转换为 StateBackend 格式的 files 字典
|
|
|
|
|
|
|
|
|
|
|
|
StateBackend 期望的格式:
|
|
|
|
|
|
{
|
|
|
|
|
|
"/attachments/file.md": {
|
|
|
|
|
|
"content": ["line1", "line2", ...],
|
|
|
|
|
|
"created_at": "...",
|
|
|
|
|
|
"modified_at": "...",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"""
|
|
|
|
|
|
files = {}
|
|
|
|
|
|
for attachment in attachments:
|
|
|
|
|
|
if attachment.get("status") != "parsed":
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
file_path = attachment.get("file_path")
|
|
|
|
|
|
markdown = attachment.get("markdown")
|
|
|
|
|
|
|
|
|
|
|
|
if not file_path or not markdown:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
2026-02-25 16:30:36 +08:00
|
|
|
|
now = datetime.now(UTC).isoformat()
|
2026-02-13 22:16:11 +08:00
|
|
|
|
# 将 markdown 内容按行拆分
|
|
|
|
|
|
content_lines = markdown.split("\n")
|
|
|
|
|
|
files[file_path] = {
|
|
|
|
|
|
"content": content_lines,
|
|
|
|
|
|
"created_at": attachment.get("uploaded_at", now),
|
|
|
|
|
|
"modified_at": attachment.get("uploaded_at", now),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return files
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-22 00:28:43 +08:00
|
|
|
|
async def _get_langgraph_messages(agent_instance, config_dict):
|
|
|
|
|
|
graph = await agent_instance.get_graph()
|
|
|
|
|
|
state = await graph.aget_state(config_dict)
|
|
|
|
|
|
|
|
|
|
|
|
if not state or not state.values:
|
|
|
|
|
|
logger.warning("No state found in LangGraph")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
return state.values.get("messages", [])
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-31 09:58:16 +08:00
|
|
|
|
def _build_langfuse_run_context(
|
|
|
|
|
|
*,
|
|
|
|
|
|
current_user,
|
|
|
|
|
|
thread_id: str,
|
|
|
|
|
|
agent_id: str,
|
|
|
|
|
|
request_id: str,
|
|
|
|
|
|
operation: str,
|
2026-05-24 00:46:07 +08:00
|
|
|
|
backend_id: str | None = None,
|
2026-03-31 09:58:16 +08:00
|
|
|
|
message_type: str | None = None,
|
|
|
|
|
|
) -> LangfuseRunContext:
|
|
|
|
|
|
return build_run_context(
|
2026-05-17 22:44:05 +08:00
|
|
|
|
user_id=str(getattr(current_user, "uid", current_user.id)),
|
2026-03-31 09:58:16 +08:00
|
|
|
|
thread_id=thread_id,
|
|
|
|
|
|
agent_id=agent_id,
|
|
|
|
|
|
request_id=request_id,
|
|
|
|
|
|
operation=operation,
|
2026-05-24 00:46:07 +08:00
|
|
|
|
backend_id=backend_id,
|
2026-03-31 09:58:16 +08:00
|
|
|
|
message_type=message_type,
|
|
|
|
|
|
username=getattr(current_user, "username", None),
|
2026-05-17 22:44:05 +08:00
|
|
|
|
login_user_id=getattr(current_user, "uid", None),
|
2026-03-31 09:58:16 +08:00
|
|
|
|
department_id=getattr(current_user, "department_id", None),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-29 10:55:00 +08:00
|
|
|
|
def extract_agent_state(values: dict) -> AgentStatePayload:
|
2026-02-13 22:16:11 +08:00
|
|
|
|
"""从 LangGraph state 中提取 agent 状态"""
|
2026-01-22 00:28:43 +08:00
|
|
|
|
if not isinstance(values, dict):
|
2026-06-01 22:28:45 +08:00
|
|
|
|
return {"todos": [], "files": {}, "artifacts": [], "subagent_runs": []}
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
2026-02-13 22:16:11 +08:00
|
|
|
|
# 直接获取,信任 state 的数据结构
|
|
|
|
|
|
todos = values.get("todos")
|
2026-03-29 10:55:00 +08:00
|
|
|
|
artifacts = values.get("artifacts")
|
2026-06-01 22:28:45 +08:00
|
|
|
|
subagent_runs = values.get("subagent_runs")
|
2026-03-29 10:55:00 +08:00
|
|
|
|
result: AgentStatePayload = {
|
2026-02-13 22:16:11 +08:00
|
|
|
|
"todos": list(todos)[:20] if todos else [],
|
|
|
|
|
|
"files": values.get("files") or {},
|
2026-03-29 10:55:00 +08:00
|
|
|
|
"artifacts": list(artifacts) if artifacts else [],
|
2026-06-01 22:28:45 +08:00
|
|
|
|
"subagent_runs": list(subagent_runs) if subagent_runs else [],
|
2026-02-13 22:16:11 +08:00
|
|
|
|
}
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-01 03:17:43 +08:00
|
|
|
|
def _agent_state_signature(agent_state: AgentStatePayload | dict | None) -> str:
|
|
|
|
|
|
if not agent_state:
|
|
|
|
|
|
return ""
|
|
|
|
|
|
try:
|
|
|
|
|
|
return json.dumps(agent_state, ensure_ascii=False, sort_keys=True)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return str(agent_state)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-01 22:28:45 +08:00
|
|
|
|
def _metadata_thread_id(metadata: dict | None, fallback: str | None = None) -> str | None:
|
|
|
|
|
|
if not isinstance(metadata, dict):
|
|
|
|
|
|
return fallback
|
|
|
|
|
|
|
|
|
|
|
|
for source in (
|
|
|
|
|
|
metadata,
|
|
|
|
|
|
metadata.get("configurable"),
|
|
|
|
|
|
metadata.get("metadata"),
|
|
|
|
|
|
metadata.get("stream_event"),
|
|
|
|
|
|
):
|
|
|
|
|
|
if isinstance(source, dict):
|
|
|
|
|
|
value = source.get("thread_id")
|
|
|
|
|
|
if isinstance(value, str) and value.strip():
|
|
|
|
|
|
return value.strip()
|
|
|
|
|
|
return fallback
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _metadata_namespace(metadata: dict | None) -> list[str]:
|
|
|
|
|
|
if not isinstance(metadata, dict):
|
|
|
|
|
|
return []
|
|
|
|
|
|
namespace = metadata.get("namespace")
|
|
|
|
|
|
if isinstance(namespace, list):
|
|
|
|
|
|
return [str(item) for item in namespace]
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _json_safe(value: Any) -> Any:
|
|
|
|
|
|
if value is None or isinstance(value, str | int | float | bool):
|
|
|
|
|
|
return value
|
|
|
|
|
|
if isinstance(value, dict):
|
|
|
|
|
|
return {str(key): _json_safe(child) for key, child in value.items()}
|
|
|
|
|
|
if isinstance(value, list | tuple):
|
|
|
|
|
|
return [_json_safe(child) for child in value]
|
|
|
|
|
|
if hasattr(value, "model_dump"):
|
|
|
|
|
|
return _json_safe(value.model_dump())
|
|
|
|
|
|
return str(value)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-05 21:02:13 +08:00
|
|
|
|
def _apply_model_override(input_context: dict, meta: dict | None) -> None:
|
|
|
|
|
|
"""对话级模型覆盖:meta.model_spec 优先于智能体配置的 model。值已在创建 run 时校验。"""
|
|
|
|
|
|
model_spec = (meta or {}).get("model_spec")
|
|
|
|
|
|
if model_spec:
|
|
|
|
|
|
input_context["model"] = model_spec
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-01 22:28:45 +08:00
|
|
|
|
def _stream_message_key(metadata: dict | None, namespace: list[str], thread_id: str | None) -> tuple[str, str]:
|
|
|
|
|
|
if not isinstance(metadata, dict):
|
|
|
|
|
|
return thread_id or "", "/".join(namespace)
|
|
|
|
|
|
return thread_id or "", str(metadata.get("run_id") or metadata.get("langgraph_node") or "/".join(namespace))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _stream_message_id(
|
|
|
|
|
|
message_ids: dict[tuple[str, str], str],
|
|
|
|
|
|
key: tuple[str, str],
|
|
|
|
|
|
preferred: str | None = None,
|
|
|
|
|
|
) -> str:
|
|
|
|
|
|
if preferred:
|
|
|
|
|
|
message_ids[key] = preferred
|
|
|
|
|
|
return preferred
|
|
|
|
|
|
return message_ids.setdefault(key, str(uuid.uuid4()))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _message_chunk_yuxi_events(
|
|
|
|
|
|
msg_dict: dict[str, Any],
|
|
|
|
|
|
*,
|
|
|
|
|
|
message_id: str,
|
|
|
|
|
|
thread_id: str | None,
|
|
|
|
|
|
namespace: list[str],
|
|
|
|
|
|
) -> list[dict[str, Any]]:
|
|
|
|
|
|
events: list[dict[str, Any]] = []
|
|
|
|
|
|
route = {"thread_id": thread_id, "namespace": namespace}
|
|
|
|
|
|
content = msg_dict.get("content")
|
|
|
|
|
|
additional_kwargs = msg_dict.get("additional_kwargs") if isinstance(msg_dict.get("additional_kwargs"), dict) else {}
|
|
|
|
|
|
reasoning_content = msg_dict.get("reasoning_content")
|
|
|
|
|
|
additional_reasoning_content = additional_kwargs.get("reasoning_content")
|
|
|
|
|
|
|
|
|
|
|
|
message_event: dict[str, Any] = {"type": "message_delta", "message_id": message_id, **route}
|
|
|
|
|
|
if isinstance(content, str) and content:
|
|
|
|
|
|
message_event["content"] = content
|
|
|
|
|
|
if isinstance(reasoning_content, str) and reasoning_content:
|
|
|
|
|
|
message_event["reasoning_content"] = reasoning_content
|
|
|
|
|
|
if isinstance(additional_reasoning_content, str) and additional_reasoning_content:
|
|
|
|
|
|
message_event["additional_reasoning_content"] = additional_reasoning_content
|
|
|
|
|
|
if len(message_event) > 4:
|
|
|
|
|
|
events.append(message_event)
|
|
|
|
|
|
|
|
|
|
|
|
tool_call_chunks = msg_dict.get("tool_call_chunks")
|
|
|
|
|
|
if isinstance(tool_call_chunks, list):
|
|
|
|
|
|
for tool_call_chunk in tool_call_chunks:
|
|
|
|
|
|
if not isinstance(tool_call_chunk, dict):
|
|
|
|
|
|
continue
|
|
|
|
|
|
args_delta = tool_call_chunk.get("args")
|
|
|
|
|
|
if args_delta is None:
|
|
|
|
|
|
args_delta = ""
|
|
|
|
|
|
elif not isinstance(args_delta, str):
|
|
|
|
|
|
args_delta = json.dumps(args_delta, ensure_ascii=False)
|
|
|
|
|
|
if not tool_call_chunk.get("id") and not tool_call_chunk.get("name") and not args_delta:
|
|
|
|
|
|
continue
|
|
|
|
|
|
events.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"type": "tool_call_delta",
|
|
|
|
|
|
"message_id": message_id,
|
|
|
|
|
|
"tool_call_id": tool_call_chunk.get("id"),
|
|
|
|
|
|
"name": tool_call_chunk.get("name"),
|
|
|
|
|
|
"args_delta": args_delta,
|
|
|
|
|
|
"index": tool_call_chunk.get("index") if tool_call_chunk.get("index") is not None else 0,
|
|
|
|
|
|
**route,
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
return events
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _protocol_event_yuxi_event(
|
|
|
|
|
|
event: dict[str, Any],
|
|
|
|
|
|
*,
|
|
|
|
|
|
message_id: str | None,
|
|
|
|
|
|
thread_id: str | None,
|
|
|
|
|
|
namespace: list[str],
|
|
|
|
|
|
) -> dict[str, Any] | None:
|
|
|
|
|
|
event_name = event.get("event")
|
|
|
|
|
|
if event_name in {"message-start", "content-block-start", "message-finish"} or not message_id:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
route = {"thread_id": thread_id, "namespace": namespace}
|
|
|
|
|
|
if event_name == "content-block-delta":
|
|
|
|
|
|
delta = event.get("delta") if isinstance(event.get("delta"), dict) else {}
|
|
|
|
|
|
text = delta.get("text")
|
|
|
|
|
|
if delta.get("type") == "text-delta" and isinstance(text, str) and text:
|
|
|
|
|
|
return {"type": "message_delta", "message_id": message_id, "content": text, **route}
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
if event_name == "content-block-finish":
|
|
|
|
|
|
content = event.get("content") if isinstance(event.get("content"), dict) else {}
|
|
|
|
|
|
if content.get("type") != "tool_call" or not content.get("id") and not content.get("name"):
|
|
|
|
|
|
return None
|
|
|
|
|
|
return {
|
|
|
|
|
|
"type": "tool_call",
|
|
|
|
|
|
"message_id": message_id,
|
|
|
|
|
|
"tool_call_id": content.get("id"),
|
|
|
|
|
|
"name": content.get("name"),
|
|
|
|
|
|
"args": content.get("args") if content.get("args") is not None else {},
|
|
|
|
|
|
"index": event.get("index") if event.get("index") is not None else 0,
|
|
|
|
|
|
**route,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _stream_event_response(event: dict[str, Any]) -> str:
|
|
|
|
|
|
if event.get("type") != "message_delta":
|
|
|
|
|
|
return ""
|
|
|
|
|
|
return str(event.get("content") or "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _message_payload_yuxi_events(
|
|
|
|
|
|
msg: Any,
|
|
|
|
|
|
*,
|
|
|
|
|
|
metadata: dict[str, Any],
|
|
|
|
|
|
namespace: list[str],
|
|
|
|
|
|
thread_id: str | None,
|
|
|
|
|
|
protocol_message_ids: dict[tuple[str, str], str],
|
|
|
|
|
|
) -> list[dict[str, Any]]:
|
|
|
|
|
|
message_key = _stream_message_key(metadata, namespace, thread_id)
|
|
|
|
|
|
if isinstance(msg, dict) and isinstance(msg.get("event"), str):
|
|
|
|
|
|
preferred_message_id = str(msg["id"]) if msg.get("event") == "message-start" and msg.get("id") else None
|
|
|
|
|
|
message_id = _stream_message_id(protocol_message_ids, message_key, preferred_message_id)
|
|
|
|
|
|
stream_event = _protocol_event_yuxi_event(
|
|
|
|
|
|
msg,
|
|
|
|
|
|
message_id=message_id,
|
|
|
|
|
|
thread_id=thread_id,
|
|
|
|
|
|
namespace=namespace,
|
|
|
|
|
|
)
|
|
|
|
|
|
return [stream_event] if stream_event else []
|
|
|
|
|
|
|
|
|
|
|
|
if isinstance(msg, AIMessageChunk) or hasattr(msg, "model_dump"):
|
|
|
|
|
|
msg_dict = msg.model_dump()
|
|
|
|
|
|
elif isinstance(msg, dict):
|
|
|
|
|
|
msg_dict = dict(msg)
|
|
|
|
|
|
else:
|
|
|
|
|
|
msg_dict = {"content": str(msg)}
|
|
|
|
|
|
|
|
|
|
|
|
message_id = str(msg_dict.get("id") or _stream_message_id(protocol_message_ids, message_key))
|
|
|
|
|
|
return _message_chunk_yuxi_events(
|
|
|
|
|
|
msg_dict,
|
|
|
|
|
|
message_id=message_id,
|
|
|
|
|
|
thread_id=thread_id,
|
|
|
|
|
|
namespace=namespace,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-01 03:17:43 +08:00
|
|
|
|
async def _stream_agent_events(agent, messages, *, input_context=None, **kwargs):
|
|
|
|
|
|
if hasattr(agent, "stream_messages_with_state"):
|
|
|
|
|
|
async for mode, payload in agent.stream_messages_with_state(
|
|
|
|
|
|
messages,
|
|
|
|
|
|
input_context=input_context,
|
|
|
|
|
|
**kwargs,
|
|
|
|
|
|
):
|
|
|
|
|
|
yield mode, payload
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
async for msg, metadata in agent.stream_messages(messages, input_context=input_context, **kwargs):
|
|
|
|
|
|
yield "messages", (msg, metadata)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-22 00:28:43 +08:00
|
|
|
|
async def _get_existing_message_ids(conv_repo: ConversationRepository, thread_id: str) -> set[str]:
|
|
|
|
|
|
existing_messages = await conv_repo.get_messages_by_thread_id(thread_id)
|
|
|
|
|
|
return {
|
|
|
|
|
|
msg.extra_metadata["id"]
|
|
|
|
|
|
for msg in existing_messages
|
|
|
|
|
|
if msg.extra_metadata and "id" in msg.extra_metadata and isinstance(msg.extra_metadata["id"], str)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-31 09:58:16 +08:00
|
|
|
|
async def _save_ai_message(
|
|
|
|
|
|
conv_repo: ConversationRepository,
|
|
|
|
|
|
thread_id: str,
|
|
|
|
|
|
msg_dict: dict,
|
|
|
|
|
|
trace_info: dict[str, Any] | None = None,
|
|
|
|
|
|
) -> None:
|
2026-01-22 00:28:43 +08:00
|
|
|
|
content = msg_dict.get("content", "")
|
2026-06-01 22:28:45 +08:00
|
|
|
|
tool_calls_data = msg_dict.get("tool_calls") or []
|
|
|
|
|
|
if isinstance(content, list):
|
|
|
|
|
|
if not tool_calls_data:
|
|
|
|
|
|
tool_calls_data = [
|
|
|
|
|
|
{"id": item.get("id"), "name": item.get("name"), "args": item.get("args") or {}}
|
|
|
|
|
|
for item in content
|
|
|
|
|
|
if isinstance(item, dict) and item.get("type") == "tool_call"
|
|
|
|
|
|
]
|
|
|
|
|
|
content = "\n".join(
|
|
|
|
|
|
item.get("text", "") for item in content if isinstance(item, dict) and isinstance(item.get("text"), str)
|
|
|
|
|
|
)
|
|
|
|
|
|
elif not isinstance(content, str):
|
|
|
|
|
|
content = str(content)
|
2026-03-31 09:58:16 +08:00
|
|
|
|
extra_metadata = dict(msg_dict)
|
|
|
|
|
|
if trace_info:
|
|
|
|
|
|
extra_metadata.update(trace_info)
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
|
|
|
|
|
ai_msg = await conv_repo.add_message_by_thread_id(
|
|
|
|
|
|
thread_id=thread_id,
|
|
|
|
|
|
role="assistant",
|
|
|
|
|
|
content=content,
|
|
|
|
|
|
message_type="text",
|
2026-03-31 09:58:16 +08:00
|
|
|
|
extra_metadata=extra_metadata,
|
2026-01-22 00:28:43 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if ai_msg and tool_calls_data:
|
|
|
|
|
|
for tc in tool_calls_data:
|
|
|
|
|
|
await conv_repo.add_tool_call(
|
|
|
|
|
|
message_id=ai_msg.id,
|
|
|
|
|
|
tool_name=tc.get("name", "unknown"),
|
|
|
|
|
|
tool_input=tc.get("args", {}),
|
|
|
|
|
|
status="pending",
|
|
|
|
|
|
langgraph_tool_call_id=tc.get("id"),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _save_tool_message(conv_repo: ConversationRepository, msg_dict: dict) -> None:
|
|
|
|
|
|
tool_call_id = msg_dict.get("tool_call_id")
|
|
|
|
|
|
content = msg_dict.get("content", "")
|
|
|
|
|
|
|
|
|
|
|
|
if not tool_call_id:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
if isinstance(content, list):
|
|
|
|
|
|
tool_output = json.dumps(content) if content else ""
|
|
|
|
|
|
else:
|
|
|
|
|
|
tool_output = str(content)
|
|
|
|
|
|
|
|
|
|
|
|
await conv_repo.update_tool_call_output(
|
|
|
|
|
|
langgraph_tool_call_id=tool_call_id,
|
|
|
|
|
|
tool_output=tool_output,
|
|
|
|
|
|
status="success",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def save_partial_message(
|
|
|
|
|
|
conv_repo: ConversationRepository,
|
|
|
|
|
|
thread_id: str,
|
|
|
|
|
|
full_msg=None,
|
|
|
|
|
|
error_message: str | None = None,
|
|
|
|
|
|
error_type: str = "interrupted",
|
2026-03-31 09:58:16 +08:00
|
|
|
|
trace_info: dict[str, Any] | None = None,
|
2026-01-22 00:28:43 +08:00
|
|
|
|
):
|
|
|
|
|
|
try:
|
|
|
|
|
|
extra_metadata = {
|
|
|
|
|
|
"error_type": error_type,
|
|
|
|
|
|
"is_error": True,
|
|
|
|
|
|
"error_message": error_message or f"发生错误: {error_type}",
|
|
|
|
|
|
}
|
|
|
|
|
|
if full_msg:
|
|
|
|
|
|
msg_dict = full_msg.model_dump() if hasattr(full_msg, "model_dump") else {}
|
|
|
|
|
|
content = full_msg.content if hasattr(full_msg, "content") else str(full_msg)
|
|
|
|
|
|
extra_metadata = msg_dict | extra_metadata
|
|
|
|
|
|
else:
|
|
|
|
|
|
content = ""
|
|
|
|
|
|
|
2026-03-31 09:58:16 +08:00
|
|
|
|
if trace_info:
|
|
|
|
|
|
extra_metadata.update(trace_info)
|
|
|
|
|
|
|
2026-01-22 00:28:43 +08:00
|
|
|
|
return await conv_repo.add_message_by_thread_id(
|
|
|
|
|
|
thread_id=thread_id,
|
|
|
|
|
|
role="assistant",
|
|
|
|
|
|
content=content,
|
|
|
|
|
|
message_type="text",
|
|
|
|
|
|
extra_metadata=extra_metadata,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2026-05-29 22:19:58 +08:00
|
|
|
|
logger.exception(f"Error saving message: {e}")
|
2026-01-22 00:28:43 +08:00
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def save_messages_from_langgraph_state(
|
|
|
|
|
|
agent_instance,
|
|
|
|
|
|
thread_id: str,
|
|
|
|
|
|
conv_repo: ConversationRepository,
|
|
|
|
|
|
config_dict: dict,
|
2026-03-31 09:58:16 +08:00
|
|
|
|
trace_info: dict[str, Any] | None = None,
|
2026-01-22 00:28:43 +08:00
|
|
|
|
) -> None:
|
2026-04-26 21:43:25 +08:00
|
|
|
|
messages = await _get_langgraph_messages(agent_instance, config_dict)
|
|
|
|
|
|
if messages is None:
|
|
|
|
|
|
return
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
2026-04-26 21:43:25 +08:00
|
|
|
|
existing_ids = await _get_existing_message_ids(conv_repo, thread_id)
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
2026-04-26 21:43:25 +08:00
|
|
|
|
for msg in messages:
|
2026-06-01 22:28:45 +08:00
|
|
|
|
if hasattr(msg, "model_dump"):
|
|
|
|
|
|
msg_dict = msg.model_dump()
|
|
|
|
|
|
elif isinstance(msg, dict):
|
|
|
|
|
|
msg_dict = dict(msg)
|
|
|
|
|
|
else:
|
|
|
|
|
|
continue
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
2026-06-01 22:28:45 +08:00
|
|
|
|
msg_type = msg_dict.get("type", "unknown")
|
|
|
|
|
|
if msg_type == "unknown":
|
|
|
|
|
|
role = msg_dict.get("role")
|
|
|
|
|
|
if role in {"assistant", "ai"}:
|
|
|
|
|
|
msg_type = "ai"
|
|
|
|
|
|
elif role in {"user", "human"}:
|
|
|
|
|
|
msg_type = "human"
|
|
|
|
|
|
elif role == "tool":
|
|
|
|
|
|
msg_type = "tool"
|
|
|
|
|
|
|
|
|
|
|
|
msg_id = getattr(msg, "id", None) or msg_dict.get("id")
|
|
|
|
|
|
if msg_type == "human" or msg_id in existing_ids:
|
2026-04-26 21:43:25 +08:00
|
|
|
|
continue
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
2026-04-26 21:43:25 +08:00
|
|
|
|
if msg_type == "ai":
|
|
|
|
|
|
await _save_ai_message(conv_repo, thread_id, msg_dict, trace_info=trace_info)
|
|
|
|
|
|
elif msg_type == "tool":
|
|
|
|
|
|
await _save_tool_message(conv_repo, msg_dict)
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
2026-04-27 02:24:52 +08:00
|
|
|
|
|
2026-03-07 23:28:25 +08:00
|
|
|
|
def _extract_interrupt_info(state) -> Any | None:
|
2026-02-25 16:30:36 +08:00
|
|
|
|
"""从 LangGraph state 中提取中断信息"""
|
|
|
|
|
|
if hasattr(state, "tasks") and state.tasks:
|
|
|
|
|
|
for task in state.tasks:
|
|
|
|
|
|
if hasattr(task, "interrupts") and task.interrupts:
|
|
|
|
|
|
return task.interrupts[0]
|
|
|
|
|
|
|
|
|
|
|
|
interrupt_data = state.values.get("__interrupt__")
|
|
|
|
|
|
if isinstance(interrupt_data, list) and interrupt_data:
|
|
|
|
|
|
return interrupt_data[0]
|
|
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-07 23:28:25 +08:00
|
|
|
|
def _coerce_interrupt_payload(info: Any) -> dict:
|
|
|
|
|
|
"""将 LangGraph interrupt 对象转换为 dict 结构。"""
|
2026-02-25 16:30:36 +08:00
|
|
|
|
if isinstance(info, dict):
|
2026-03-07 23:28:25 +08:00
|
|
|
|
return info
|
|
|
|
|
|
|
|
|
|
|
|
payload = getattr(info, "value", None)
|
|
|
|
|
|
if isinstance(payload, dict):
|
|
|
|
|
|
return payload
|
|
|
|
|
|
|
2026-03-17 03:39:48 +08:00
|
|
|
|
questions = getattr(info, "questions", None)
|
|
|
|
|
|
source = getattr(info, "source", None)
|
2026-03-07 23:28:25 +08:00
|
|
|
|
result: dict[str, Any] = {}
|
2026-03-17 03:39:48 +08:00
|
|
|
|
if isinstance(questions, list):
|
|
|
|
|
|
result["questions"] = questions
|
|
|
|
|
|
if isinstance(source, str) and source.strip():
|
|
|
|
|
|
result["source"] = source
|
2026-03-07 23:28:25 +08:00
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _build_ask_user_question_payload(info: Any, thread_id: str) -> dict[str, Any]:
|
|
|
|
|
|
"""将 interrupt 信息标准化为 ask_user_question_required 载荷。"""
|
|
|
|
|
|
payload = _coerce_interrupt_payload(info)
|
|
|
|
|
|
|
2026-03-17 03:39:48 +08:00
|
|
|
|
questions = _normalize_interrupt_questions(payload.get("questions"))
|
|
|
|
|
|
|
|
|
|
|
|
if not questions:
|
|
|
|
|
|
questions = [
|
|
|
|
|
|
{
|
|
|
|
|
|
"question_id": str(uuid.uuid4()),
|
|
|
|
|
|
"question": "请选择一个选项",
|
|
|
|
|
|
"options": [],
|
|
|
|
|
|
"multi_select": False,
|
|
|
|
|
|
"allow_other": True,
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
2026-03-07 23:28:25 +08:00
|
|
|
|
|
2026-03-17 03:39:48 +08:00
|
|
|
|
source = str(payload.get("source") or payload.get("tool_name") or "interrupt")
|
2026-03-07 23:28:25 +08:00
|
|
|
|
|
|
|
|
|
|
return {
|
2026-03-17 03:39:48 +08:00
|
|
|
|
"questions": questions,
|
2026-03-07 23:28:25 +08:00
|
|
|
|
"source": source,
|
|
|
|
|
|
"thread_id": thread_id,
|
|
|
|
|
|
}
|
2026-02-25 16:30:36 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ensure_full_msg(full_msg: AIMessage | None, accumulated_content: list[str]) -> AIMessage | None:
|
|
|
|
|
|
"""如果 full_msg 为空且有累积内容,构建 AIMessage"""
|
|
|
|
|
|
if not full_msg and accumulated_content:
|
|
|
|
|
|
return AIMessage(content="".join(accumulated_content))
|
|
|
|
|
|
return full_msg
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-26 23:13:44 +08:00
|
|
|
|
def _extract_ai_message(messages: list[Any] | None) -> AIMessage | None:
|
|
|
|
|
|
"""从消息列表中提取最后一条 AIMessage。"""
|
|
|
|
|
|
if not isinstance(messages, list):
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
for msg in reversed(messages):
|
|
|
|
|
|
if isinstance(msg, AIMessage):
|
|
|
|
|
|
return msg
|
|
|
|
|
|
|
|
|
|
|
|
msg_dict = msg.model_dump() if hasattr(msg, "model_dump") else {}
|
|
|
|
|
|
if msg_dict.get("type") == "ai":
|
|
|
|
|
|
content = msg_dict.get("content", "")
|
|
|
|
|
|
return msg if hasattr(msg, "content") else AIMessage(content=content)
|
|
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-24 00:46:07 +08:00
|
|
|
|
async def _resolve_agent_runtime(
|
|
|
|
|
|
*,
|
|
|
|
|
|
db,
|
|
|
|
|
|
user: User,
|
|
|
|
|
|
requested_agent_id: str | None,
|
|
|
|
|
|
thread_id: str | None,
|
|
|
|
|
|
) -> tuple[Agent, Any, dict]:
|
|
|
|
|
|
agent_repo = AgentRepository(db)
|
|
|
|
|
|
conv_repo = ConversationRepository(db)
|
|
|
|
|
|
bound_agent_id = requested_agent_id
|
2026-03-26 23:13:44 +08:00
|
|
|
|
|
2026-05-24 00:46:07 +08:00
|
|
|
|
if thread_id:
|
|
|
|
|
|
conversation = await conv_repo.get_conversation_by_thread_id(thread_id)
|
|
|
|
|
|
if conversation:
|
|
|
|
|
|
if conversation.uid != str(user.uid) or conversation.status == "deleted":
|
|
|
|
|
|
raise ValueError("对话线程不存在")
|
|
|
|
|
|
if requested_agent_id and requested_agent_id != conversation.agent_id:
|
|
|
|
|
|
raise ValueError("已有线程已绑定智能体,不能切换")
|
|
|
|
|
|
bound_agent_id = conversation.agent_id
|
2026-03-26 23:13:44 +08:00
|
|
|
|
|
2026-05-24 00:46:07 +08:00
|
|
|
|
if not bound_agent_id:
|
|
|
|
|
|
raise ValueError("缺少必需的 agent_id 字段")
|
2026-03-18 22:55:52 +08:00
|
|
|
|
|
2026-05-24 00:46:07 +08:00
|
|
|
|
agent_item = await agent_repo.get_visible_by_slug(slug=bound_agent_id, user=user)
|
|
|
|
|
|
if not agent_item:
|
|
|
|
|
|
raise ValueError("智能体不存在或无权限访问")
|
2026-02-25 16:30:36 +08:00
|
|
|
|
|
2026-05-24 00:46:07 +08:00
|
|
|
|
backend = agent_manager.get_agent(agent_item.backend_id)
|
|
|
|
|
|
if not backend:
|
|
|
|
|
|
raise ValueError(f"智能体后端 {agent_item.backend_id} 不存在")
|
2026-02-25 16:30:36 +08:00
|
|
|
|
|
2026-05-24 00:46:07 +08:00
|
|
|
|
agent_config = await normalize_agent_context_config(
|
|
|
|
|
|
(agent_item.config_json or {}).get("context", {}),
|
2026-05-19 18:37:29 +08:00
|
|
|
|
db=db,
|
|
|
|
|
|
user=user,
|
2026-05-24 00:46:07 +08:00
|
|
|
|
context_schema=backend.context_schema,
|
2026-05-19 18:37:29 +08:00
|
|
|
|
)
|
2026-05-24 00:46:07 +08:00
|
|
|
|
return agent_item, backend, agent_config
|
2026-02-25 16:30:36 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-01-22 00:28:43 +08:00
|
|
|
|
async def check_and_handle_interrupts(
|
|
|
|
|
|
agent,
|
|
|
|
|
|
langgraph_config: dict,
|
|
|
|
|
|
make_chunk,
|
|
|
|
|
|
meta: dict,
|
|
|
|
|
|
thread_id: str,
|
|
|
|
|
|
) -> AsyncIterator[bytes]:
|
|
|
|
|
|
try:
|
|
|
|
|
|
graph = await agent.get_graph()
|
|
|
|
|
|
state = await graph.aget_state(langgraph_config)
|
|
|
|
|
|
|
|
|
|
|
|
if not state or not state.values:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2026-02-25 16:30:36 +08:00
|
|
|
|
interrupt_info = _extract_interrupt_info(state)
|
2026-01-22 00:28:43 +08:00
|
|
|
|
if interrupt_info:
|
2026-03-07 23:28:25 +08:00
|
|
|
|
question_payload = _build_ask_user_question_payload(interrupt_info, thread_id)
|
|
|
|
|
|
meta["interrupt"] = question_payload
|
|
|
|
|
|
yield make_chunk(status="ask_user_question_required", meta=meta, **question_payload)
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2026-05-29 22:19:58 +08:00
|
|
|
|
logger.exception(f"Error checking interrupts: {e}")
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-05-24 00:46:07 +08:00
|
|
|
|
async def _ensure_thread_bound_agent(
|
2026-03-27 01:15:51 +08:00
|
|
|
|
*,
|
|
|
|
|
|
conv_repo: ConversationRepository,
|
|
|
|
|
|
thread_id: str,
|
2026-05-17 22:44:05 +08:00
|
|
|
|
uid: str,
|
2026-05-24 00:46:07 +08:00
|
|
|
|
agent_item: Agent,
|
2026-03-27 01:15:51 +08:00
|
|
|
|
) -> None:
|
|
|
|
|
|
conversation = await conv_repo.get_conversation_by_thread_id(thread_id)
|
|
|
|
|
|
if not conversation:
|
2026-05-24 00:46:07 +08:00
|
|
|
|
await conv_repo.create_conversation(
|
2026-05-17 22:44:05 +08:00
|
|
|
|
uid=uid,
|
2026-05-24 00:46:07 +08:00
|
|
|
|
agent_id=agent_item.slug,
|
2026-03-27 01:15:51 +08:00
|
|
|
|
thread_id=thread_id,
|
2026-05-24 00:46:07 +08:00
|
|
|
|
metadata={"backend_id": agent_item.backend_id},
|
2026-03-27 01:15:51 +08:00
|
|
|
|
)
|
2026-05-24 00:46:07 +08:00
|
|
|
|
return
|
2026-03-27 01:15:51 +08:00
|
|
|
|
|
2026-05-24 00:46:07 +08:00
|
|
|
|
if conversation.agent_id != agent_item.slug:
|
|
|
|
|
|
raise ValueError("已有线程已绑定智能体,不能切换")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_attachment_file_ids(meta: dict | None) -> list[str]:
|
|
|
|
|
|
file_ids = (meta or {}).get("attachment_file_ids") or []
|
|
|
|
|
|
if not isinstance(file_ids, list):
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
normalized = []
|
|
|
|
|
|
seen = set()
|
|
|
|
|
|
for file_id in file_ids:
|
|
|
|
|
|
value = str(file_id).strip()
|
|
|
|
|
|
if not value or value in seen:
|
|
|
|
|
|
continue
|
|
|
|
|
|
seen.add(value)
|
|
|
|
|
|
normalized.append(value)
|
|
|
|
|
|
return normalized
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _bind_request_attachments(
|
|
|
|
|
|
*,
|
|
|
|
|
|
conv_repo: ConversationRepository,
|
|
|
|
|
|
thread_id: str,
|
|
|
|
|
|
request_id: str,
|
|
|
|
|
|
attachment_file_ids: list[str],
|
|
|
|
|
|
) -> list[dict]:
|
|
|
|
|
|
conversation = await conv_repo.get_conversation_by_thread_id(thread_id)
|
|
|
|
|
|
if not conversation:
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
if attachment_file_ids:
|
|
|
|
|
|
attachments = await conv_repo.bind_attachments_to_request(conversation.id, request_id, attachment_file_ids)
|
|
|
|
|
|
else:
|
|
|
|
|
|
attachments = await conv_repo.get_attachments_by_request_id(conversation.id, request_id)
|
|
|
|
|
|
|
|
|
|
|
|
return [serialize_attachment(attachment) for attachment in attachments]
|
2026-03-27 01:15:51 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-03-26 00:26:45 +08:00
|
|
|
|
async def agent_chat(
|
|
|
|
|
|
*,
|
|
|
|
|
|
query: str,
|
2026-05-24 00:46:07 +08:00
|
|
|
|
agent_id: str,
|
2026-03-26 23:13:44 +08:00
|
|
|
|
thread_id: str | None,
|
2026-03-26 00:26:45 +08:00
|
|
|
|
meta: dict,
|
|
|
|
|
|
image_content: str | None,
|
|
|
|
|
|
current_user,
|
|
|
|
|
|
db,
|
|
|
|
|
|
) -> dict:
|
|
|
|
|
|
"""非流式对话,返回完整响应"""
|
|
|
|
|
|
start_time = asyncio.get_event_loop().time()
|
|
|
|
|
|
|
|
|
|
|
|
if image_content:
|
|
|
|
|
|
human_message = HumanMessage(
|
|
|
|
|
|
content=[
|
|
|
|
|
|
{"type": "text", "text": query},
|
|
|
|
|
|
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_content}"}},
|
|
|
|
|
|
]
|
|
|
|
|
|
)
|
|
|
|
|
|
message_type = "multimodal_image"
|
|
|
|
|
|
else:
|
|
|
|
|
|
human_message = HumanMessage(content=query)
|
|
|
|
|
|
message_type = "text"
|
|
|
|
|
|
|
|
|
|
|
|
if conf.enable_content_guard and await content_guard.check(query):
|
|
|
|
|
|
return {
|
|
|
|
|
|
"status": "error",
|
|
|
|
|
|
"error_type": "content_guard_blocked",
|
|
|
|
|
|
"error_message": "输入内容包含敏感词",
|
|
|
|
|
|
"request_id": meta.get("request_id"),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 22:44:05 +08:00
|
|
|
|
uid = str(current_user.uid)
|
2026-03-26 23:13:44 +08:00
|
|
|
|
meta = dict(meta or {})
|
|
|
|
|
|
if "request_id" not in meta or not meta.get("request_id"):
|
|
|
|
|
|
logger.warning("请求缺少 request_id,已自动生成一个新的 request_id")
|
|
|
|
|
|
meta["request_id"] = str(uuid.uuid4())
|
|
|
|
|
|
|
2026-05-24 00:46:07 +08:00
|
|
|
|
if not thread_id:
|
|
|
|
|
|
thread_id = str(uuid.uuid4())
|
|
|
|
|
|
logger.warning(f"No thread_id provided, generated new thread_id: {thread_id}")
|
|
|
|
|
|
|
2026-03-26 00:26:45 +08:00
|
|
|
|
try:
|
2026-05-24 00:46:07 +08:00
|
|
|
|
agent_item, agent, agent_config = await _resolve_agent_runtime(
|
|
|
|
|
|
db=db,
|
|
|
|
|
|
user=current_user,
|
|
|
|
|
|
requested_agent_id=agent_id,
|
|
|
|
|
|
thread_id=thread_id,
|
|
|
|
|
|
)
|
2026-03-26 00:26:45 +08:00
|
|
|
|
except ValueError as e:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"status": "error",
|
2026-05-24 00:46:07 +08:00
|
|
|
|
"error_type": "invalid_agent",
|
2026-03-26 00:26:45 +08:00
|
|
|
|
"error_message": str(e),
|
|
|
|
|
|
"request_id": meta.get("request_id"),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 23:13:44 +08:00
|
|
|
|
meta.update(
|
|
|
|
|
|
{
|
|
|
|
|
|
"query": query,
|
2026-05-24 00:46:07 +08:00
|
|
|
|
"agent_id": agent_item.slug,
|
|
|
|
|
|
"backend_id": agent_item.backend_id,
|
|
|
|
|
|
"server_model_name": agent_item.backend_id,
|
2026-03-26 23:13:44 +08:00
|
|
|
|
"thread_id": thread_id,
|
2026-05-17 22:44:05 +08:00
|
|
|
|
"uid": current_user.uid,
|
2026-03-26 23:13:44 +08:00
|
|
|
|
"has_image": bool(image_content),
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
messages = [human_message]
|
2026-06-02 18:47:28 +08:00
|
|
|
|
input_context = await build_agent_input_context(
|
|
|
|
|
|
agent_config,
|
|
|
|
|
|
thread_id=thread_id,
|
|
|
|
|
|
uid=uid,
|
|
|
|
|
|
run_id=meta.get("run_id"),
|
|
|
|
|
|
request_id=meta.get("request_id"),
|
|
|
|
|
|
)
|
2026-06-05 21:02:13 +08:00
|
|
|
|
_apply_model_override(input_context, meta)
|
2026-03-31 09:58:16 +08:00
|
|
|
|
langfuse_run = _build_langfuse_run_context(
|
|
|
|
|
|
current_user=current_user,
|
|
|
|
|
|
thread_id=thread_id,
|
2026-05-24 00:46:07 +08:00
|
|
|
|
agent_id=agent_item.slug,
|
|
|
|
|
|
backend_id=agent_item.backend_id,
|
2026-03-31 09:58:16 +08:00
|
|
|
|
request_id=meta["request_id"],
|
|
|
|
|
|
operation="agent_chat_sync",
|
|
|
|
|
|
message_type=message_type,
|
|
|
|
|
|
)
|
|
|
|
|
|
trace_info: dict[str, Any] = {}
|
2026-03-26 00:26:45 +08:00
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
conv_repo = ConversationRepository(db)
|
2026-05-24 00:46:07 +08:00
|
|
|
|
await _ensure_thread_bound_agent(
|
2026-03-27 01:15:51 +08:00
|
|
|
|
conv_repo=conv_repo,
|
|
|
|
|
|
thread_id=thread_id,
|
2026-05-17 22:44:05 +08:00
|
|
|
|
uid=uid,
|
2026-05-24 00:46:07 +08:00
|
|
|
|
agent_item=agent_item,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
request_attachments = await _bind_request_attachments(
|
|
|
|
|
|
conv_repo=conv_repo,
|
|
|
|
|
|
thread_id=thread_id,
|
|
|
|
|
|
request_id=meta["request_id"],
|
|
|
|
|
|
attachment_file_ids=_normalize_attachment_file_ids(meta),
|
2026-03-27 01:15:51 +08:00
|
|
|
|
)
|
2026-03-26 00:26:45 +08:00
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
await conv_repo.add_message_by_thread_id(
|
|
|
|
|
|
thread_id=thread_id,
|
|
|
|
|
|
role="user",
|
|
|
|
|
|
content=query,
|
|
|
|
|
|
message_type=message_type,
|
|
|
|
|
|
image_content=image_content,
|
2026-04-19 23:44:46 +08:00
|
|
|
|
extra_metadata={
|
|
|
|
|
|
"raw_message": human_message.model_dump(),
|
|
|
|
|
|
"request_id": meta.get("request_id"),
|
2026-05-24 00:46:07 +08:00
|
|
|
|
"attachments": request_attachments,
|
2026-04-19 23:44:46 +08:00
|
|
|
|
},
|
2026-03-26 00:26:45 +08:00
|
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Error saving user message: {e}")
|
|
|
|
|
|
|
2026-05-17 22:44:05 +08:00
|
|
|
|
langgraph_config = {"configurable": {"thread_id": thread_id, "uid": uid}}
|
2026-03-31 09:58:16 +08:00
|
|
|
|
invoke_result = await agent.invoke_messages(
|
|
|
|
|
|
messages,
|
|
|
|
|
|
input_context=input_context,
|
|
|
|
|
|
callbacks=langfuse_run.callbacks,
|
|
|
|
|
|
metadata=langfuse_run.metadata,
|
|
|
|
|
|
tags=langfuse_run.tags,
|
|
|
|
|
|
)
|
2026-03-26 23:13:44 +08:00
|
|
|
|
full_msg = _extract_ai_message(invoke_result.get("messages") if isinstance(invoke_result, dict) else None)
|
2026-03-31 09:58:16 +08:00
|
|
|
|
trace_info = get_trace_info(langfuse_run)
|
2026-03-26 00:26:45 +08:00
|
|
|
|
|
2026-03-26 23:13:44 +08:00
|
|
|
|
if full_msg is None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
graph = await agent.get_graph()
|
|
|
|
|
|
state = await graph.aget_state(langgraph_config)
|
|
|
|
|
|
full_msg = _extract_ai_message(getattr(state, "values", {}).get("messages", [])) if state else None
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
full_msg = None
|
2026-03-26 00:26:45 +08:00
|
|
|
|
|
2026-03-26 23:13:44 +08:00
|
|
|
|
full_content = full_msg.content if full_msg else ""
|
2026-03-26 00:26:45 +08:00
|
|
|
|
|
|
|
|
|
|
if conf.enable_content_guard and await content_guard.check(full_content):
|
2026-03-31 09:58:16 +08:00
|
|
|
|
await save_partial_message(
|
|
|
|
|
|
conv_repo,
|
|
|
|
|
|
thread_id,
|
|
|
|
|
|
full_msg,
|
|
|
|
|
|
"content_guard_blocked",
|
|
|
|
|
|
trace_info=trace_info,
|
|
|
|
|
|
)
|
2026-03-26 00:26:45 +08:00
|
|
|
|
return {
|
|
|
|
|
|
"status": "interrupted",
|
|
|
|
|
|
"message": "检测到敏感内容,已中断输出",
|
|
|
|
|
|
"request_id": meta.get("request_id"),
|
|
|
|
|
|
"time_cost": asyncio.get_event_loop().time() - start_time,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
graph = await agent.get_graph()
|
|
|
|
|
|
state = await graph.aget_state(langgraph_config)
|
|
|
|
|
|
agent_state = extract_agent_state(getattr(state, "values", {})) if state else {}
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
agent_state = {}
|
|
|
|
|
|
|
2026-04-26 21:43:25 +08:00
|
|
|
|
try:
|
|
|
|
|
|
await save_messages_from_langgraph_state(
|
|
|
|
|
|
agent_instance=agent,
|
|
|
|
|
|
thread_id=thread_id,
|
|
|
|
|
|
conv_repo=conv_repo,
|
|
|
|
|
|
config_dict=langgraph_config,
|
|
|
|
|
|
trace_info=trace_info,
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
2026-05-29 22:19:58 +08:00
|
|
|
|
logger.exception(f"Error saving messages from LangGraph state: {e}")
|
2026-04-26 21:43:25 +08:00
|
|
|
|
return {
|
|
|
|
|
|
"status": "error",
|
|
|
|
|
|
"error_type": "save_message_error",
|
|
|
|
|
|
"error_message": f"消息保存失败: {e}",
|
|
|
|
|
|
"request_id": meta.get("request_id"),
|
|
|
|
|
|
}
|
2026-03-26 00:26:45 +08:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"status": "finished",
|
|
|
|
|
|
"response": full_content,
|
|
|
|
|
|
"request_id": meta.get("request_id"),
|
|
|
|
|
|
"thread_id": thread_id,
|
|
|
|
|
|
"agent_state": agent_state,
|
|
|
|
|
|
"time_cost": asyncio.get_event_loop().time() - start_time,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2026-05-29 22:19:58 +08:00
|
|
|
|
logger.exception(f"Error in agent_chat: {e}")
|
2026-03-26 00:26:45 +08:00
|
|
|
|
return {
|
|
|
|
|
|
"status": "error",
|
|
|
|
|
|
"error_type": "unexpected_error",
|
|
|
|
|
|
"error_message": str(e),
|
|
|
|
|
|
"request_id": meta.get("request_id"),
|
|
|
|
|
|
}
|
2026-03-31 09:58:16 +08:00
|
|
|
|
finally:
|
|
|
|
|
|
flush_langfuse()
|
2026-03-26 00:26:45 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-01-22 00:28:43 +08:00
|
|
|
|
async def stream_agent_chat(
|
|
|
|
|
|
*,
|
|
|
|
|
|
query: str,
|
2026-05-24 00:46:07 +08:00
|
|
|
|
agent_id: str,
|
2026-03-26 23:13:44 +08:00
|
|
|
|
thread_id: str | None,
|
2026-01-22 00:28:43 +08:00
|
|
|
|
meta: dict,
|
|
|
|
|
|
image_content: str | None,
|
|
|
|
|
|
current_user,
|
|
|
|
|
|
db,
|
2026-05-28 12:53:29 +08:00
|
|
|
|
save_user_message: bool = True,
|
2026-01-22 00:28:43 +08:00
|
|
|
|
) -> AsyncIterator[bytes]:
|
|
|
|
|
|
start_time = asyncio.get_event_loop().time()
|
|
|
|
|
|
|
|
|
|
|
|
def make_chunk(content=None, **kwargs):
|
2026-06-01 22:28:45 +08:00
|
|
|
|
chunk_thread_id = kwargs.pop("thread_id", None) or meta.get("thread_id") or thread_id
|
2026-01-22 00:28:43 +08:00
|
|
|
|
return (
|
|
|
|
|
|
json.dumps(
|
2026-06-01 22:28:45 +08:00
|
|
|
|
{"request_id": meta.get("request_id"), "response": content, "thread_id": chunk_thread_id, **kwargs},
|
|
|
|
|
|
ensure_ascii=False,
|
2026-01-22 00:28:43 +08:00
|
|
|
|
).encode("utf-8")
|
|
|
|
|
|
+ b"\n"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-24 00:46:07 +08:00
|
|
|
|
meta = dict(meta or {})
|
|
|
|
|
|
if "request_id" not in meta or not meta.get("request_id"):
|
|
|
|
|
|
logger.warning("请求缺少 request_id,已自动生成一个新的 request_id")
|
|
|
|
|
|
meta["request_id"] = str(uuid.uuid4())
|
|
|
|
|
|
|
|
|
|
|
|
uid = str(current_user.uid)
|
|
|
|
|
|
if not thread_id:
|
|
|
|
|
|
thread_id = str(uuid.uuid4())
|
|
|
|
|
|
logger.warning(f"No thread_id provided, generated new thread_id: {thread_id}")
|
|
|
|
|
|
|
2026-01-22 00:28:43 +08:00
|
|
|
|
if image_content:
|
|
|
|
|
|
human_message = HumanMessage(
|
|
|
|
|
|
content=[
|
|
|
|
|
|
{"type": "text", "text": query},
|
|
|
|
|
|
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_content}"}},
|
|
|
|
|
|
]
|
|
|
|
|
|
)
|
|
|
|
|
|
message_type = "multimodal_image"
|
|
|
|
|
|
else:
|
|
|
|
|
|
human_message = HumanMessage(content=query)
|
|
|
|
|
|
message_type = "text"
|
|
|
|
|
|
|
|
|
|
|
|
if conf.enable_content_guard and await content_guard.check(query):
|
|
|
|
|
|
yield make_chunk(
|
|
|
|
|
|
status="error", error_type="content_guard_blocked", error_message="输入内容包含敏感词", meta=meta
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2026-03-26 23:13:44 +08:00
|
|
|
|
try:
|
2026-05-24 00:46:07 +08:00
|
|
|
|
agent_item, agent, agent_config = await _resolve_agent_runtime(
|
|
|
|
|
|
db=db,
|
|
|
|
|
|
user=current_user,
|
|
|
|
|
|
requested_agent_id=agent_id,
|
|
|
|
|
|
thread_id=thread_id,
|
|
|
|
|
|
)
|
2026-03-26 23:13:44 +08:00
|
|
|
|
except ValueError as e:
|
2026-05-24 00:46:07 +08:00
|
|
|
|
yield make_chunk(status="error", error_type="invalid_agent", error_message=str(e), meta=meta)
|
2026-03-26 23:13:44 +08:00
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
meta.update(
|
|
|
|
|
|
{
|
|
|
|
|
|
"query": query,
|
2026-05-24 00:46:07 +08:00
|
|
|
|
"agent_id": agent_item.slug,
|
|
|
|
|
|
"backend_id": agent_item.backend_id,
|
|
|
|
|
|
"server_model_name": agent_item.backend_id,
|
2026-03-26 23:13:44 +08:00
|
|
|
|
"thread_id": thread_id,
|
2026-05-17 22:44:05 +08:00
|
|
|
|
"uid": current_user.uid,
|
2026-03-26 23:13:44 +08:00
|
|
|
|
"has_image": bool(image_content),
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-22 00:28:43 +08:00
|
|
|
|
messages = [human_message]
|
2026-06-02 18:47:28 +08:00
|
|
|
|
input_context = await build_agent_input_context(
|
|
|
|
|
|
agent_config,
|
|
|
|
|
|
thread_id=thread_id,
|
|
|
|
|
|
uid=uid,
|
|
|
|
|
|
run_id=meta.get("run_id"),
|
|
|
|
|
|
request_id=meta.get("request_id"),
|
|
|
|
|
|
)
|
2026-06-05 21:02:13 +08:00
|
|
|
|
_apply_model_override(input_context, meta)
|
2026-03-31 09:58:16 +08:00
|
|
|
|
langfuse_run = _build_langfuse_run_context(
|
|
|
|
|
|
current_user=current_user,
|
|
|
|
|
|
thread_id=thread_id,
|
2026-05-24 00:46:07 +08:00
|
|
|
|
agent_id=agent_item.slug,
|
|
|
|
|
|
backend_id=agent_item.backend_id,
|
2026-03-31 09:58:16 +08:00
|
|
|
|
request_id=meta["request_id"],
|
|
|
|
|
|
operation="agent_chat_stream",
|
|
|
|
|
|
message_type=message_type,
|
|
|
|
|
|
)
|
2026-03-07 23:28:25 +08:00
|
|
|
|
full_msg = None
|
|
|
|
|
|
accumulated_content: list[str] = []
|
2026-03-31 09:58:16 +08:00
|
|
|
|
trace_info: dict[str, Any] = {}
|
2026-04-01 03:17:43 +08:00
|
|
|
|
last_agent_state_signature = ""
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
conv_repo = ConversationRepository(db)
|
2026-05-24 00:46:07 +08:00
|
|
|
|
await _ensure_thread_bound_agent(
|
2026-03-27 01:15:51 +08:00
|
|
|
|
conv_repo=conv_repo,
|
|
|
|
|
|
thread_id=thread_id,
|
2026-05-17 22:44:05 +08:00
|
|
|
|
uid=uid,
|
2026-05-24 00:46:07 +08:00
|
|
|
|
agent_item=agent_item,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
request_attachments = await _bind_request_attachments(
|
|
|
|
|
|
conv_repo=conv_repo,
|
|
|
|
|
|
thread_id=thread_id,
|
|
|
|
|
|
request_id=meta["request_id"],
|
|
|
|
|
|
attachment_file_ids=_normalize_attachment_file_ids(meta),
|
2026-03-27 01:15:51 +08:00
|
|
|
|
)
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
2026-05-24 00:46:07 +08:00
|
|
|
|
init_msg = {
|
|
|
|
|
|
"role": "user",
|
|
|
|
|
|
"content": query,
|
|
|
|
|
|
"type": "human",
|
|
|
|
|
|
"message_type": message_type,
|
|
|
|
|
|
"extra_metadata": {
|
|
|
|
|
|
"request_id": meta.get("request_id"),
|
|
|
|
|
|
"attachments": request_attachments,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
if image_content:
|
|
|
|
|
|
init_msg["image_content"] = image_content
|
|
|
|
|
|
yield make_chunk(status="init", meta=meta, msg=init_msg)
|
|
|
|
|
|
|
2026-05-28 12:53:29 +08:00
|
|
|
|
if save_user_message:
|
|
|
|
|
|
try:
|
|
|
|
|
|
await conv_repo.add_message_by_thread_id(
|
|
|
|
|
|
thread_id=thread_id,
|
|
|
|
|
|
role="user",
|
|
|
|
|
|
content=query,
|
|
|
|
|
|
message_type=message_type,
|
|
|
|
|
|
image_content=image_content,
|
|
|
|
|
|
extra_metadata={
|
|
|
|
|
|
"raw_message": human_message.model_dump(),
|
|
|
|
|
|
"request_id": meta.get("request_id"),
|
|
|
|
|
|
"attachments": request_attachments,
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Error saving user message: {e}")
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
2026-02-13 22:16:11 +08:00
|
|
|
|
# 先构建 langgraph_config
|
2026-05-17 22:44:05 +08:00
|
|
|
|
langgraph_config = {"configurable": {"thread_id": thread_id, "uid": uid}}
|
2026-02-13 22:16:11 +08:00
|
|
|
|
|
2026-03-05 22:50:38 +08:00
|
|
|
|
# LangGraph 会自动从 checkpointer 恢复 state(包括 uploads)
|
2026-02-13 22:16:11 +08:00
|
|
|
|
# 无需手动加载或传递
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
|
|
|
|
|
full_msg = None
|
|
|
|
|
|
accumulated_content = []
|
2026-06-01 22:28:45 +08:00
|
|
|
|
protocol_message_ids: dict[tuple[str, str], str] = {}
|
2026-04-01 03:17:43 +08:00
|
|
|
|
async for mode, payload in _stream_agent_events(
|
|
|
|
|
|
agent,
|
2026-03-31 09:58:16 +08:00
|
|
|
|
messages,
|
|
|
|
|
|
input_context=input_context,
|
|
|
|
|
|
callbacks=langfuse_run.callbacks,
|
|
|
|
|
|
metadata=langfuse_run.metadata,
|
|
|
|
|
|
tags=langfuse_run.tags,
|
|
|
|
|
|
):
|
2026-04-01 03:17:43 +08:00
|
|
|
|
if mode == "values":
|
|
|
|
|
|
agent_state = extract_agent_state(payload if isinstance(payload, dict) else {})
|
|
|
|
|
|
signature = _agent_state_signature(agent_state)
|
|
|
|
|
|
if signature and signature != last_agent_state_signature:
|
|
|
|
|
|
last_agent_state_signature = signature
|
|
|
|
|
|
yield make_chunk(status="agent_state", agent_state=agent_state, meta=meta)
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
2026-06-01 22:28:45 +08:00
|
|
|
|
if mode == "stream_event":
|
|
|
|
|
|
yield make_chunk(
|
|
|
|
|
|
status="stream_event",
|
|
|
|
|
|
event=payload,
|
|
|
|
|
|
namespace=payload.get("namespace") if isinstance(payload, dict) else [],
|
|
|
|
|
|
meta=meta,
|
|
|
|
|
|
thread_id=payload.get("thread_id") if isinstance(payload, dict) else None,
|
|
|
|
|
|
)
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
2026-04-01 03:17:43 +08:00
|
|
|
|
msg, metadata = payload
|
2026-06-01 22:28:45 +08:00
|
|
|
|
namespace = _metadata_namespace(metadata)
|
|
|
|
|
|
chunk_thread_id = _metadata_thread_id(metadata, thread_id if not namespace else None)
|
|
|
|
|
|
if namespace and not chunk_thread_id:
|
|
|
|
|
|
continue
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
2026-06-01 22:28:45 +08:00
|
|
|
|
is_subagent_chunk = bool(chunk_thread_id and chunk_thread_id != thread_id)
|
|
|
|
|
|
stream_events = _message_payload_yuxi_events(
|
|
|
|
|
|
msg,
|
|
|
|
|
|
metadata=metadata,
|
|
|
|
|
|
namespace=namespace,
|
|
|
|
|
|
thread_id=chunk_thread_id,
|
|
|
|
|
|
protocol_message_ids=protocol_message_ids,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
for stream_event in stream_events:
|
|
|
|
|
|
content = _stream_event_response(stream_event)
|
|
|
|
|
|
if not is_subagent_chunk and content:
|
|
|
|
|
|
trace_info = get_trace_info(langfuse_run)
|
|
|
|
|
|
accumulated_content.append(content)
|
|
|
|
|
|
content_for_check = "".join(accumulated_content[-10:])
|
|
|
|
|
|
if conf.enable_content_guard and await content_guard.check_with_keywords(content_for_check):
|
|
|
|
|
|
full_msg = AIMessage(content="".join(accumulated_content))
|
|
|
|
|
|
await save_partial_message(
|
|
|
|
|
|
conv_repo,
|
|
|
|
|
|
thread_id,
|
|
|
|
|
|
full_msg,
|
|
|
|
|
|
"content_guard_blocked",
|
|
|
|
|
|
trace_info=trace_info,
|
|
|
|
|
|
)
|
|
|
|
|
|
meta["time_cost"] = asyncio.get_event_loop().time() - start_time
|
|
|
|
|
|
yield make_chunk(status="interrupted", message="检测到敏感内容,已中断输出", meta=meta)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
yield make_chunk(
|
|
|
|
|
|
content=content,
|
|
|
|
|
|
stream_event=stream_event,
|
|
|
|
|
|
metadata=metadata,
|
|
|
|
|
|
status="loading",
|
|
|
|
|
|
thread_id=chunk_thread_id,
|
|
|
|
|
|
)
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
2026-02-25 16:30:36 +08:00
|
|
|
|
full_msg = _ensure_full_msg(full_msg, accumulated_content)
|
2026-03-31 09:58:16 +08:00
|
|
|
|
trace_info = get_trace_info(langfuse_run)
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
|
|
|
|
|
if conf.enable_content_guard and hasattr(full_msg, "content") and await content_guard.check(full_msg.content):
|
2026-03-31 09:58:16 +08:00
|
|
|
|
await save_partial_message(
|
|
|
|
|
|
conv_repo,
|
|
|
|
|
|
thread_id,
|
|
|
|
|
|
full_msg,
|
|
|
|
|
|
"content_guard_blocked",
|
|
|
|
|
|
trace_info=trace_info,
|
|
|
|
|
|
)
|
2026-01-22 00:28:43 +08:00
|
|
|
|
meta["time_cost"] = asyncio.get_event_loop().time() - start_time
|
|
|
|
|
|
yield make_chunk(status="interrupted", message="检测到敏感内容,已中断输出", meta=meta)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2026-06-02 18:47:28 +08:00
|
|
|
|
interrupted = False
|
2026-01-22 00:28:43 +08:00
|
|
|
|
async for chunk in check_and_handle_interrupts(agent, langgraph_config, make_chunk, meta, thread_id):
|
2026-06-02 18:47:28 +08:00
|
|
|
|
interrupted = True
|
2026-01-22 00:28:43 +08:00
|
|
|
|
yield chunk
|
|
|
|
|
|
|
|
|
|
|
|
meta["time_cost"] = asyncio.get_event_loop().time() - start_time
|
|
|
|
|
|
try:
|
|
|
|
|
|
graph = await agent.get_graph()
|
|
|
|
|
|
state = await graph.aget_state(langgraph_config)
|
|
|
|
|
|
agent_state = extract_agent_state(getattr(state, "values", {})) if state else {}
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
agent_state = {}
|
|
|
|
|
|
|
2026-04-01 03:17:43 +08:00
|
|
|
|
final_signature = _agent_state_signature(agent_state)
|
|
|
|
|
|
if final_signature and final_signature != last_agent_state_signature:
|
|
|
|
|
|
last_agent_state_signature = final_signature
|
2026-01-22 00:28:43 +08:00
|
|
|
|
yield make_chunk(status="agent_state", agent_state=agent_state, meta=meta)
|
|
|
|
|
|
|
2026-02-25 12:17:02 +08:00
|
|
|
|
# 先存储数据库,再返回 finished,避免前端查询时数据未落库
|
2026-04-26 21:43:25 +08:00
|
|
|
|
try:
|
|
|
|
|
|
await save_messages_from_langgraph_state(
|
|
|
|
|
|
agent_instance=agent,
|
|
|
|
|
|
thread_id=thread_id,
|
|
|
|
|
|
conv_repo=conv_repo,
|
|
|
|
|
|
config_dict=langgraph_config,
|
|
|
|
|
|
trace_info=trace_info,
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
2026-05-29 22:19:58 +08:00
|
|
|
|
logger.exception(f"Error saving messages from LangGraph state: {e}")
|
2026-04-26 21:43:25 +08:00
|
|
|
|
yield make_chunk(status="warning", message=f"消息保存失败: {e}", meta=meta)
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
2026-06-02 18:47:28 +08:00
|
|
|
|
if interrupted:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2026-02-25 12:17:02 +08:00
|
|
|
|
yield make_chunk(status="finished", meta=meta)
|
|
|
|
|
|
|
2026-01-22 00:28:43 +08:00
|
|
|
|
except (asyncio.CancelledError, ConnectionError) as e:
|
|
|
|
|
|
logger.warning(f"Client disconnected, cancelling stream: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
async def save_cleanup():
|
|
|
|
|
|
nonlocal full_msg
|
2026-02-25 16:30:36 +08:00
|
|
|
|
full_msg = _ensure_full_msg(full_msg, accumulated_content)
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
|
|
|
|
|
async with pg_manager.get_async_session_context() as new_db:
|
|
|
|
|
|
new_conv_repo = ConversationRepository(new_db)
|
|
|
|
|
|
await save_partial_message(
|
|
|
|
|
|
new_conv_repo,
|
|
|
|
|
|
thread_id,
|
|
|
|
|
|
full_msg=full_msg,
|
|
|
|
|
|
error_message="对话已中断" if not full_msg else None,
|
|
|
|
|
|
error_type="interrupted",
|
2026-03-31 09:58:16 +08:00
|
|
|
|
trace_info=trace_info,
|
2026-01-22 00:28:43 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
cleanup_task = asyncio.create_task(save_cleanup())
|
|
|
|
|
|
try:
|
|
|
|
|
|
await asyncio.shield(cleanup_task)
|
|
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
logger.error(f"Error during cleanup save: {exc}")
|
|
|
|
|
|
|
|
|
|
|
|
yield make_chunk(status="interrupted", message="对话已中断", meta=meta)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2026-05-29 22:19:58 +08:00
|
|
|
|
logger.exception(f"Error streaming messages: {e}")
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
|
|
|
|
|
error_msg = f"Error streaming messages: {e}"
|
|
|
|
|
|
error_type = "unexpected_error"
|
|
|
|
|
|
|
2026-02-25 16:30:36 +08:00
|
|
|
|
full_msg = _ensure_full_msg(full_msg, accumulated_content)
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
|
|
|
|
|
async with pg_manager.get_async_session_context() as new_db:
|
|
|
|
|
|
new_conv_repo = ConversationRepository(new_db)
|
|
|
|
|
|
await save_partial_message(
|
|
|
|
|
|
new_conv_repo,
|
|
|
|
|
|
thread_id,
|
|
|
|
|
|
full_msg=full_msg,
|
|
|
|
|
|
error_message=error_msg,
|
|
|
|
|
|
error_type=error_type,
|
2026-03-31 09:58:16 +08:00
|
|
|
|
trace_info=trace_info,
|
2026-01-22 00:28:43 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
yield make_chunk(status="error", error_type=error_type, error_message=error_msg, meta=meta)
|
2026-03-31 09:58:16 +08:00
|
|
|
|
finally:
|
|
|
|
|
|
flush_langfuse()
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def stream_agent_resume(
|
|
|
|
|
|
*,
|
|
|
|
|
|
thread_id: str,
|
2026-03-07 23:28:25 +08:00
|
|
|
|
resume_input: Any,
|
2026-01-22 00:28:43 +08:00
|
|
|
|
meta: dict,
|
|
|
|
|
|
current_user,
|
|
|
|
|
|
db,
|
|
|
|
|
|
) -> AsyncIterator[bytes]:
|
|
|
|
|
|
start_time = asyncio.get_event_loop().time()
|
|
|
|
|
|
|
|
|
|
|
|
def make_resume_chunk(content=None, **kwargs):
|
2026-06-01 22:28:45 +08:00
|
|
|
|
chunk_thread_id = kwargs.pop("thread_id", None) or meta.get("thread_id") or thread_id
|
2026-01-22 00:28:43 +08:00
|
|
|
|
return (
|
|
|
|
|
|
json.dumps(
|
2026-06-01 22:28:45 +08:00
|
|
|
|
{"request_id": meta.get("request_id"), "response": content, "thread_id": chunk_thread_id, **kwargs},
|
|
|
|
|
|
ensure_ascii=False,
|
2026-01-22 00:28:43 +08:00
|
|
|
|
).encode("utf-8")
|
|
|
|
|
|
+ b"\n"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-28 12:53:29 +08:00
|
|
|
|
yield make_resume_chunk(status="init", meta=meta)
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
2026-03-07 23:28:25 +08:00
|
|
|
|
resume_command = Command(resume=resume_input)
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
2026-05-17 22:44:05 +08:00
|
|
|
|
uid = str(current_user.uid)
|
2026-03-25 03:36:08 +08:00
|
|
|
|
try:
|
2026-05-24 00:46:07 +08:00
|
|
|
|
agent_item, agent, agent_config = await _resolve_agent_runtime(
|
|
|
|
|
|
db=db,
|
|
|
|
|
|
user=current_user,
|
|
|
|
|
|
requested_agent_id=None,
|
|
|
|
|
|
thread_id=thread_id,
|
2026-05-19 18:37:29 +08:00
|
|
|
|
)
|
2026-03-25 03:36:08 +08:00
|
|
|
|
except ValueError as e:
|
2026-05-24 00:46:07 +08:00
|
|
|
|
yield make_resume_chunk(status="error", error_type="invalid_agent", error_message=str(e), meta=meta)
|
2026-03-25 03:36:08 +08:00
|
|
|
|
return
|
2026-01-22 05:57:13 +08:00
|
|
|
|
|
2026-05-24 00:46:07 +08:00
|
|
|
|
meta["agent_id"] = agent_item.slug
|
|
|
|
|
|
meta["backend_id"] = agent_item.backend_id
|
2026-06-02 18:47:28 +08:00
|
|
|
|
input_context = await build_agent_input_context(
|
|
|
|
|
|
agent_config or {},
|
|
|
|
|
|
thread_id=thread_id,
|
|
|
|
|
|
uid=uid,
|
|
|
|
|
|
run_id=meta.get("run_id"),
|
|
|
|
|
|
request_id=meta.get("request_id"),
|
|
|
|
|
|
)
|
2026-06-05 21:02:13 +08:00
|
|
|
|
_apply_model_override(input_context, meta)
|
2026-01-22 05:57:13 +08:00
|
|
|
|
context = agent.context_schema()
|
2026-06-02 18:47:28 +08:00
|
|
|
|
context.update(input_context)
|
2026-03-31 09:58:16 +08:00
|
|
|
|
langfuse_run = _build_langfuse_run_context(
|
|
|
|
|
|
current_user=current_user,
|
|
|
|
|
|
thread_id=thread_id,
|
2026-05-24 00:46:07 +08:00
|
|
|
|
agent_id=agent_item.slug,
|
|
|
|
|
|
backend_id=agent_item.backend_id,
|
2026-03-31 09:58:16 +08:00
|
|
|
|
request_id=meta.get("request_id") or str(uuid.uuid4()),
|
|
|
|
|
|
operation="agent_chat_resume",
|
|
|
|
|
|
message_type="resume",
|
|
|
|
|
|
)
|
|
|
|
|
|
trace_info: dict[str, Any] = {}
|
2026-04-01 04:16:22 +08:00
|
|
|
|
last_agent_state_signature = ""
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
2026-06-01 22:28:45 +08:00
|
|
|
|
stream_source = agent.stream_resume_with_state(
|
2026-01-22 05:57:13 +08:00
|
|
|
|
resume_command,
|
2026-06-02 18:47:28 +08:00
|
|
|
|
input_context=input_context,
|
2026-06-01 22:28:45 +08:00
|
|
|
|
callbacks=langfuse_run.callbacks,
|
|
|
|
|
|
metadata=langfuse_run.metadata,
|
|
|
|
|
|
tags=langfuse_run.tags,
|
2026-01-22 00:28:43 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-01 22:28:45 +08:00
|
|
|
|
protocol_message_ids: dict[tuple[str, str], str] = {}
|
|
|
|
|
|
|
2026-01-22 00:28:43 +08:00
|
|
|
|
try:
|
2026-04-01 03:17:43 +08:00
|
|
|
|
async for mode, payload in stream_source:
|
|
|
|
|
|
if mode == "values":
|
|
|
|
|
|
agent_state = extract_agent_state(payload if isinstance(payload, dict) else {})
|
|
|
|
|
|
signature = _agent_state_signature(agent_state)
|
|
|
|
|
|
if signature and signature != last_agent_state_signature:
|
|
|
|
|
|
last_agent_state_signature = signature
|
|
|
|
|
|
yield make_resume_chunk(status="agent_state", agent_state=agent_state, meta=meta)
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
2026-06-01 22:28:45 +08:00
|
|
|
|
if mode == "stream_event":
|
|
|
|
|
|
event_payload = payload if isinstance(payload, dict) else {}
|
|
|
|
|
|
yield make_resume_chunk(
|
|
|
|
|
|
status="stream_event",
|
|
|
|
|
|
event=event_payload,
|
|
|
|
|
|
namespace=event_payload.get("namespace") or [],
|
|
|
|
|
|
meta=meta,
|
|
|
|
|
|
thread_id=event_payload.get("thread_id"),
|
|
|
|
|
|
)
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
if mode != "messages":
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
2026-04-01 03:17:43 +08:00
|
|
|
|
msg, metadata = payload
|
2026-06-01 22:28:45 +08:00
|
|
|
|
metadata = dict(metadata or {})
|
|
|
|
|
|
namespace = _metadata_namespace(metadata)
|
|
|
|
|
|
chunk_thread_id = _metadata_thread_id(metadata, thread_id if not namespace else None)
|
|
|
|
|
|
if namespace and not chunk_thread_id:
|
|
|
|
|
|
continue
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
2026-06-01 22:28:45 +08:00
|
|
|
|
if chunk_thread_id == thread_id:
|
|
|
|
|
|
trace_info = get_trace_info(langfuse_run)
|
|
|
|
|
|
|
|
|
|
|
|
stream_events = _message_payload_yuxi_events(
|
|
|
|
|
|
msg,
|
|
|
|
|
|
metadata=metadata,
|
|
|
|
|
|
namespace=namespace,
|
|
|
|
|
|
thread_id=chunk_thread_id,
|
|
|
|
|
|
protocol_message_ids=protocol_message_ids,
|
2026-01-22 00:28:43 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-01 22:28:45 +08:00
|
|
|
|
for stream_event in stream_events:
|
|
|
|
|
|
content = _stream_event_response(stream_event)
|
|
|
|
|
|
yield make_resume_chunk(
|
|
|
|
|
|
content=content,
|
|
|
|
|
|
stream_event=stream_event,
|
|
|
|
|
|
metadata=metadata,
|
|
|
|
|
|
status="loading",
|
|
|
|
|
|
thread_id=chunk_thread_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-17 22:44:05 +08:00
|
|
|
|
langgraph_config = {"configurable": {"thread_id": thread_id, "uid": uid}}
|
2026-06-02 18:47:28 +08:00
|
|
|
|
interrupted = False
|
2026-01-22 00:28:43 +08:00
|
|
|
|
async for chunk in check_and_handle_interrupts(agent, langgraph_config, make_resume_chunk, meta, thread_id):
|
2026-06-02 18:47:28 +08:00
|
|
|
|
interrupted = True
|
2026-01-22 00:28:43 +08:00
|
|
|
|
yield chunk
|
|
|
|
|
|
|
|
|
|
|
|
meta["time_cost"] = asyncio.get_event_loop().time() - start_time
|
|
|
|
|
|
|
2026-04-01 03:17:43 +08:00
|
|
|
|
try:
|
2026-06-01 22:28:45 +08:00
|
|
|
|
graph = await agent.get_graph(context=context)
|
2026-04-01 03:17:43 +08:00
|
|
|
|
state = await graph.aget_state(langgraph_config)
|
|
|
|
|
|
agent_state = extract_agent_state(getattr(state, "values", {})) if state else {}
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
agent_state = {}
|
|
|
|
|
|
|
|
|
|
|
|
final_signature = _agent_state_signature(agent_state)
|
|
|
|
|
|
if final_signature and final_signature != last_agent_state_signature:
|
|
|
|
|
|
yield make_resume_chunk(status="agent_state", agent_state=agent_state, meta=meta)
|
|
|
|
|
|
|
2026-02-25 12:17:02 +08:00
|
|
|
|
# 先存储数据库,再返回 finished,避免前端查询时数据未落库
|
2026-01-22 00:28:43 +08:00
|
|
|
|
conv_repo = ConversationRepository(db)
|
2026-04-26 21:43:25 +08:00
|
|
|
|
try:
|
|
|
|
|
|
await save_messages_from_langgraph_state(
|
|
|
|
|
|
agent_instance=agent,
|
|
|
|
|
|
thread_id=thread_id,
|
|
|
|
|
|
conv_repo=conv_repo,
|
|
|
|
|
|
config_dict=langgraph_config,
|
|
|
|
|
|
trace_info=trace_info,
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
2026-05-29 22:19:58 +08:00
|
|
|
|
logger.exception(f"Error saving messages from LangGraph state: {e}")
|
2026-04-27 02:24:52 +08:00
|
|
|
|
yield make_resume_chunk(status="warning", message=f"消息保存失败: {e}", meta=meta)
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
2026-06-02 18:47:28 +08:00
|
|
|
|
if interrupted:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2026-02-25 12:17:02 +08:00
|
|
|
|
yield make_resume_chunk(status="finished", meta=meta)
|
|
|
|
|
|
|
2026-01-22 00:28:43 +08:00
|
|
|
|
except (asyncio.CancelledError, ConnectionError) as e:
|
|
|
|
|
|
logger.warning(f"Client disconnected during resume: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
async with pg_manager.get_async_session_context() as new_db:
|
|
|
|
|
|
new_conv_repo = ConversationRepository(new_db)
|
|
|
|
|
|
await save_partial_message(
|
2026-03-31 09:58:16 +08:00
|
|
|
|
new_conv_repo,
|
|
|
|
|
|
thread_id,
|
|
|
|
|
|
error_message="对话恢复已中断",
|
|
|
|
|
|
error_type="resume_interrupted",
|
|
|
|
|
|
trace_info=trace_info,
|
2026-01-22 00:28:43 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
yield make_resume_chunk(status="interrupted", message="对话恢复已中断", meta=meta)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2026-05-29 22:19:58 +08:00
|
|
|
|
logger.exception(f"Error during resume: {e}")
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
|
|
|
|
|
async with pg_manager.get_async_session_context() as new_db:
|
|
|
|
|
|
new_conv_repo = ConversationRepository(new_db)
|
|
|
|
|
|
await save_partial_message(
|
2026-03-31 09:58:16 +08:00
|
|
|
|
new_conv_repo,
|
|
|
|
|
|
thread_id,
|
|
|
|
|
|
error_message=f"Error during resume: {e}",
|
|
|
|
|
|
error_type="resume_error",
|
|
|
|
|
|
trace_info=trace_info,
|
2026-01-22 00:28:43 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
yield make_resume_chunk(message=f"Error during resume: {e}", status="error")
|
2026-03-31 09:58:16 +08:00
|
|
|
|
finally:
|
|
|
|
|
|
flush_langfuse()
|
2026-01-22 00:28:43 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-06-01 22:28:45 +08:00
|
|
|
|
def _serialize_state_messages(values: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
|
|
|
messages = values.get("messages") if isinstance(values, dict) else None
|
|
|
|
|
|
if not isinstance(messages, list):
|
|
|
|
|
|
return []
|
|
|
|
|
|
serialized = []
|
|
|
|
|
|
for message in messages:
|
|
|
|
|
|
if hasattr(message, "model_dump"):
|
|
|
|
|
|
serialized.append(message.model_dump())
|
|
|
|
|
|
elif isinstance(message, dict):
|
|
|
|
|
|
serialized.append(dict(message))
|
|
|
|
|
|
else:
|
|
|
|
|
|
serialized.append({"type": "unknown", "content": str(message)})
|
|
|
|
|
|
return serialized
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _read_checkpoint_state(agent, *, uid: str, thread_id: str):
|
|
|
|
|
|
graph = await agent.get_graph()
|
|
|
|
|
|
langgraph_config = {"configurable": {"uid": uid, "thread_id": thread_id}}
|
|
|
|
|
|
return await graph.aget_state(langgraph_config)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-02 18:47:28 +08:00
|
|
|
|
def _serialize_subagent_run(run) -> dict[str, Any]:
|
|
|
|
|
|
payload = run.input_payload if isinstance(run.input_payload, dict) else {}
|
|
|
|
|
|
return {
|
|
|
|
|
|
"id": payload.get("tool_call_id") or run.id,
|
|
|
|
|
|
"run_id": run.id,
|
|
|
|
|
|
"subagent_type": payload.get("subagent_type") or run.agent_id,
|
|
|
|
|
|
"subagent_name": payload.get("subagent_name"),
|
|
|
|
|
|
"child_thread_id": payload.get("child_thread_id") or run.thread_id,
|
|
|
|
|
|
"description": payload.get("description"),
|
|
|
|
|
|
"status": run.status,
|
|
|
|
|
|
"created_at": run.to_dict().get("created_at"),
|
|
|
|
|
|
"completed_at": run.to_dict().get("finished_at"),
|
|
|
|
|
|
"result_preview": payload.get("result_preview"),
|
|
|
|
|
|
"error": run.error_message,
|
|
|
|
|
|
"parent_agent_run_id": run.parent_agent_run_id,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-22 00:28:43 +08:00
|
|
|
|
async def get_agent_state_view(
|
|
|
|
|
|
*,
|
|
|
|
|
|
thread_id: str,
|
2026-05-17 22:44:05 +08:00
|
|
|
|
current_uid: str,
|
2026-01-22 00:28:43 +08:00
|
|
|
|
db,
|
2026-06-01 22:28:45 +08:00
|
|
|
|
include_messages: bool = False,
|
2026-01-22 00:28:43 +08:00
|
|
|
|
) -> dict:
|
2026-05-24 00:46:07 +08:00
|
|
|
|
from fastapi import HTTPException
|
|
|
|
|
|
|
2026-01-22 00:28:43 +08:00
|
|
|
|
conv_repo = ConversationRepository(db)
|
2026-06-01 22:28:45 +08:00
|
|
|
|
agent_repo = AgentRepository(db)
|
2026-01-22 00:28:43 +08:00
|
|
|
|
conversation = await conv_repo.get_conversation_by_thread_id(thread_id)
|
2026-06-01 22:28:45 +08:00
|
|
|
|
if conversation:
|
|
|
|
|
|
if conversation.uid != str(current_uid) or conversation.status == "deleted":
|
|
|
|
|
|
raise HTTPException(status_code=404, detail="对话线程不存在")
|
|
|
|
|
|
|
|
|
|
|
|
agent_item = await agent_repo.get_by_slug(conversation.agent_id)
|
|
|
|
|
|
if not agent_item:
|
|
|
|
|
|
raise HTTPException(status_code=404, detail="智能体不存在")
|
|
|
|
|
|
agent = agent_manager.get_agent(agent_item.backend_id)
|
|
|
|
|
|
if not agent:
|
|
|
|
|
|
raise HTTPException(status_code=404, detail="智能体后端不存在")
|
|
|
|
|
|
state = await _read_checkpoint_state(agent, uid=str(current_uid), thread_id=thread_id)
|
|
|
|
|
|
values = getattr(state, "values", {}) if state else {}
|
|
|
|
|
|
response = {"agent_state": extract_agent_state(values)}
|
|
|
|
|
|
if include_messages:
|
|
|
|
|
|
response["messages"] = _serialize_state_messages(values)
|
|
|
|
|
|
return response
|
|
|
|
|
|
|
2026-06-02 18:47:28 +08:00
|
|
|
|
run_repo = AgentRunRepository(db)
|
|
|
|
|
|
subagent_run = await run_repo.get_latest_subagent_run_by_thread_for_user(thread_id, str(current_uid))
|
|
|
|
|
|
if not subagent_run or not subagent_run.parent_agent_run_id:
|
|
|
|
|
|
raise HTTPException(status_code=404, detail="对话线程不存在")
|
|
|
|
|
|
|
|
|
|
|
|
parent_run = await run_repo.get_run_for_user(subagent_run.parent_agent_run_id, str(current_uid))
|
|
|
|
|
|
if not parent_run:
|
2026-01-22 00:28:43 +08:00
|
|
|
|
raise HTTPException(status_code=404, detail="对话线程不存在")
|
|
|
|
|
|
|
2026-06-02 18:47:28 +08:00
|
|
|
|
parent_conversation = await conv_repo.get_conversation_by_thread_id(parent_run.thread_id)
|
2026-06-01 22:28:45 +08:00
|
|
|
|
if (
|
|
|
|
|
|
not parent_conversation
|
2026-06-02 18:47:28 +08:00
|
|
|
|
or parent_conversation.id != parent_run.conversation_id
|
2026-06-01 22:28:45 +08:00
|
|
|
|
or parent_conversation.uid != str(current_uid)
|
|
|
|
|
|
or parent_conversation.status == "deleted"
|
|
|
|
|
|
):
|
|
|
|
|
|
raise HTTPException(status_code=404, detail="对话线程不存在")
|
|
|
|
|
|
|
2026-06-02 18:47:28 +08:00
|
|
|
|
child_agent_item = await agent_repo.get_by_slug(subagent_run.agent_id)
|
2026-06-01 22:28:45 +08:00
|
|
|
|
if not child_agent_item:
|
|
|
|
|
|
raise HTTPException(status_code=404, detail="智能体不存在")
|
|
|
|
|
|
child_agent = agent_manager.get_agent(child_agent_item.backend_id)
|
|
|
|
|
|
if not child_agent:
|
|
|
|
|
|
raise HTTPException(status_code=404, detail="智能体后端不存在")
|
|
|
|
|
|
|
2026-06-02 18:47:28 +08:00
|
|
|
|
checkpoint_thread_id = subagent_run.checkpoint_thread_id or subagent_run.thread_id
|
|
|
|
|
|
child_state = await _read_checkpoint_state(child_agent, uid=str(current_uid), thread_id=checkpoint_thread_id)
|
2026-06-01 22:28:45 +08:00
|
|
|
|
child_values = getattr(child_state, "values", {}) if child_state else {}
|
|
|
|
|
|
response = {
|
|
|
|
|
|
"agent_state": extract_agent_state(child_values),
|
2026-06-02 18:47:28 +08:00
|
|
|
|
"parent_thread_id": parent_run.thread_id,
|
|
|
|
|
|
"subagent_run": _serialize_subagent_run(subagent_run),
|
2026-06-01 22:28:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
if include_messages:
|
|
|
|
|
|
response["messages"] = _serialize_state_messages(child_values)
|
|
|
|
|
|
return response
|