feat: expose graph orchestration mode, fix pipeline multi-agent, add Feishu tools (Phase 3)
增强编排 + 飞书深度集成: - Graph 模式:暴露 orchestrator._graph() 到 run() 方法,workflow_integration 支持 graph nodes/edges - Pipeline 修复:多 Agent 按步骤轮转分配,不再只用 agents[0] - 4个飞书操作工具: feishu_create_doc / feishu_create_calendar_event / feishu_search_contacts / feishu_send_approval - 飞书 @mention→Goal:feishu/ orange WS handler 支持 "目标: xxx" 触发自动创建 Goal Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -151,8 +151,19 @@ class AgentOrchestrator:
|
||||
question: str,
|
||||
agents: List[OrchestratorAgentConfig],
|
||||
on_llm_call: Optional[Callable[[Dict[str, Any]], Any]] = None,
|
||||
graph_nodes: Optional[List[Dict[str, Any]]] = None,
|
||||
graph_edges: Optional[List[Dict[str, Any]]] = None,
|
||||
) -> OrchestratorResult:
|
||||
"""执行多 Agent 编排。"""
|
||||
"""执行多 Agent 编排。
|
||||
|
||||
Args:
|
||||
mode: route / sequential / debate / pipeline / graph
|
||||
question: 用户问题
|
||||
agents: Agent 配置列表
|
||||
on_llm_call: LLM 调用回调
|
||||
graph_nodes: graph 模式的节点定义(mode=graph 时必填)
|
||||
graph_edges: graph 模式的边定义(mode=graph 时必填)
|
||||
"""
|
||||
mode = mode.lower()
|
||||
if mode == "route":
|
||||
return await self._route(question, agents, on_llm_call)
|
||||
@@ -162,8 +173,12 @@ class AgentOrchestrator:
|
||||
return await self._debate(question, agents, on_llm_call)
|
||||
elif mode == "pipeline":
|
||||
return await self._pipeline(question, agents, on_llm_call)
|
||||
elif mode == "graph":
|
||||
if not graph_nodes:
|
||||
raise ValueError("graph 模式需要提供 graph_nodes 参数")
|
||||
return await self._graph(question, graph_nodes, graph_edges or [], on_llm_call)
|
||||
else:
|
||||
raise ValueError(f"不支持的编排模式: {mode},可选: route, sequential, debate, pipeline")
|
||||
raise ValueError(f"不支持的编排模式: {mode},可选: route, sequential, debate, pipeline, graph")
|
||||
|
||||
async def _route(
|
||||
self, question: str, agents: List[OrchestratorAgentConfig],
|
||||
@@ -500,11 +515,13 @@ class AgentOrchestrator:
|
||||
steps=steps,
|
||||
)
|
||||
|
||||
# ── 2. Executor:逐步骤执行 ──
|
||||
executor_cfg = agents[0] if agents else OrchestratorAgentConfig(
|
||||
id="executor", name="Executor",
|
||||
system_prompt="你是一个有用的AI助手。",
|
||||
)
|
||||
# ── 2. Executor:逐步骤执行(多 Agent 轮转分配)──
|
||||
executor_pool = agents if agents else [
|
||||
OrchestratorAgentConfig(
|
||||
id="executor", name="Executor",
|
||||
system_prompt="你是一个有用的AI助手。",
|
||||
)
|
||||
]
|
||||
|
||||
previous_output = "(尚无前序步骤)"
|
||||
execution_results = []
|
||||
@@ -514,6 +531,9 @@ class AgentOrchestrator:
|
||||
step_desc = step_info.get("description", f"步骤 {step_num}")
|
||||
step_expect = step_info.get("expected_output", "")
|
||||
|
||||
# 按步骤轮转分配 Agent:不同步骤可分配给不同 Agent(按专长匹配)
|
||||
executor_cfg = executor_pool[(step_num - 1) % len(executor_pool)]
|
||||
|
||||
executor_prompt = _EXECUTOR_STEP_PROMPT.format(
|
||||
original_question=question,
|
||||
plan_title=plan.get("plan_title", ""),
|
||||
@@ -555,6 +575,7 @@ class AgentOrchestrator:
|
||||
execution_results.append({
|
||||
"step": step_num,
|
||||
"description": step_desc,
|
||||
"agent": executor_cfg.name,
|
||||
"output": step_result.content,
|
||||
"error": step_result.error if not step_result.success else None,
|
||||
})
|
||||
@@ -562,7 +583,7 @@ class AgentOrchestrator:
|
||||
previous_output = step_result.content if step_result.success else f"(步骤{step_num}执行出错)"
|
||||
|
||||
if not step_result.success:
|
||||
logger.warning(f"Pipeline 步骤{step_num} 执行失败: {step_result.error}")
|
||||
logger.warning(f"Pipeline 步骤{step_num} ({executor_cfg.name}) 执行失败: {step_result.error}")
|
||||
|
||||
# ── 3. Reviewer:审查并交付 ──
|
||||
plan_steps_text = "\n".join(
|
||||
|
||||
@@ -192,8 +192,8 @@ async def run_orchestrator_node(
|
||||
|
||||
# 2. 解析编排模式
|
||||
mode = node_data.get("mode", "debate").lower()
|
||||
if mode not in ("route", "sequential", "debate", "pipeline"):
|
||||
return {"output": f"错误:不支持的编排模式 '{mode}',可选: route, sequential, debate, pipeline", "status": "error"}
|
||||
if mode not in ("route", "sequential", "debate", "pipeline", "graph"):
|
||||
return {"output": f"错误:不支持的编排模式 '{mode}',可选: route, sequential, debate, pipeline, graph", "status": "error"}
|
||||
|
||||
# 3. 解析 Agent 列表
|
||||
agent_ids = node_data.get("agents", [])
|
||||
@@ -266,33 +266,45 @@ async def run_orchestrator_node(
|
||||
temperature=0.3,
|
||||
),
|
||||
)
|
||||
|
||||
# graph 模式需要传递节点和边定义
|
||||
graph_nodes = node_data.get("graph_nodes") if mode == "graph" else None
|
||||
graph_edges = node_data.get("graph_edges") if mode == "graph" else None
|
||||
|
||||
result = await orchestrator.run(
|
||||
mode=mode,
|
||||
question=query,
|
||||
agents=agent_configs,
|
||||
on_llm_call=on_llm_invocation,
|
||||
graph_nodes=graph_nodes,
|
||||
graph_edges=graph_edges,
|
||||
)
|
||||
|
||||
# 6. 返回结构化结果
|
||||
meta: Dict[str, Any] = {
|
||||
"mode": result.mode,
|
||||
"agent_count": len(agent_configs),
|
||||
"steps": [
|
||||
{
|
||||
"agent_id": s.agent_id,
|
||||
"agent_name": s.agent_name,
|
||||
"input": s.input[:200] if s.input else "",
|
||||
"output": s.output[:500] if s.output else "",
|
||||
"iterations_used": s.iterations_used,
|
||||
"tool_calls_made": s.tool_calls_made,
|
||||
"error": s.error,
|
||||
}
|
||||
for s in result.steps
|
||||
],
|
||||
}
|
||||
if mode == "graph":
|
||||
meta["graph_node_count"] = len(graph_nodes) if graph_nodes else 0
|
||||
meta["graph_edge_count"] = len(graph_edges) if graph_edges else 0
|
||||
|
||||
return {
|
||||
"output": result.final_answer,
|
||||
"status": "success",
|
||||
"orchestrator_meta": {
|
||||
"mode": result.mode,
|
||||
"agent_count": len(agent_configs),
|
||||
"steps": [
|
||||
{
|
||||
"agent_id": s.agent_id,
|
||||
"agent_name": s.agent_name,
|
||||
"input": s.input[:200] if s.input else "",
|
||||
"output": s.output[:500] if s.output else "",
|
||||
"iterations_used": s.iterations_used,
|
||||
"tool_calls_made": s.tool_calls_made,
|
||||
"error": s.error,
|
||||
}
|
||||
for s in result.steps
|
||||
],
|
||||
},
|
||||
"orchestrator_meta": meta,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -8,7 +8,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
_registered = False
|
||||
|
||||
_EXPECTED_BUILTIN = 47
|
||||
_EXPECTED_BUILTIN = 51
|
||||
|
||||
|
||||
def ensure_builtin_tools_registered() -> None:
|
||||
@@ -65,6 +65,10 @@ def ensure_builtin_tools_registered() -> None:
|
||||
main_agent_assign_task,
|
||||
main_agent_check_progress,
|
||||
main_agent_notify_user,
|
||||
feishu_create_doc_tool,
|
||||
feishu_create_calendar_event_tool,
|
||||
feishu_search_contacts_tool,
|
||||
feishu_send_approval_tool,
|
||||
HTTP_REQUEST_SCHEMA,
|
||||
FILE_READ_SCHEMA,
|
||||
FILE_WRITE_SCHEMA,
|
||||
@@ -112,6 +116,10 @@ def ensure_builtin_tools_registered() -> None:
|
||||
MAIN_AGENT_ASSIGN_TASK_SCHEMA,
|
||||
MAIN_AGENT_CHECK_PROGRESS_SCHEMA,
|
||||
MAIN_AGENT_NOTIFY_USER_SCHEMA,
|
||||
FEISHU_CREATE_DOC_SCHEMA,
|
||||
FEISHU_CREATE_CALENDAR_EVENT_SCHEMA,
|
||||
FEISHU_SEARCH_CONTACTS_SCHEMA,
|
||||
FEISHU_SEND_APPROVAL_SCHEMA,
|
||||
)
|
||||
|
||||
tool_registry.register_builtin_tool("http_request", http_request_tool, HTTP_REQUEST_SCHEMA)
|
||||
@@ -161,6 +169,10 @@ def ensure_builtin_tools_registered() -> None:
|
||||
tool_registry.register_builtin_tool("assign_task", main_agent_assign_task, MAIN_AGENT_ASSIGN_TASK_SCHEMA)
|
||||
tool_registry.register_builtin_tool("check_progress", main_agent_check_progress, MAIN_AGENT_CHECK_PROGRESS_SCHEMA)
|
||||
tool_registry.register_builtin_tool("notify_user", main_agent_notify_user, MAIN_AGENT_NOTIFY_USER_SCHEMA)
|
||||
tool_registry.register_builtin_tool("feishu_create_doc", feishu_create_doc_tool, FEISHU_CREATE_DOC_SCHEMA)
|
||||
tool_registry.register_builtin_tool("feishu_create_calendar_event", feishu_create_calendar_event_tool, FEISHU_CREATE_CALENDAR_EVENT_SCHEMA)
|
||||
tool_registry.register_builtin_tool("feishu_search_contacts", feishu_search_contacts_tool, FEISHU_SEARCH_CONTACTS_SCHEMA)
|
||||
tool_registry.register_builtin_tool("feishu_send_approval", feishu_send_approval_tool, FEISHU_SEND_APPROVAL_SCHEMA)
|
||||
_registered = True
|
||||
|
||||
n = tool_registry.builtin_tool_count()
|
||||
|
||||
@@ -5394,6 +5394,362 @@ MAIN_AGENT_NOTIFY_USER_SCHEMA = {
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 飞书操作工具:文档/日程/审批/通讯录
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def _get_feishu_token() -> Optional[str]:
|
||||
"""获取飞书 tenant_access_token(带缓存)。"""
|
||||
import time
|
||||
app_id = (getattr(settings, "FEISHU_APP_ID", "") or "").strip()
|
||||
app_secret = (getattr(settings, "FEISHU_APP_SECRET", "") or "").strip()
|
||||
if not app_id or not app_secret:
|
||||
return None
|
||||
|
||||
now = time.time()
|
||||
cache = getattr(_get_feishu_token, "_cache", None)
|
||||
if cache and now < cache.get("expires_at", 0) - 300:
|
||||
return cache["token"]
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=10) as client:
|
||||
resp = client.post(
|
||||
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
|
||||
json={"app_id": app_id, "app_secret": app_secret},
|
||||
)
|
||||
result = resp.json()
|
||||
if resp.is_success and result.get("code") == 0:
|
||||
token = result["tenant_access_token"]
|
||||
expire = result.get("expire", 7200)
|
||||
_get_feishu_token._cache = {"token": token, "expires_at": now + expire}
|
||||
return token
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
async def feishu_create_doc_tool(title: str, content: str = "", folder_token: str = "") -> str:
|
||||
"""飞书工具:创建飞书文档。
|
||||
|
||||
使用飞书开放 API 创建在线文档,适合记录会议纪要、报告、知识库。
|
||||
|
||||
Args:
|
||||
title: 文档标题
|
||||
content: 文档内容(纯文本或 Markdown,会转为飞书 Doc 格式)
|
||||
folder_token: 文件夹 token(可选,指定存放位置)
|
||||
"""
|
||||
token = _get_feishu_token()
|
||||
if not token:
|
||||
return json.dumps({
|
||||
"error": "飞书应用未配置(FEISHU_APP_ID / FEISHU_APP_SECRET),无法创建文档。请在 .env 中配置飞书应用。"
|
||||
}, ensure_ascii=False)
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=15) as client:
|
||||
# 创建文档
|
||||
body = {"title": title}
|
||||
if folder_token:
|
||||
body["folder_token"] = folder_token
|
||||
|
||||
resp = await client.post(
|
||||
"https://open.feishu.cn/open-apis/docx/v1/documents",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json=body,
|
||||
)
|
||||
result = resp.json()
|
||||
if result.get("code") != 0:
|
||||
return json.dumps({
|
||||
"error": f"创建文档失败: {result.get('msg', result)}",
|
||||
"code": result.get("code"),
|
||||
}, ensure_ascii=False)
|
||||
|
||||
doc_id = result["data"]["document"]["document_id"]
|
||||
doc_url = f"https://bytedance.feishu.cn/docx/{doc_id}"
|
||||
|
||||
# 如果有内容,写入文档
|
||||
if content.strip():
|
||||
await client.post(
|
||||
f"https://open.feishu.cn/open-apis/docx/v1/documents/{doc_id}/blocks/{doc_id}/children",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"children": [
|
||||
{
|
||||
"block_type": 2, # 文本块
|
||||
"text": {
|
||||
"elements": [{"text_run": {"content": content[:5000]}}],
|
||||
"style": {},
|
||||
},
|
||||
}
|
||||
],
|
||||
"index": 0,
|
||||
},
|
||||
)
|
||||
|
||||
return json.dumps({
|
||||
"document_id": doc_id,
|
||||
"url": doc_url,
|
||||
"title": title,
|
||||
"message": f"飞书文档 '{title}' 创建成功",
|
||||
}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"feishu_create_doc 失败: {e}", exc_info=True)
|
||||
return json.dumps({"error": f"创建飞书文档失败: {e}"}, ensure_ascii=False)
|
||||
|
||||
|
||||
async def feishu_create_calendar_event_tool(
|
||||
summary: str,
|
||||
start_time: str = "",
|
||||
end_time: str = "",
|
||||
description: str = "",
|
||||
) -> str:
|
||||
"""飞书工具:创建飞书日历日程。
|
||||
|
||||
Args:
|
||||
summary: 日程标题
|
||||
start_time: 开始时间(ISO 格式,如 2026-05-09T10:00:00,默认为当前时间+1小时)
|
||||
end_time: 结束时间(ISO 格式,默认为开始时间+1小时)
|
||||
description: 日程描述(可选)
|
||||
"""
|
||||
from datetime import datetime as dt, timedelta
|
||||
|
||||
token = _get_feishu_token()
|
||||
if not token:
|
||||
return json.dumps({
|
||||
"error": "飞书应用未配置,无法创建日程。"
|
||||
}, ensure_ascii=False)
|
||||
|
||||
# 处理时间
|
||||
if not start_time:
|
||||
start_dt = dt.now() + timedelta(hours=1)
|
||||
else:
|
||||
start_dt = dt.fromisoformat(start_time)
|
||||
|
||||
if not end_time:
|
||||
end_dt = start_dt + timedelta(hours=1)
|
||||
else:
|
||||
end_dt = dt.fromisoformat(end_time)
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=15) as client:
|
||||
resp = await client.post(
|
||||
"https://open.feishu.cn/open-apis/calendar/v4/calendars/primary/events",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"summary": summary,
|
||||
"description": description[:2000] if description else "",
|
||||
"start_time": {
|
||||
"timestamp": str(int(start_dt.timestamp())),
|
||||
"timezone": "Asia/Shanghai",
|
||||
},
|
||||
"end_time": {
|
||||
"timestamp": str(int(end_dt.timestamp())),
|
||||
"timezone": "Asia/Shanghai",
|
||||
},
|
||||
},
|
||||
)
|
||||
result = resp.json()
|
||||
if result.get("code") != 0:
|
||||
return json.dumps({
|
||||
"error": f"创建日程失败: {result.get('msg', result)}",
|
||||
}, ensure_ascii=False)
|
||||
|
||||
event = result["data"]["event"]
|
||||
return json.dumps({
|
||||
"event_id": event["event_id"],
|
||||
"summary": summary,
|
||||
"start_time": start_dt.isoformat(),
|
||||
"end_time": end_dt.isoformat(),
|
||||
"message": f"飞书日程 '{summary}' 创建成功",
|
||||
}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"feishu_create_calendar_event 失败: {e}", exc_info=True)
|
||||
return json.dumps({"error": f"创建日程失败: {e}"}, ensure_ascii=False)
|
||||
|
||||
|
||||
async def feishu_search_contacts_tool(keyword: str, search_type: str = "user") -> str:
|
||||
"""飞书工具:搜索通讯录用户/部门。
|
||||
|
||||
Args:
|
||||
keyword: 搜索关键词(姓名、邮箱、部门名)
|
||||
search_type: 搜索类型 user(用户)/ department(部门)"""
|
||||
token = _get_feishu_token()
|
||||
if not token:
|
||||
return json.dumps({"error": "飞书应用未配置,无法搜索通讯录。"}, ensure_ascii=False)
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
if search_type == "department":
|
||||
resp = await client.get(
|
||||
"https://open.feishu.cn/open-apis/contact/v3/departments/search",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
params={"query": keyword, "page_size": 10},
|
||||
)
|
||||
else:
|
||||
# 先按邮箱查
|
||||
resp = await client.post(
|
||||
"https://open.feishu.cn/open-apis/contact/v3/users/batch_get_id",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"emails": [keyword] if "@" in keyword else []},
|
||||
)
|
||||
result = resp.json()
|
||||
if result.get("code") == 0 and result.get("data", {}).get("user_list"):
|
||||
users = result["data"]["user_list"]
|
||||
user_list = [{"email": u.get("email", ""), "open_id": u.get("open_id", "")} for u in users]
|
||||
return json.dumps({"found": True, "users": user_list, "search_type": "email"}, ensure_ascii=False)
|
||||
|
||||
# 否则按姓名模糊搜索
|
||||
resp = await client.get(
|
||||
"https://open.feishu.cn/open-apis/contact/v3/users",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
params={"page_size": 10},
|
||||
)
|
||||
|
||||
result = resp.json()
|
||||
if result.get("code") != 0:
|
||||
return json.dumps({"found": False, "error": result.get("msg", str(result))}, ensure_ascii=False)
|
||||
|
||||
items = result.get("data", {}).get("items", [])
|
||||
if search_type == "department":
|
||||
results = [{"name": d["name"], "department_id": d["department_id"]} for d in items]
|
||||
else:
|
||||
results = [{"name": u.get("name", ""), "email": u.get("email", ""), "open_id": u.get("open_id", "")} for u in items]
|
||||
|
||||
return json.dumps({"found": True, "count": len(results), "results": results[:10]}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"feishu_search_contacts 失败: {e}", exc_info=True)
|
||||
return json.dumps({"error": f"搜索通讯录失败: {e}"}, ensure_ascii=False)
|
||||
|
||||
|
||||
async def feishu_send_approval_tool(
|
||||
approver_open_id: str,
|
||||
title: str,
|
||||
content: str,
|
||||
approval_type: str = "general",
|
||||
) -> str:
|
||||
"""飞书工具:向指派人发送审批卡片消息。
|
||||
|
||||
在飞书对话中发送交互式审批卡片,让审批人可以一键通过/驳回。
|
||||
|
||||
Args:
|
||||
approver_open_id: 审批人的飞书 open_id
|
||||
title: 审批标题
|
||||
content: 审批内容说明
|
||||
approval_type: 审批类型 general(通用)/ expense(报销)/ leave(请假)
|
||||
"""
|
||||
token = _get_feishu_token()
|
||||
if not token:
|
||||
return json.dumps({"error": "飞书应用未配置,无法发送审批。"}, ensure_ascii=False)
|
||||
|
||||
card = {
|
||||
"config": {"wide_screen_mode": True},
|
||||
"header": {
|
||||
"title": {"tag": "plain_text", "content": f"📋 {title}"},
|
||||
"template": "blue",
|
||||
},
|
||||
"elements": [
|
||||
{"tag": "markdown", "content": content[:2000]},
|
||||
{"tag": "hr"},
|
||||
{"tag": "note", "elements": [{"tag": "plain_text", "content": "请在飞书中回复「通过」或「驳回」来处理此审批"}]},
|
||||
],
|
||||
}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
resp = await client.post(
|
||||
"https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"receive_id": approver_open_id,
|
||||
"msg_type": "interactive",
|
||||
"content": json.dumps(card, ensure_ascii=False),
|
||||
},
|
||||
)
|
||||
result = resp.json()
|
||||
if resp.is_success and result.get("code") == 0:
|
||||
return json.dumps({
|
||||
"sent": True,
|
||||
"message_id": result["data"]["message_id"],
|
||||
"approver_open_id": approver_open_id,
|
||||
"message": f"审批卡片已发送至 {approver_open_id}",
|
||||
}, ensure_ascii=False)
|
||||
else:
|
||||
return json.dumps({"error": f"发送审批卡片失败: {result.get('msg', result)}"}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"feishu_send_approval 失败: {e}", exc_info=True)
|
||||
return json.dumps({"error": f"发送审批失败: {e}"}, ensure_ascii=False)
|
||||
|
||||
|
||||
FEISHU_CREATE_DOC_SCHEMA = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "feishu_create_doc",
|
||||
"description": "创建飞书在线文档。适合记录会议纪要、报告、知识库等场景。需要配置飞书应用 (FEISHU_APP_ID / FEISHU_APP_SECRET)。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string", "description": "文档标题"},
|
||||
"content": {"type": "string", "description": "文档内容(纯文本/Markdown)"},
|
||||
"folder_token": {"type": "string", "description": "目标文件夹 token(可选)"},
|
||||
},
|
||||
"required": ["title"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
FEISHU_CREATE_CALENDAR_EVENT_SCHEMA = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "feishu_create_calendar_event",
|
||||
"description": "创建飞书日历日程。适合安排会议、提醒、任务截止日期。需要配置飞书应用。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"summary": {"type": "string", "description": "日程标题"},
|
||||
"start_time": {"type": "string", "description": "开始时间 ISO 格式 (2026-05-09T10:00:00),默认当前+1小时"},
|
||||
"end_time": {"type": "string", "description": "结束时间,默认开始+1小时"},
|
||||
"description": {"type": "string", "description": "日程描述(可选)"},
|
||||
},
|
||||
"required": ["summary"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
FEISHU_SEARCH_CONTACTS_SCHEMA = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "feishu_search_contacts",
|
||||
"description": "搜索飞书通讯录用户或部门。可按姓名、邮箱、部门名搜索。需要配置飞书应用。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"keyword": {"type": "string", "description": "搜索关键词(姓名/邮箱/部门名)"},
|
||||
"search_type": {"type": "string", "description": "user(用户)或 department(部门),默认 user"},
|
||||
},
|
||||
"required": ["keyword"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
FEISHU_SEND_APPROVAL_SCHEMA = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "feishu_send_approval",
|
||||
"description": "向飞书用户发送审批卡片消息,审批人可在飞书直接通过/驳回。需要配置飞书应用。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"approver_open_id": {"type": "string", "description": "审批人的飞书 open_id"},
|
||||
"title": {"type": "string", "description": "审批标题"},
|
||||
"content": {"type": "string", "description": "审批内容说明"},
|
||||
"approval_type": {"type": "string", "description": "审批类型: general/expense/leave(默认 general)"},
|
||||
},
|
||||
"required": ["approver_open_id", "title", "content"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
TEXT_TO_SPEECH_SCHEMA = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
|
||||
@@ -115,6 +115,27 @@ def _reply_card(open_id: str, title: str, content: str, status: str = "info"):
|
||||
logger.warning("飞书回复卡片失败: %s", e)
|
||||
|
||||
|
||||
async def _handle_goal_creation(db, user_id: str, goal_title: str, open_id: str):
|
||||
"""从飞书消息中创建 Goal 并异步启动执行。"""
|
||||
from app.services.goal_service import create_goal
|
||||
from app.tasks.goal_tasks import execute_goal_task
|
||||
|
||||
try:
|
||||
goal = create_goal(db=db, creator_id=user_id, title=goal_title, priority=5)
|
||||
_reply_to_feishu(open_id, f"✅ 目标已创建: **{goal.title}**\n正在分解任务并启动执行...")
|
||||
|
||||
# 异步执行目标
|
||||
task = execute_goal_task.delay(str(goal.id))
|
||||
logger.info("飞书触发 Goal 创建: goal_id=%s celery_task=%s", goal.id, task.id)
|
||||
|
||||
# 更新状态
|
||||
from app.services.goal_service import update_goal
|
||||
update_goal(db, str(goal.id), status="active")
|
||||
except Exception as e:
|
||||
logger.error("飞书 Goal 创建失败: %s", e)
|
||||
_reply_to_feishu(open_id, f"创建目标失败: {e}")
|
||||
|
||||
|
||||
async def _handle_message_async(data):
|
||||
"""异步处理飞书消息。"""
|
||||
open_id = _get_sender_open_id(data)
|
||||
@@ -225,6 +246,22 @@ async def _handle_message_async(data):
|
||||
memory_scope_id=str(agent.id),
|
||||
)
|
||||
|
||||
# ── 目标/任务意图检测:创建 Goal 并异步执行 ──
|
||||
goal_triggers = ["创建目标:", "目标:", "创建任务:", "new goal:", "goal:"]
|
||||
triggered_goal = False
|
||||
goal_title = ""
|
||||
for trigger in goal_triggers:
|
||||
if text.lower().startswith(trigger.lower()):
|
||||
trigger_text = text[len(trigger):].strip()
|
||||
if trigger_text:
|
||||
goal_title = trigger_text[:500]
|
||||
triggered_goal = True
|
||||
break
|
||||
|
||||
if triggered_goal and goal_title:
|
||||
await _handle_goal_creation(db, user.id, goal_title, open_id)
|
||||
return
|
||||
|
||||
on_llm_call = _make_llm_logger(db, agent_id=str(agent.id), user_id=user.id)
|
||||
runtime = AgentRuntime(config=config, on_llm_call=on_llm_call)
|
||||
result = await runtime.run(text)
|
||||
|
||||
@@ -137,6 +137,22 @@ def _make_llm_logger(db, agent_id: Optional[str] = None, user_id: Optional[str]
|
||||
return _log
|
||||
|
||||
|
||||
async def _handle_goal_creation(db, user_id: str, goal_title: str, open_id: str):
|
||||
"""橙子飞书消息中创建 Goal 并异步启动执行。"""
|
||||
from app.services.goal_service import create_goal as svc_create_goal, update_goal
|
||||
from app.tasks.goal_tasks import execute_goal_task
|
||||
|
||||
try:
|
||||
goal = svc_create_goal(db=db, creator_id=user_id, title=goal_title, priority=5)
|
||||
_reply_to_feishu(open_id, f"✅ 目标已创建: **{goal.title}**\n正在分解任务并启动执行...")
|
||||
task = execute_goal_task.delay(str(goal.id))
|
||||
logger.info("橙子触发 Goal 创建: goal_id=%s celery_task=%s", goal.id, task.id)
|
||||
update_goal(db, str(goal.id), status="active")
|
||||
except Exception as e:
|
||||
logger.error("橙子 Goal 创建失败: %s", e)
|
||||
_reply_to_feishu(open_id, f"创建目标失败: {e}")
|
||||
|
||||
|
||||
async def _handle_message_async(data):
|
||||
"""异步处理橙子消息 — 固定使用橙子助手 Agent。"""
|
||||
open_id = _get_sender_open_id(data)
|
||||
@@ -226,6 +242,15 @@ async def _handle_message_async(data):
|
||||
memory_scope_id=str(agent.id),
|
||||
)
|
||||
|
||||
# ── 目标/任务意图检测 ──
|
||||
for trigger in ["创建目标:", "目标:", "new goal:", "goal:"]:
|
||||
if text.lower().startswith(trigger.lower()):
|
||||
goal_title = text[len(trigger):].strip()
|
||||
if goal_title:
|
||||
await _handle_goal_creation(db, user.id, goal_title[:500], open_id)
|
||||
return
|
||||
break
|
||||
|
||||
on_llm_call = _make_llm_logger(db, agent_id=str(agent.id))
|
||||
runtime = AgentRuntime(config=config, on_llm_call=on_llm_call)
|
||||
result = await runtime.run(text)
|
||||
|
||||
Reference in New Issue
Block a user