feat: enhance agent orchestration, knowledge flow and UI refinements

This commit is contained in:
2026-03-29 20:31:13 +08:00
parent d85cb9cf35
commit e0fe3ca623
301 changed files with 1197804 additions and 7863 deletions

View File

@@ -5,18 +5,17 @@ Jarvis Agent 服务层
import json
import uuid
from datetime import datetime
import logging
from datetime import UTC, datetime
from typing import Any, AsyncGenerator
import asyncio
from openai import BadRequestError
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from langchain_core.messages import HumanMessage, AIMessage
from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic
from langchain_ollama import ChatOllama
import httpx
from app.database import async_session
from app.logging_utils import summarize_llm_config
from app.models.conversation import Conversation, Message
from app.models.user import User
@@ -24,43 +23,35 @@ from app.agents.graph import get_agent_graph
from app.agents.context import set_current_user, clear_current_user
from app.services import memory_service
from app.services.brain_service import BrainService
from app.services.llm_service import create_llm_from_config, resolve_provider_capabilities
from app.agents.tools.time_reasoning import extract_reference_datetime
from app.agents.state import initial_state
logger = logging.getLogger(__name__)
def _create_llm_from_config(config: dict):
"""根据用户模型配置创建 LLM 实例"""
provider = config.get("provider", "openai")
model = config.get("model", "")
api_key = config.get("api_key", "")
base_url = config.get("base_url", "")
def _is_streaming_rejection_error(error: Exception, user_llm_config: dict | None) -> bool:
capabilities = resolve_provider_capabilities(user_llm_config)
error_text = str(error).lower()
markers = [
"invalid chat setting",
"invalid params",
"stream",
"streaming",
"unsupported",
"bad_request_error",
"http 400",
"error code: 400",
]
if provider == "openai" or provider == "deepseek" or provider == "custom":
return ChatOpenAI(
api_key=api_key,
model=model,
base_url=base_url or None,
timeout=httpx.Timeout(60.0, connect=10.0),
)
elif provider == "claude":
return ChatAnthropic(
api_key=api_key,
model=model,
timeout=httpx.Timeout(60.0, connect=10.0),
)
elif provider == "ollama":
return ChatOllama(
base_url=base_url or "http://localhost:11434",
model=model,
timeout=httpx.Timeout(120.0, connect=10.0),
)
else:
# 默认使用 OpenAI
return ChatOpenAI(
api_key=api_key,
model=model,
base_url=base_url or None,
timeout=httpx.Timeout(60.0, connect=10.0),
if isinstance(error, BadRequestError):
return (
getattr(capabilities, "provider", None) not in {"openai", "claude"}
and any(marker in error_text for marker in markers)
)
return any(marker in error_text for marker in markers)
class AgentService:
"""对话 Agent 服务"""
@@ -101,27 +92,18 @@ class AgentService:
llm_config = user.llm_config
# 如果指定了模型名称,查找对应的配置
if model_name:
for model_type in ["chat", "vlm"]:
models = llm_config.get(model_type, [])
for m in models:
if m.get("name") == model_name:
return m
# 没找到,返回 None 让调用方知道配置不存在
models = llm_config.get("chat", [])
for m in models:
if m.get("name") == model_name:
return m
return None
# 如果没指定模型名,返回默认启用的 chat 模型
chat_models = llm_config.get("chat", [])
for m in chat_models:
if m.get("enabled"):
return m
vlm_models = llm_config.get("vlm", [])
for m in vlm_models:
if m.get("enabled"):
return m
return None
async def chat(
@@ -134,11 +116,26 @@ class AgentService:
) -> tuple[str, str, AsyncGenerator[dict[str, Any], None]]:
"""
处理对话请求(流式)
Returns:
(conversation_id, message_id, response_stream)
"""
# 获取或创建对话
user_llm_config = await self._get_user_llm_config(user_id, model_name)
model_name_used = model_name
if model_name and not user_llm_config:
raise ValueError("所选模型不可用于聊天,请切换到聊天模型")
if user_llm_config:
model_name_used = user_llm_config.get("name", model_name)
logger.info(
"agent_chat_started",
extra={
"details": {
"mode": "stream",
"requested_model_name": model_name,
"resolved_model_name": model_name_used,
"message_length": len(message or ""),
}
},
)
if conversation_id:
result = await self.db.execute(
select(Conversation).where(Conversation.id == conversation_id)
@@ -156,7 +153,6 @@ class AgentService:
else:
conversation_id = conv.id
# 如果有文件,读取内容作为上下文
file_context = ""
if file_ids:
from app.services.document_service import DocumentService
@@ -168,7 +164,6 @@ class AgentService:
full_message = f"{message}\n{file_context}" if file_context else message
# 存储用户消息
user_msg = Message(
conversation_id=conversation_id,
role="user",
@@ -193,156 +188,133 @@ class AgentService:
)
await self.db.commit()
# 预创建助手消息(后续更新内容)
user_llm_config = await self._get_user_llm_config(user_id, model_name)
model_name_used = model_name
if user_llm_config:
model_name_used = user_llm_config.get("name", model_name)
memory_ctx = await memory_service.build_memory_context(
self.db, user_id, conversation_id, message
)
assistant_msg = Message(
conversation_id=conversation_id,
role="assistant",
content="",
model=model_name_used or "jarvis",
attachments=None,
)
self.db.add(assistant_msg)
await self.db.commit()
await self.db.refresh(assistant_msg)
# 加载记忆上下文
memory_ctx = await memory_service.build_memory_context(
self.db, user_id, conversation_id, message
)
def _build_current_datetime_context() -> str:
now_utc = datetime.now(UTC)
return (
"【当前时间】\n"
f"- current_time_utc: {now_utc.isoformat()}\n"
f"- current_date_utc: {now_utc.date().isoformat()}\n"
"说明:解析‘今天/明天/后天/本周/下周’等相对时间时,请以 current_time_utc 为准。"
)
# 调用 LangGraph Agent
async def run_agent():
set_current_user(user_id)
try:
graph = get_agent_graph()
langgraph_state = {
"messages": [HumanMessage(content=full_message)], # type: ignore[arg-type]
"user_id": user_id,
"conversation_id": conversation_id,
"current_agent": "master",
"active_agents": ["master"],
"current_sub_commander": None,
"active_sub_commanders": [],
"sub_commander_trace": [],
"pending_tasks": [],
"completed_tasks": [],
"tool_calls": [],
"last_tool_result": None,
"knowledge_context": None,
"graph_context": None,
"plan": None,
"plan_steps": [],
"analysis_report": None,
"final_response": None,
"should_respond": True,
current_datetime_context = _build_current_datetime_context()
# 使用 initial_state 构建状态
state = initial_state(user_id, conversation_id)
state.update({
"messages": [HumanMessage(content=full_message)],
"memory_context": memory_ctx,
"current_datetime_context": current_datetime_context,
"user_llm_config": user_llm_config,
}
})
yield self._build_progress_event("thinking", "Jarvis 正在分析请求", agent="master", step="理解你的问题")
collected = ""
async for event in graph.astream_events(langgraph_state, version="v2"):
kind = event.get("event")
event_name = event.get("name", "")
metadata = event.get("metadata", {})
data = event.get("data", {})
try:
async for event in graph.astream_events(state, version="v2"):
kind = event.get("event")
event_name = event.get("name", "")
metadata = event.get("metadata", {})
data = event.get("data", {})
if kind == "on_chain_start" and event_name in {"master", "planner", "executor", "librarian", "analyst"}:
stage_map = {
"master": ("thinking", "Jarvis 正在理解请求"),
"planner": ("planning", "Jarvis 正在拆解步骤"),
"executor": ("tool", "Jarvis 正在执行操作"),
"librarian": ("tool", "Jarvis 正在检索知识"),
"analyst": ("thinking", "Jarvis 正在分析信息"),
}
stage, label = stage_map[event_name]
yield self._build_progress_event(stage, label, agent=event_name, step=label)
elif kind == "on_tool_start":
tool_input = data.get("input")
step = None
if isinstance(tool_input, dict) and tool_input:
step = f"调用工具 {event_name}"
yield self._build_progress_event("tool", f"Jarvis 正在调用工具 {event_name}", agent="executor", tool_name=event_name, step=step)
elif kind == "on_tool_end":
yield self._build_progress_event("tool", f"工具 {event_name} 已完成", agent="executor", tool_name=event_name, step=f"已获得 {event_name} 结果")
elif kind == "on_chain_end" and event_name == "planner":
output = data.get("output") or {}
plan_steps = output.get("plan_steps") or []
steps = [item.get("description", "") for item in plan_steps if item.get("description")]
yield self._build_progress_event("planning", "Jarvis 已生成处理步骤", agent="planner", step=steps[0] if steps else "正在整理计划", steps=steps[:4])
elif kind == "on_chat_model_stream":
chunk = data.get("chunk")
content = getattr(chunk, "content", "") if chunk else ""
if isinstance(content, list):
text_parts = []
for item in content:
if isinstance(item, dict):
text_parts.append(item.get("text", ""))
else:
text_parts.append(str(item))
content = "".join(text_parts)
if content:
collected += content
yield {"type": "chunk", "content": content}
elif kind == "on_chat_model_end" and not collected:
output = data.get("output")
content = getattr(output, "content", "") if output else ""
if isinstance(content, list):
text_parts = []
for item in content:
if isinstance(item, dict):
text_parts.append(item.get("text", ""))
else:
text_parts.append(str(item))
content = "".join(text_parts)
if content:
collected = content
yield {"type": "chunk", "content": content}
elif kind == "on_chain_end" and event_name in {"executor", "librarian", "analyst"}:
yield self._build_progress_event("responding", "Jarvis 正在整理最终回答", agent=event_name, step="生成回复")
except Exception as e:
fallback = f"抱歉,发生错误: {str(e)}"
collected = fallback
yield {"type": "error", "error": str(e)}
yield {"type": "chunk", "content": fallback}
if kind == "on_chain_start" and event_name in {"master", "schedule_planner", "executor", "librarian", "analyst"}:
stage_map = {
"master": ("thinking", "Jarvis 正在理解请求"),
"schedule_planner": ("planning", "Jarvis 正在编排日程"),
"executor": ("tool", "Jarvis 正在执行操作"),
"librarian": ("tool", "Jarvis 正在检索知识"),
"analyst": ("thinking", "Jarvis 正在分析信息"),
}
stage, label = stage_map.get(event_name, ("thinking", "Jarvis 正在思考"))
yield self._build_progress_event(stage, label, agent=event_name, step=label)
elif kind == "on_tool_start":
yield self._build_progress_event(
"tool",
f"Jarvis 正在调用工具 {event_name}",
agent="executor",
tool_name=event_name,
step=f"正在执行 {event_name}",
)
elif kind == "on_tool_end":
tool_result = data.get("output")
step = f"已完成 {event_name}"
if isinstance(tool_result, str) and len(tool_result) > 0:
step = tool_result[:100]
yield self._build_progress_event(
"tool",
f"工具 {event_name} 已完成",
agent="executor",
tool_name=event_name,
step=step,
)
elif kind == "on_chat_model_stream":
chunk = data.get("chunk")
content = getattr(chunk, "content", "") if chunk else ""
if content:
collected += content
yield {"type": "chunk", "content": content}
elif kind == "on_chain_end" and event_name == "create_agent_graph":
# 最终输出通常在这里
output = data.get("output")
if isinstance(output, dict) and "final_response" in output:
final_resp = output["final_response"]
# 如果还没流式输出完整,补全它
if final_resp and not collected:
collected = final_resp
yield {"type": "chunk", "content": collected}
except Exception as e:
if _is_streaming_rejection_error(e, user_llm_config) and not collected:
yield self._build_progress_event("responding", "Jarvis 正在生成回复", agent="master", step="fallback")
try:
result_state = await graph.ainvoke(state)
fallback_content = result_state.get("final_response") or str(result_state.get("messages", [AIMessage(content="")])[-1].content)
collected = str(fallback_content)
yield {"type": "chunk", "content": collected}
except Exception as fallback_error:
logger.exception("llm_sync_fallback_failed")
yield {"type": "error", "error": "模型服务暂不可用。"}
else:
logger.exception("agent_streaming_failed")
yield {"type": "error", "error": str(e)}
finally:
clear_current_user()
try:
asyncio.get_running_loop().create_task(
self._try_auto_summarize_background(user_id, conversation_id)
)
except Exception:
pass
asyncio.create_task(self._try_auto_summarize_background(user_id, conversation_id))
# 最终更新数据库中的消息内容
if collected:
try:
result2 = await self.db.execute(
select(Message).where(Message.id == assistant_msg.id)
)
msg = result2.scalar_one_or_none()
if msg:
msg.content = collected
await self.db.commit()
await brain_service.create_event(
user_id,
source_type="conversation",
source_id=conversation_id,
event_type="message_created",
title="Assistant message",
content_summary=collected[:500],
raw_excerpt=collected[:2000],
metadata_={"role": "assistant"},
importance_signal=1.0,
)
await self.db.commit()
async with async_session() as session:
result2 = await session.execute(select(Message).where(Message.id == assistant_msg.id))
msg = result2.scalar_one_or_none()
if msg:
msg.content = collected
await session.commit()
except Exception:
pass
logger.exception("save_assistant_message_failed")
return conversation_id, assistant_msg.id, run_agent()
@@ -355,117 +327,44 @@ class AgentService:
model_name: str | None = None,
) -> tuple[str, str, str, str | None]:
"""
简单同步版对话(无流式)
Returns:
(conversation_id, message_id, response_content, model_name_used)
简单同步版对话
"""
# 获取或创建对话
if conversation_id:
result = await self.db.execute(
select(Conversation).where(Conversation.id == conversation_id)
)
conv = result.scalar_one_or_none()
else:
conv = None
if not conv:
conv = Conversation(user_id=user_id, title=message[:50])
self.db.add(conv)
await self.db.commit()
await self.db.refresh(conv)
conversation_id = conv.id
else:
conversation_id = conv.id
# 如果有文件,读取内容作为上下文
file_context = ""
if file_ids:
from app.services.document_service import DocumentService
doc_svc = DocumentService(self.db)
for file_id in file_ids:
content = await doc_svc.get_document_content(user_id, file_id)
if content:
file_context += f"\n\n[用户上传文件内容]\n{content}\n[/文件内容]"
# 将文件上下文添加到消息
full_message = f"{message}\n{file_context}" if file_context else message
# 存储用户消息
user_msg = Message(
conversation_id=conversation_id,
role="user",
content=message,
attachments=[{"file_ids": file_ids}] if file_ids else None,
)
self.db.add(user_msg)
await self.db.commit()
await self.db.refresh(user_msg)
brain_service = BrainService(self.db)
await brain_service.create_event(
user_id,
source_type="conversation",
source_id=conversation_id,
event_type="message_created",
title="User message",
content_summary=message[:500],
raw_excerpt=message[:2000],
metadata_={"role": "user"},
importance_signal=1.0,
)
await self.db.commit()
# 加载记忆上下文
memory_ctx = await memory_service.build_memory_context(
self.db, user_id, conversation_id, message
)
# 获取用户配置的 LLM
user_llm_config = await self._get_user_llm_config(user_id, model_name)
model_name_used = model_name
if user_llm_config:
model_name_used = user_llm_config.get("name", model_name)
# 调用 LangGraph Agent
set_current_user(user_id)
graph = get_agent_graph()
langgraph_state = {
"messages": [HumanMessage(content=full_message)], # type: ignore[arg-type]
"user_id": user_id,
"conversation_id": conversation_id,
"current_agent": "master",
"active_agents": ["master"],
"pending_tasks": [],
"completed_tasks": [],
"tool_calls": [],
"last_tool_result": None,
"knowledge_context": None,
"graph_context": None,
"plan": None,
"plan_steps": [],
"analysis_report": None,
"final_response": None,
"should_respond": True,
"memory_context": memory_ctx,
"user_llm_config": user_llm_config, # 传递用户 LLM 配置
}
if not conversation_id:
conv = Conversation(user_id=user_id, title=message[:50])
self.db.add(conv)
await self.db.commit()
await self.db.refresh(conv)
conversation_id = conv.id
user_msg = Message(conversation_id=conversation_id, role="user", content=message)
self.db.add(user_msg)
memory_ctx = await memory_service.build_memory_context(self.db, user_id, conversation_id, message)
set_current_user(user_id)
try:
result_state = await graph.ainvoke(langgraph_state)
response_content = result_state.get("final_response", "抱歉,我无法处理这个请求。")
graph = get_agent_graph()
state = initial_state(user_id, conversation_id)
state.update({
"messages": [HumanMessage(content=message)],
"memory_context": memory_ctx,
"current_datetime_context": datetime.now(UTC).isoformat(),
"user_llm_config": user_llm_config,
})
result_state = await graph.ainvoke(state)
response_content = result_state.get("final_response") or str(result_state.get("messages", [AIMessage(content="")])[-1].content)
except Exception as e:
response_content = f"抱歉,发生错误: {str(e)}"
logger.exception("agent_chat_simple_failed")
response_content = "抱歉,发生错误。"
finally:
clear_current_user()
try:
asyncio.get_running_loop().create_task(
self._try_auto_summarize_background(user_id, conversation_id)
)
except Exception:
pass
# 保存助手消息
assistant_msg = Message(
conversation_id=conversation_id,
role="assistant",
@@ -474,19 +373,5 @@ class AgentService:
)
self.db.add(assistant_msg)
await self.db.commit()
await self.db.refresh(assistant_msg)
await brain_service.create_event(
user_id,
source_type="conversation",
source_id=conversation_id,
event_type="message_created",
title="Assistant message",
content_summary=response_content[:500],
raw_excerpt=response_content[:2000],
metadata_={"role": "assistant"},
importance_signal=1.0,
)
await self.db.commit()
return conversation_id, assistant_msg.id, response_content, model_name_used