feat: 向量记忆 RAG、工具市场、SSE 流式响应、前端集成与测试覆盖
- 新增 embedding_service(语义检索)、knowledge_service(RAG)、text_chunker、document_parser - 新增 tool_registry(自定义工具注册表)并完善工具市场 API(CRUD + code/http 执行) - 新增 agent_vector_memory / knowledge_base 模型及对应数据库表 - 实现 SSE 流式响应与 Agent 预算控制 - AgentChat.vue 集成 MainLayout 导航布局 - 完善测试体系:7 个新测试文件共 110 个测试覆盖 - 修复 conftest.py SQLite 内存数据库连接隔离问题 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,8 +8,10 @@ POST /api/v1/agent-chat/bare
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
import json
|
||||
from typing import Any, AsyncGenerator, Dict, List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.core.database import get_db
|
||||
@@ -23,6 +25,7 @@ from app.agent_runtime import (
|
||||
AgentConfig,
|
||||
AgentLLMConfig,
|
||||
AgentToolConfig,
|
||||
AgentBudgetConfig,
|
||||
AgentStep,
|
||||
AgentOrchestrator,
|
||||
OrchestratorAgentConfig,
|
||||
@@ -64,6 +67,14 @@ def _make_llm_logger(
|
||||
return _log
|
||||
|
||||
|
||||
async def _sse_stream(gen: AsyncGenerator[dict, None]) -> AsyncGenerator[str, None]:
|
||||
"""将 run_stream 生成的 dict 事件格式化为 SSE 文本流。"""
|
||||
async for event in gen:
|
||||
event_type = event.get("type", "message")
|
||||
data = {k: v for k, v in event.items() if k != "type"}
|
||||
yield f"event: {event_type}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n"
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
message: str
|
||||
session_id: Optional[str] = None
|
||||
@@ -205,6 +216,39 @@ async def chat_bare(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/bare/stream")
|
||||
async def chat_bare_stream(
|
||||
req: ChatRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""无需 Agent 配置,使用默认设置直接对话(流式 SSE)。"""
|
||||
config = AgentConfig(
|
||||
name="bare_agent",
|
||||
system_prompt="你是一个有用的AI助手。请使用可用工具来帮助用户完成任务。",
|
||||
llm=AgentLLMConfig(
|
||||
model=req.model or (
|
||||
"gpt-4o-mini" if settings.OPENAI_API_KEY and settings.OPENAI_API_KEY != "your-openai-api-key"
|
||||
else "deepseek-v4-flash"
|
||||
),
|
||||
temperature=req.temperature or 0.7,
|
||||
max_iterations=req.max_iterations or 10,
|
||||
),
|
||||
user_id=current_user.id,
|
||||
)
|
||||
on_llm_call = _make_llm_logger(db, agent_id=None, user_id=current_user.id)
|
||||
runtime = AgentRuntime(config=config, on_llm_call=on_llm_call)
|
||||
return StreamingResponse(
|
||||
_sse_stream(runtime.run_stream(req.message)),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{agent_id}", response_model=ChatResponse)
|
||||
async def chat_with_agent(
|
||||
agent_id: str,
|
||||
@@ -225,9 +269,25 @@ async def chat_with_agent(
|
||||
# 查找 agent 节点的配置(或第一个 llm 节点的配置)
|
||||
agent_node_cfg = _find_agent_node_config(nodes)
|
||||
|
||||
# 构建 system prompt,并自动注入智能体名称
|
||||
system_prompt = agent_node_cfg.get("system_prompt") or agent.description or "你是一个有用的AI助手。"
|
||||
if agent.name:
|
||||
name_prefix = f"你的名字是{agent.name}"
|
||||
if name_prefix not in system_prompt:
|
||||
system_prompt = f"{name_prefix}。\n\n{system_prompt}"
|
||||
|
||||
# 合并执行预算:Agent.budget_config 覆盖默认值
|
||||
budget = AgentBudgetConfig()
|
||||
if agent.budget_config and isinstance(agent.budget_config, dict):
|
||||
bc = agent.budget_config
|
||||
if "max_llm_invocations" in bc and bc["max_llm_invocations"] is not None:
|
||||
budget.max_llm_invocations = max(1, int(bc["max_llm_invocations"]))
|
||||
if "max_tool_calls" in bc and bc["max_tool_calls"] is not None:
|
||||
budget.max_tool_calls = max(1, int(bc["max_tool_calls"]))
|
||||
|
||||
config = AgentConfig(
|
||||
name=agent.name,
|
||||
system_prompt=agent_node_cfg.get("system_prompt") or agent.description or "你是一个有用的AI助手。",
|
||||
system_prompt=system_prompt,
|
||||
llm=AgentLLMConfig(
|
||||
provider=agent_node_cfg.get("provider", "openai"),
|
||||
model=req.model or agent_node_cfg.get("model", "gpt-4o-mini"),
|
||||
@@ -238,6 +298,7 @@ async def chat_with_agent(
|
||||
include_tools=agent_node_cfg.get("tools", []),
|
||||
exclude_tools=agent_node_cfg.get("exclude_tools", []),
|
||||
),
|
||||
budget=budget,
|
||||
user_id=current_user.id,
|
||||
)
|
||||
|
||||
@@ -256,6 +317,68 @@ async def chat_with_agent(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{agent_id}/stream")
|
||||
async def chat_with_agent_stream(
|
||||
agent_id: str,
|
||||
req: ChatRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""与指定的 Agent 对话(流式 SSE)。"""
|
||||
agent = db.query(Agent).filter(Agent.id == agent_id).first()
|
||||
if not agent:
|
||||
raise HTTPException(status_code=404, detail="Agent 不存在")
|
||||
if agent.user_id and agent.user_id != current_user.id and current_user.role != "admin":
|
||||
raise HTTPException(status_code=403, detail="无权访问该 Agent")
|
||||
|
||||
wc = agent.workflow_config or {}
|
||||
nodes = wc.get("nodes", [])
|
||||
agent_node_cfg = _find_agent_node_config(nodes)
|
||||
|
||||
system_prompt = agent_node_cfg.get("system_prompt") or agent.description or "你是一个有用的AI助手。"
|
||||
if agent.name:
|
||||
name_prefix = f"你的名字是{agent.name}"
|
||||
if name_prefix not in system_prompt:
|
||||
system_prompt = f"{name_prefix}。\n\n{system_prompt}"
|
||||
|
||||
budget = AgentBudgetConfig()
|
||||
if agent.budget_config and isinstance(agent.budget_config, dict):
|
||||
bc = agent.budget_config
|
||||
if "max_llm_invocations" in bc and bc["max_llm_invocations"] is not None:
|
||||
budget.max_llm_invocations = max(1, int(bc["max_llm_invocations"]))
|
||||
if "max_tool_calls" in bc and bc["max_tool_calls"] is not None:
|
||||
budget.max_tool_calls = max(1, int(bc["max_tool_calls"]))
|
||||
|
||||
config = AgentConfig(
|
||||
name=agent.name,
|
||||
system_prompt=system_prompt,
|
||||
llm=AgentLLMConfig(
|
||||
provider=agent_node_cfg.get("provider", "openai"),
|
||||
model=req.model or agent_node_cfg.get("model", "gpt-4o-mini"),
|
||||
temperature=req.temperature or float(agent_node_cfg.get("temperature", 0.7)),
|
||||
max_iterations=req.max_iterations or int(agent_node_cfg.get("max_iterations", 10)),
|
||||
),
|
||||
tools=AgentToolConfig(
|
||||
include_tools=agent_node_cfg.get("tools", []),
|
||||
exclude_tools=agent_node_cfg.get("exclude_tools", []),
|
||||
),
|
||||
budget=budget,
|
||||
user_id=current_user.id,
|
||||
)
|
||||
|
||||
on_llm_call = _make_llm_logger(db, agent_id=agent_id, user_id=current_user.id)
|
||||
runtime = AgentRuntime(config=config, on_llm_call=on_llm_call)
|
||||
return StreamingResponse(
|
||||
_sse_stream(runtime.run_stream(req.message)),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _find_agent_node_config(nodes: list) -> Dict[str, Any]:
|
||||
"""从工作流节点列表中查找第一个 agent 类型或 llm 类型的节点配置。"""
|
||||
if not nodes:
|
||||
|
||||
Reference in New Issue
Block a user