fix: 恢复 rsync 误删的近期开发(agents/skills/mcp/knowledge/workflow/tenant/billing/storage/privacy/promotion/achievement/automation/code-exec/developer/distribution/im-bot/portal/casdoor 等后端模块 + 对应前端页面路由, 补回 main.py 挂载与 App.tsx 路由
This commit is contained in:
1 parent
d95b847fee
commit
e8e5ffa1a9
65 files changed
+35624
-339
No files matched your search
@@ -0,0 +1,491 @@
|
||||
"""
|
||||
ACP Gateway - Agent Communication Protocol 内部实现
|
||||
Phase G3: 多 Agent 协作编排层
|
||||
|
||||
将平台已有的 Agent(MarketApp type='agent')封装为可编排调用的"Agent 节点"。
|
||||
每个 Agent 节点执行时,通过本 Gateway 创建会话、发送提示、接收流式响应。
|
||||
支持通过 Redis EventBus 发布节点进度通知,供 WebSocket 实时推送到前端。
|
||||
"""
|
||||
import json
|
||||
import uuid
|
||||
import time
|
||||
import logging
|
||||
import asyncio
|
||||
import threading
|
||||
from typing import Optional, Dict, Any, Callable, AsyncIterator
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ============================================================
|
||||
# Data Models
|
||||
# ============================================================
|
||||
|
||||
@dataclass
|
||||
class AcpSessionState:
|
||||
"""ACP 会话状态"""
|
||||
session_id: str
|
||||
agent_id: str # MarketApp id
|
||||
agent_name: str = ""
|
||||
status: str = "idle" # idle / running / completed / failed / cancelled
|
||||
prompt: str = ""
|
||||
created_at: float = field(default_factory=time.time)
|
||||
completed_at: Optional[float] = None
|
||||
output: str = ""
|
||||
error: Optional[str] = None
|
||||
tool_calls: list = field(default_factory=list)
|
||||
chain_of_thought: list = field(default_factory=list) # 思考过程
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"session_id": self.session_id,
|
||||
"agent_id": self.agent_id,
|
||||
"agent_name": self.agent_name,
|
||||
"status": self.status,
|
||||
"prompt": self.prompt[:200],
|
||||
"created_at": self.created_at,
|
||||
"completed_at": self.completed_at,
|
||||
"output": self.output[:500] if self.output else "",
|
||||
"error": self.error,
|
||||
"tool_calls": self.tool_calls,
|
||||
"chain_of_thought": self.chain_of_thought,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentConfig:
|
||||
"""从 MarketApp.skill_content 解析出的 Agent 配置"""
|
||||
agent_id: str = ""
|
||||
agent_name: str = ""
|
||||
system_prompt: str = ""
|
||||
instructions: str = ""
|
||||
model_id: Optional[str] = None
|
||||
temperature: float = 0.7
|
||||
max_tokens: int = 4096
|
||||
skills: list = field(default_factory=list)
|
||||
knowledge_base_ids: list = field(default_factory=list)
|
||||
tools: list = field(default_factory=list)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Agent Resolver
|
||||
# ============================================================
|
||||
|
||||
def resolve_agent_config(db, agent_id: str) -> Optional[AgentConfig]:
|
||||
"""从 MarketApp 解析 Agent 配置"""
|
||||
from app.models import MarketApp
|
||||
|
||||
agent = db.query(MarketApp).filter(
|
||||
MarketApp.id == agent_id,
|
||||
MarketApp.type == "agent"
|
||||
).first()
|
||||
|
||||
if not agent:
|
||||
return None
|
||||
|
||||
config_json = {}
|
||||
if agent.skill_content:
|
||||
try:
|
||||
config_json = json.loads(agent.skill_content) if isinstance(agent.skill_content, str) else agent.skill_content
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return AgentConfig(
|
||||
agent_id=agent.id,
|
||||
agent_name=agent.name,
|
||||
system_prompt=config_json.get("system_prompt", config_json.get("role", agent.description or "AI助手")),
|
||||
instructions=config_json.get("instructions", ""),
|
||||
model_id=config_json.get("model_id"),
|
||||
temperature=float(config_json.get("temperature", 0.7)),
|
||||
max_tokens=int(config_json.get("max_tokens", 4096)),
|
||||
skills=config_json.get("skills", []),
|
||||
knowledge_base_ids=config_json.get("knowledge_base_ids", []),
|
||||
tools=config_json.get("tools", []),
|
||||
)
|
||||
|
||||
|
||||
def resolve_model_config(db, model_id: str) -> dict:
|
||||
"""获取模型的 API 配置"""
|
||||
from app.models import AIModel
|
||||
|
||||
model = db.query(AIModel).filter(AIModel.id == model_id).first()
|
||||
if not model:
|
||||
# Fallback to default model
|
||||
default_model = db.query(AIModel).filter(AIModel.is_enabled == True).first()
|
||||
if default_model:
|
||||
model = default_model
|
||||
else:
|
||||
return {
|
||||
"api_key": "",
|
||||
"api_base": "https://api.siliconflow.cn/v1",
|
||||
"model_name": "deepseek-ai/DeepSeek-V4-Flash",
|
||||
}
|
||||
|
||||
return {
|
||||
"api_key": model.api_key or "",
|
||||
"api_base": model.api_base or "https://api.siliconflow.cn/v1",
|
||||
"model_name": model.model_id or model.name,
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ACP Gateway
|
||||
# ============================================================
|
||||
|
||||
class AcpGateway:
|
||||
"""Agent 通信网关 — 平台内部多 Agent 编排核心"""
|
||||
|
||||
def __init__(self):
|
||||
self._sessions: Dict[str, AcpSessionState] = {}
|
||||
self._lock = threading.Lock()
|
||||
self._on_notification: Optional[Callable] = None
|
||||
|
||||
def set_notification_handler(self, handler: Callable[[dict], None]):
|
||||
"""设置通知回调 — 用于发布到 Redis EventBus"""
|
||||
self._on_notification = handler
|
||||
|
||||
def _notify(self, event_type: str, data: dict):
|
||||
"""发送通知"""
|
||||
if self._on_notification:
|
||||
try:
|
||||
self._on_notification({
|
||||
"type": event_type,
|
||||
"timestamp": time.time(),
|
||||
**data,
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"[AcpGateway] 通知回调异常: {e}")
|
||||
|
||||
# ---- Session Management ----
|
||||
|
||||
def create_session(self, agent_id: str, agent_name: str = "") -> str:
|
||||
"""创建新的 Agent 会话"""
|
||||
session_id = f"acp-{uuid.uuid4().hex[:12]}"
|
||||
with self._lock:
|
||||
self._sessions[session_id] = AcpSessionState(
|
||||
session_id=session_id,
|
||||
agent_id=agent_id,
|
||||
agent_name=agent_name or agent_id,
|
||||
status="idle",
|
||||
)
|
||||
logger.info(f"[AcpGateway] 创建会话: {session_id} (agent={agent_name or agent_id})")
|
||||
return session_id
|
||||
|
||||
def get_session(self, session_id: str) -> Optional[AcpSessionState]:
|
||||
"""获取会话"""
|
||||
with self._lock:
|
||||
return self._sessions.get(session_id)
|
||||
|
||||
def cancel_session(self, session_id: str) -> bool:
|
||||
"""取消会话"""
|
||||
with self._lock:
|
||||
sess = self._sessions.get(session_id)
|
||||
if sess and sess.status == "running":
|
||||
sess.status = "cancelled"
|
||||
sess.completed_at = time.time()
|
||||
self._notify("agent_cancelled", sess.to_dict())
|
||||
return True
|
||||
return False
|
||||
|
||||
# ---- Agent Invocation ----
|
||||
|
||||
async def invoke_agent(
|
||||
self,
|
||||
agent_id: str,
|
||||
prompt: str,
|
||||
system_prompt: str = "",
|
||||
model_id: Optional[str] = None,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 4096,
|
||||
context: Optional[dict] = None,
|
||||
db_session=None,
|
||||
) -> dict:
|
||||
"""调用 Agent — 创建会话、发送提示、等待完成
|
||||
|
||||
这是一个高级封装,内部自动创建和管理会话生命周期。
|
||||
返回完整结果字典,包含 output, tool_calls, chain_of_thought 等。
|
||||
"""
|
||||
import httpx
|
||||
|
||||
# Resolve agent config from DB
|
||||
agent_name = agent_id
|
||||
if db_session:
|
||||
config = resolve_agent_config(db_session, agent_id)
|
||||
if config:
|
||||
agent_name = config.agent_name
|
||||
system_prompt = system_prompt or config.system_prompt
|
||||
model_id = model_id or config.model_id
|
||||
temperature = config.temperature
|
||||
max_tokens = config.max_tokens
|
||||
|
||||
# Create session
|
||||
session_id = self.create_session(agent_id, agent_name)
|
||||
sess = self.get_session(session_id)
|
||||
|
||||
# Resolve model
|
||||
if db_session and model_id:
|
||||
model_cfg = resolve_model_config(db_session, model_id)
|
||||
else:
|
||||
model_cfg = {"api_key": "", "api_base": "https://api.siliconflow.cn/v1", "model_name": "deepseek-ai/DeepSeek-V4-Flash"}
|
||||
|
||||
# Update status
|
||||
with self._lock:
|
||||
sess.status = "running"
|
||||
sess.prompt = prompt
|
||||
|
||||
self._notify("agent_running", {"session_id": session_id, "agent_name": agent_name, "prompt": prompt[:200]})
|
||||
|
||||
try:
|
||||
# Build messages
|
||||
messages = []
|
||||
if system_prompt:
|
||||
messages.append({"role": "system", "content": system_prompt})
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
|
||||
# Call LLM
|
||||
headers = {
|
||||
"Authorization": f"Bearer {model_cfg['api_key']}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
payload = {
|
||||
"model": model_cfg["model_name"],
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens,
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(timeout=120.0) as client:
|
||||
resp = await client.post(
|
||||
f"{model_cfg['api_base']}/chat/completions",
|
||||
headers=headers,
|
||||
json=payload,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
output = data["choices"][0]["message"]["content"]
|
||||
usage = data.get("usage", {})
|
||||
|
||||
with self._lock:
|
||||
sess.status = "completed"
|
||||
sess.output = output
|
||||
sess.completed_at = time.time()
|
||||
|
||||
result = {
|
||||
"session_id": session_id,
|
||||
"status": "completed",
|
||||
"output": output,
|
||||
"agent_name": agent_name,
|
||||
"usage": usage,
|
||||
"tool_calls": sess.tool_calls,
|
||||
"chain_of_thought": sess.chain_of_thought,
|
||||
}
|
||||
self._notify("agent_completed", {"session_id": session_id, "output_preview": output[:200]})
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
with self._lock:
|
||||
sess.status = "failed"
|
||||
sess.error = error_msg
|
||||
sess.completed_at = time.time()
|
||||
|
||||
result = {
|
||||
"session_id": session_id,
|
||||
"status": "failed",
|
||||
"output": f"[Agent] 调用失败: {error_msg}",
|
||||
"agent_name": agent_name,
|
||||
"error": error_msg,
|
||||
}
|
||||
self._notify("agent_failed", {"session_id": session_id, "error": error_msg})
|
||||
return result
|
||||
|
||||
async def invoke_agent_streaming(
|
||||
self,
|
||||
agent_id: str,
|
||||
prompt: str,
|
||||
system_prompt: str = "",
|
||||
model_id: Optional[str] = None,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 4096,
|
||||
context: Optional[dict] = None,
|
||||
db_session=None,
|
||||
) -> AsyncIterator[dict]:
|
||||
"""流式调用 Agent — 逐步返回 LLM 生成的 token chunks
|
||||
|
||||
每个 yield 的 dict 格式:
|
||||
{"type": "token"|"done"|"error", "content": str, ...}
|
||||
"""
|
||||
import httpx
|
||||
|
||||
# Resolve agent config
|
||||
agent_name = agent_id
|
||||
if db_session:
|
||||
config = resolve_agent_config(db_session, agent_id)
|
||||
if config:
|
||||
agent_name = config.agent_name
|
||||
system_prompt = system_prompt or config.system_prompt
|
||||
model_id = model_id or config.model_id
|
||||
temperature = config.temperature
|
||||
max_tokens = config.max_tokens
|
||||
|
||||
session_id = self.create_session(agent_id, agent_name)
|
||||
sess = self.get_session(session_id)
|
||||
|
||||
if db_session and model_id:
|
||||
model_cfg = resolve_model_config(db_session, model_id)
|
||||
else:
|
||||
model_cfg = {"api_key": "", "api_base": "https://api.siliconflow.cn/v1", "model_name": "deepseek-ai/DeepSeek-V4-Flash"}
|
||||
|
||||
with self._lock:
|
||||
sess.status = "running"
|
||||
sess.prompt = prompt
|
||||
|
||||
self._notify("agent_running", {"session_id": session_id, "agent_name": agent_name})
|
||||
|
||||
try:
|
||||
messages = []
|
||||
if system_prompt:
|
||||
messages.append({"role": "system", "content": system_prompt})
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {model_cfg['api_key']}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
payload = {
|
||||
"model": model_cfg["model_name"],
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens,
|
||||
"stream": True,
|
||||
}
|
||||
|
||||
collected = []
|
||||
async with httpx.AsyncClient(timeout=120.0) as client:
|
||||
async with client.stream("POST", f"{model_cfg['api_base']}/chat/completions",
|
||||
headers=headers, json=payload) as resp:
|
||||
resp.raise_for_status()
|
||||
async for line in resp.aiter_lines():
|
||||
if line.startswith("data: "):
|
||||
data_str = line[6:]
|
||||
if data_str.strip() == "[DONE]":
|
||||
break
|
||||
try:
|
||||
chunk = json.loads(data_str)
|
||||
delta = chunk.get("choices", [{}])[0].get("delta", {})
|
||||
content = delta.get("content", "")
|
||||
if content:
|
||||
collected.append(content)
|
||||
yield {"type": "token", "content": content, "session_id": session_id}
|
||||
# Notify periodically (every ~20 tokens)
|
||||
if len(collected) % 20 == 0:
|
||||
self._notify("agent_token", {
|
||||
"session_id": session_id,
|
||||
"partial": "".join(collected[-20:]),
|
||||
})
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
full_output = "".join(collected)
|
||||
with self._lock:
|
||||
sess.status = "completed"
|
||||
sess.output = full_output
|
||||
sess.completed_at = time.time()
|
||||
|
||||
self._notify("agent_completed", {"session_id": session_id, "output_preview": full_output[:200]})
|
||||
yield {"type": "done", "session_id": session_id, "output": full_output, "status": "completed"}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
with self._lock:
|
||||
sess.status = "failed"
|
||||
sess.error = error_msg
|
||||
sess.completed_at = time.time()
|
||||
|
||||
self._notify("agent_failed", {"session_id": session_id, "error": error_msg})
|
||||
yield {"type": "error", "session_id": session_id, "error": error_msg, "status": "failed"}
|
||||
|
||||
# ---- Multi-Agent Parallel Execution ----
|
||||
|
||||
async def invoke_agents_parallel(
|
||||
self,
|
||||
tasks: list,
|
||||
db_session=None,
|
||||
) -> list:
|
||||
"""并行调用多个 Agent
|
||||
|
||||
tasks = [
|
||||
{"agent_id": "xxx", "prompt": "...", "system_prompt": "..."},
|
||||
{"agent_id": "yyy", "prompt": "...", "system_prompt": "..."},
|
||||
]
|
||||
|
||||
Returns list of results in same order.
|
||||
"""
|
||||
async def _invoke_one(task: dict) -> dict:
|
||||
try:
|
||||
return await self.invoke_agent(
|
||||
agent_id=task["agent_id"],
|
||||
prompt=task.get("prompt", ""),
|
||||
system_prompt=task.get("system_prompt", ""),
|
||||
model_id=task.get("model_id"),
|
||||
temperature=task.get("temperature", 0.7),
|
||||
max_tokens=task.get("max_tokens", 4096),
|
||||
context=task.get("context"),
|
||||
db_session=db_session,
|
||||
)
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "failed",
|
||||
"output": f"[Agent {task.get('agent_id', 'unknown')}] 执行异常: {str(e)}",
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
return await asyncio.gather(*[_invoke_one(t) for t in tasks])
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Global Singleton
|
||||
# ============================================================
|
||||
|
||||
_acp_gateway_instance: Optional[AcpGateway] = None
|
||||
_lock_gateway = threading.Lock()
|
||||
|
||||
|
||||
def get_acp_gateway() -> AcpGateway:
|
||||
"""获取全局 ACP Gateway 单例"""
|
||||
global _acp_gateway_instance
|
||||
if _acp_gateway_instance is None:
|
||||
with _lock_gateway:
|
||||
if _acp_gateway_instance is None:
|
||||
_acp_gateway_instance = AcpGateway()
|
||||
return _acp_gateway_instance
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Redis Notification Helper
|
||||
# ============================================================
|
||||
|
||||
def publish_workflow_event(workflow_id: str, node_id: str, event: dict):
|
||||
"""发布工作流执行事件到 Redis EventBus"""
|
||||
try:
|
||||
from app.event_bus import get_event_bus, MessageEvent
|
||||
|
||||
bus = get_event_bus()
|
||||
msg = MessageEvent(
|
||||
platform="workflow",
|
||||
channel_id=f"{workflow_id}:{node_id}",
|
||||
content=json.dumps(event, ensure_ascii=False),
|
||||
content_type="workflow_event",
|
||||
raw={
|
||||
"workflow_id": workflow_id,
|
||||
"node_id": node_id,
|
||||
**event,
|
||||
},
|
||||
)
|
||||
bus.publish(msg)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"[AcpGateway] Redis 发布失败: {e}")
|
||||
return False
|
||||
Reference in new issue
Block a user