agentchat-task-agent 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentchat_task_agent-0.1.0.dist-info/METADATA +251 -0
- agentchat_task_agent-0.1.0.dist-info/RECORD +21 -0
- agentchat_task_agent-0.1.0.dist-info/WHEEL +5 -0
- agentchat_task_agent-0.1.0.dist-info/entry_points.txt +3 -0
- agentchat_task_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
- agentchat_task_agent-0.1.0.dist-info/top_level.txt +1 -0
- task_agent/__init__.py +44 -0
- task_agent/cli.py +98 -0
- task_agent/config.py +32 -0
- task_agent/demo.py +132 -0
- task_agent/executor.py +42 -0
- task_agent/graph.py +314 -0
- task_agent/judge.py +48 -0
- task_agent/llm.py +19 -0
- task_agent/memory.py +45 -0
- task_agent/nodes.py +394 -0
- task_agent/prompts.py +120 -0
- task_agent/py.typed +1 -0
- task_agent/state.py +36 -0
- task_agent/telemetry.py +45 -0
- task_agent/tools.py +155 -0
task_agent/nodes.py
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"""自主任务 Agent 的图节点(通过 Runtime 闭包注入配置/LLM/执行器)。
|
|
2
|
+
|
|
3
|
+
- plan_node :LLM 把目标拆成子任务列表(fixed 一期);
|
|
4
|
+
- execute_node :对当前子任务调用一次 executor(fixed 一期;顺序、含失败标记);
|
|
5
|
+
- final_node :整合所有子任务结果,输出最终交付;
|
|
6
|
+
- replan_node :每步动态决定下一步动作 + 标注信息来源(replan 二期);
|
|
7
|
+
- execute_action_node :按来源路由执行当前动作(调用注入的 Executor);
|
|
8
|
+
- check_node :判断是否充分达成目标;
|
|
9
|
+
- verify_node :子任务失败后的自检(是否值得重试)——节点容错;
|
|
10
|
+
- human_confirm_node :节点级 HITL——让用户确认/编辑/跳过下一步动作(可关)。
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Any, Awaitable, Callable
|
|
17
|
+
|
|
18
|
+
from langgraph.types import interrupt
|
|
19
|
+
|
|
20
|
+
from task_agent.config import TaskAgentConfig
|
|
21
|
+
from task_agent.executor import SOURCE_KEYS, ExecuteRequest, Executor
|
|
22
|
+
from task_agent.llm import LLMFactory, llm_text
|
|
23
|
+
from task_agent.memory import TaskMemory
|
|
24
|
+
from task_agent.prompts import (
|
|
25
|
+
CHECK_PROMPT,
|
|
26
|
+
COMPRESS_PROMPT,
|
|
27
|
+
FINAL_PROMPT,
|
|
28
|
+
PLAN_PROMPT,
|
|
29
|
+
REPLAN_PROMPT,
|
|
30
|
+
VERIFY_PROMPT,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class Runtime:
|
|
36
|
+
"""构建图时注入的运行上下文(配置 + LLM 工厂 + 执行器 + checkpointer 提供者 + 事件回调)。"""
|
|
37
|
+
|
|
38
|
+
config: TaskAgentConfig
|
|
39
|
+
llm_factory: LLMFactory
|
|
40
|
+
executor: Executor
|
|
41
|
+
checkpointer_provider: Callable[[], Any | None] = lambda: None
|
|
42
|
+
on_event: Callable[[str, dict], None] | None = None
|
|
43
|
+
memory: TaskMemory | None = None
|
|
44
|
+
|
|
45
|
+
def emit(self, kind: str, data: dict | None = None) -> None:
|
|
46
|
+
"""发事件(如 plan/replan/execute/check/verify/final/hitl),供宿主/日志观测。"""
|
|
47
|
+
if self.on_event is not None:
|
|
48
|
+
self.on_event(kind, data or {})
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
Node = Callable[[dict], Awaitable[dict]]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def make_nodes(runtime: Runtime) -> dict[str, Node]:
|
|
55
|
+
"""按 Runtime 生成全部节点(闭包注入,避免全局可变状态)。"""
|
|
56
|
+
config = runtime.config
|
|
57
|
+
|
|
58
|
+
async def plan_node(state: dict) -> dict:
|
|
59
|
+
mem = await _memory_ctx(runtime, state["goal"])
|
|
60
|
+
plan = _parse_plan(
|
|
61
|
+
await llm_text(
|
|
62
|
+
runtime.llm_factory(),
|
|
63
|
+
PLAN_PROMPT.format(goal=state["goal"], memory=mem),
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
fallback = False
|
|
67
|
+
if not plan:
|
|
68
|
+
fallback = True
|
|
69
|
+
plan = [
|
|
70
|
+
{
|
|
71
|
+
"id": "1",
|
|
72
|
+
"desc": "请直接回答:" + state["goal"],
|
|
73
|
+
"status": "pending",
|
|
74
|
+
"result": "",
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
runtime.emit("plan", {"subtasks": len(plan), "fallback": fallback})
|
|
78
|
+
return {"plan": plan, "current_idx": 0, "findings": []}
|
|
79
|
+
|
|
80
|
+
async def execute_node(state: dict) -> dict:
|
|
81
|
+
idx = state["current_idx"]
|
|
82
|
+
plan = [dict(p) for p in state["plan"]]
|
|
83
|
+
if idx >= len(plan):
|
|
84
|
+
return {"current_idx": idx}
|
|
85
|
+
task = plan[idx]
|
|
86
|
+
try:
|
|
87
|
+
result = await runtime.executor(
|
|
88
|
+
ExecuteRequest(action=task["desc"], source="default")
|
|
89
|
+
)
|
|
90
|
+
finding = (result.answer or "(子任务无输出)")[:800]
|
|
91
|
+
ok = bool(finding and finding.strip() and finding != "(子任务无输出)")
|
|
92
|
+
plan[idx]["status"] = "done" if ok else "failed"
|
|
93
|
+
except Exception as exc: # noqa: BLE001 - 单子任务失败不中断整个任务
|
|
94
|
+
finding = f"子任务失败:{exc}"
|
|
95
|
+
plan[idx]["status"] = "failed"
|
|
96
|
+
ok = False
|
|
97
|
+
plan[idx]["result"] = finding
|
|
98
|
+
runtime.emit("execute", {"subtask": task["desc"], "ok": ok})
|
|
99
|
+
out: dict = {"plan": plan, "current_idx": idx + 1}
|
|
100
|
+
out.update(await _append_finding(runtime, state, finding))
|
|
101
|
+
return out
|
|
102
|
+
|
|
103
|
+
async def final_node(state: dict) -> dict:
|
|
104
|
+
goal = state["goal"]
|
|
105
|
+
findings = "\n\n".join(
|
|
106
|
+
f"[{i + 1}] {f[:400]}"
|
|
107
|
+
for i, f in enumerate(state.get("findings", []))
|
|
108
|
+
)
|
|
109
|
+
summary = state.get("findings_summary") or ""
|
|
110
|
+
if summary:
|
|
111
|
+
findings = f"历史摘要:{summary}\n\n{findings}"
|
|
112
|
+
final = (
|
|
113
|
+
await llm_text(
|
|
114
|
+
runtime.llm_factory(),
|
|
115
|
+
FINAL_PROMPT.format(goal=goal, findings=findings or "(无)"),
|
|
116
|
+
)
|
|
117
|
+
).strip()
|
|
118
|
+
if runtime.memory is not None:
|
|
119
|
+
try:
|
|
120
|
+
await runtime.memory.remember(goal, final)
|
|
121
|
+
except Exception: # noqa: BLE001 - 记忆失败不影响交付
|
|
122
|
+
pass
|
|
123
|
+
runtime.emit("final", {"answer": final})
|
|
124
|
+
return {"final_answer": final}
|
|
125
|
+
|
|
126
|
+
async def replan_node(state: dict) -> dict:
|
|
127
|
+
goal = state["goal"]
|
|
128
|
+
findings = state.get("findings") or []
|
|
129
|
+
mem = await _memory_ctx(runtime, goal)
|
|
130
|
+
action, source = _parse_next_action(
|
|
131
|
+
await llm_text(
|
|
132
|
+
runtime.llm_factory(),
|
|
133
|
+
REPLAN_PROMPT.format(
|
|
134
|
+
goal=goal,
|
|
135
|
+
findings=_fmt_findings(findings) or "(尚无)",
|
|
136
|
+
memory=mem,
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
if not action:
|
|
141
|
+
# 无下一步 → 视为可完成
|
|
142
|
+
runtime.emit("replan", {"action": "", "source": source})
|
|
143
|
+
return {"current_action": "", "done": True, "retries": 0}
|
|
144
|
+
runtime.emit("replan", {"action": action, "source": source})
|
|
145
|
+
return {"current_action": action, "expected_source": source, "retries": 0}
|
|
146
|
+
|
|
147
|
+
async def execute_action_node(state: dict) -> dict:
|
|
148
|
+
action = state.get("current_action") or ""
|
|
149
|
+
retries = int(state.get("retries") or 0)
|
|
150
|
+
is_retry = retries > 0
|
|
151
|
+
if not action:
|
|
152
|
+
# 空动作:跳过执行只推进步数
|
|
153
|
+
return {"step": int(state.get("step") or 0) + 1}
|
|
154
|
+
try:
|
|
155
|
+
result = await runtime.executor(
|
|
156
|
+
ExecuteRequest(
|
|
157
|
+
action=action,
|
|
158
|
+
source=state.get("expected_source") or "default",
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
finding = (result.answer or "(子任务无输出)")[:800]
|
|
162
|
+
# 空答案视为失败:否则 verify 判重试后 retries 又被归零,形成无限循环
|
|
163
|
+
ok = bool(finding and finding.strip() and finding != "(子任务无输出)")
|
|
164
|
+
new_retries = 0 if ok else retries
|
|
165
|
+
except Exception as exc: # noqa: BLE001 - 单步失败不中断
|
|
166
|
+
finding = f"子任务失败:{exc}"
|
|
167
|
+
new_retries = retries # 失败 → 保留计数(由 verify 决定是否 +1 / 放弃)
|
|
168
|
+
ok = False
|
|
169
|
+
step = int(state.get("step") or 0) + (0 if is_retry else 1)
|
|
170
|
+
runtime.emit("execute", {"action": action, "source": state.get("expected_source") or "default", "ok": ok})
|
|
171
|
+
out: dict = {"step": step, "retries": new_retries}
|
|
172
|
+
out.update(await _append_finding(runtime, state, finding))
|
|
173
|
+
return out
|
|
174
|
+
|
|
175
|
+
async def check_node(state: dict) -> dict:
|
|
176
|
+
step = int(state.get("step") or 0)
|
|
177
|
+
if step >= config.max_steps:
|
|
178
|
+
return {"done": True}
|
|
179
|
+
goal = state["goal"]
|
|
180
|
+
findings = state.get("findings") or []
|
|
181
|
+
done = _parse_done(
|
|
182
|
+
await llm_text(
|
|
183
|
+
runtime.llm_factory(),
|
|
184
|
+
CHECK_PROMPT.format(
|
|
185
|
+
goal=goal, findings=_fmt_findings(findings) or "(尚无)"
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
runtime.emit("check", {"done": done, "step": step})
|
|
190
|
+
return {"done": done}
|
|
191
|
+
|
|
192
|
+
async def verify_node(state: dict) -> dict:
|
|
193
|
+
goal = state["goal"]
|
|
194
|
+
action = state.get("current_action") or ""
|
|
195
|
+
findings = state.get("findings") or []
|
|
196
|
+
finding = findings[-1] if findings else ""
|
|
197
|
+
retries = int(state.get("retries") or 0)
|
|
198
|
+
if retries >= config.max_retries:
|
|
199
|
+
return {"should_retry": False, "retries": retries}
|
|
200
|
+
data = _jump_json(
|
|
201
|
+
await llm_text(
|
|
202
|
+
runtime.llm_factory(),
|
|
203
|
+
VERIFY_PROMPT.format(
|
|
204
|
+
goal=goal,
|
|
205
|
+
action=action,
|
|
206
|
+
finding=finding,
|
|
207
|
+
findings=_fmt_findings(findings) or "(无)",
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
retry = bool(data.get("retry", False))
|
|
212
|
+
runtime.emit("verify", {"retry": retry, "retries": retries + 1 if retry else retries})
|
|
213
|
+
if retry:
|
|
214
|
+
return {"should_retry": True, "retries": retries + 1}
|
|
215
|
+
return {"should_retry": False, "retries": 0}
|
|
216
|
+
|
|
217
|
+
async def human_confirm_node(state: dict) -> dict:
|
|
218
|
+
"""节点级 HITL:replan 产出下一步后让用户确认/编辑/跳过。
|
|
219
|
+
|
|
220
|
+
- 关闭(config.hitl=False) → 透传 proceed,不 interrupt;
|
|
221
|
+
- 开启 → interrupt 暂停,宿主经 resume(decision) 恢复;
|
|
222
|
+
- 依赖 checkpointer 持久化;无 checkpointer 时禁用,避免中断后无法恢复
|
|
223
|
+
→ 降级为全自主(与关闭 HITL 一致)。
|
|
224
|
+
"""
|
|
225
|
+
if not config.hitl:
|
|
226
|
+
return {"_confirm_verb": "proceed"}
|
|
227
|
+
if runtime.checkpointer_provider() is None:
|
|
228
|
+
return {"_confirm_verb": "proceed"}
|
|
229
|
+
runtime.emit(
|
|
230
|
+
"hitl",
|
|
231
|
+
{
|
|
232
|
+
"next_action": state.get("current_action") or "",
|
|
233
|
+
"expected_source": state.get("expected_source") or "default",
|
|
234
|
+
"step": state.get("step") or 0,
|
|
235
|
+
},
|
|
236
|
+
)
|
|
237
|
+
decision = interrupt(
|
|
238
|
+
{
|
|
239
|
+
"type": "task_confirm",
|
|
240
|
+
"goal": state.get("goal", ""),
|
|
241
|
+
"next_action": state.get("current_action") or "",
|
|
242
|
+
"expected_source": state.get("expected_source") or "default",
|
|
243
|
+
"step": state.get("step") or 0,
|
|
244
|
+
"findings": state.get("findings") or [],
|
|
245
|
+
}
|
|
246
|
+
)
|
|
247
|
+
return _apply_confirm(
|
|
248
|
+
decision,
|
|
249
|
+
state.get("current_action") or "",
|
|
250
|
+
state.get("expected_source") or "default",
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
"plan_node": plan_node,
|
|
255
|
+
"execute_node": execute_node,
|
|
256
|
+
"final_node": final_node,
|
|
257
|
+
"replan_node": replan_node,
|
|
258
|
+
"execute_action_node": execute_action_node,
|
|
259
|
+
"check_node": check_node,
|
|
260
|
+
"verify_node": verify_node,
|
|
261
|
+
"human_confirm_node": human_confirm_node,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ---------------- 纯函数辅助(便于单测) ----------------
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _parse_plan(text: str) -> list[dict]:
|
|
269
|
+
"""从 LLM 输出解析子任务数组;失败返回空(由下游标记为空计划)。"""
|
|
270
|
+
s = text.strip()
|
|
271
|
+
if s.startswith("```"):
|
|
272
|
+
s = "\n".join(l for l in s.splitlines() if not l.strip().startswith("```"))
|
|
273
|
+
try:
|
|
274
|
+
begin, end = s.index("["), s.rindex("]")
|
|
275
|
+
data = json.loads(s[begin : end + 1])
|
|
276
|
+
except (ValueError, json.JSONDecodeError):
|
|
277
|
+
return []
|
|
278
|
+
out = []
|
|
279
|
+
for i, item in enumerate(data or []):
|
|
280
|
+
if isinstance(item, dict) and item.get("desc"):
|
|
281
|
+
out.append(
|
|
282
|
+
{
|
|
283
|
+
"id": str(item.get("id") or i + 1),
|
|
284
|
+
"desc": item["desc"],
|
|
285
|
+
"status": "pending",
|
|
286
|
+
"result": "",
|
|
287
|
+
}
|
|
288
|
+
)
|
|
289
|
+
return out
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _fmt_findings(findings: list[str]) -> str:
|
|
293
|
+
"""把结果列表格式化为 replan/check 的上下文文本(截断)。"""
|
|
294
|
+
return "\n\n".join(
|
|
295
|
+
f"[{i + 1}] {f[:400]}" for i, f in enumerate(findings or [])
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
async def _compress_findings(
|
|
300
|
+
runtime: Runtime, findings: list[str], summary: str
|
|
301
|
+
) -> tuple[list[str], str]:
|
|
302
|
+
"""超预算压缩:保留最新一条,历史交给 LLM 压缩进 findings_summary。
|
|
303
|
+
|
|
304
|
+
LLM 失败时退化为截断拼接(不中断执行)。返回 (保留的 findings, 新摘要)。
|
|
305
|
+
"""
|
|
306
|
+
keep = findings[-1:]
|
|
307
|
+
older = findings[:-1]
|
|
308
|
+
try:
|
|
309
|
+
text = (
|
|
310
|
+
await llm_text(
|
|
311
|
+
runtime.llm_factory(),
|
|
312
|
+
COMPRESS_PROMPT.format(
|
|
313
|
+
summary=summary or "(无)",
|
|
314
|
+
findings=_fmt_findings(older),
|
|
315
|
+
),
|
|
316
|
+
)
|
|
317
|
+
).strip()
|
|
318
|
+
except Exception: # noqa: BLE001 - 压缩失败降级为截断
|
|
319
|
+
text = ""
|
|
320
|
+
if not text:
|
|
321
|
+
text = ";".join(older)[:500]
|
|
322
|
+
new_summary = f"{summary};{text}" if summary else text
|
|
323
|
+
return keep, new_summary[:1500]
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
async def _append_finding(runtime: Runtime, state: dict, finding: str) -> dict:
|
|
327
|
+
"""构造 findings 增量:未超预算 → 普通追加;超预算 → 整体替换 + 压缩摘要。"""
|
|
328
|
+
budget = runtime.config.findings_budget
|
|
329
|
+
existing = list(state.get("findings") or [])
|
|
330
|
+
if budget is None or len(existing) + 1 <= budget:
|
|
331
|
+
return {"findings": [finding]}
|
|
332
|
+
keep, summary = await _compress_findings(
|
|
333
|
+
runtime, existing + [finding], str(state.get("findings_summary") or "")
|
|
334
|
+
)
|
|
335
|
+
return {"findings": {"_replace": keep}, "findings_summary": summary}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
async def _memory_ctx(runtime: Runtime, goal: str) -> str:
|
|
339
|
+
"""把跨任务记忆召回结果格式化为提示词上下文(无记忆/失败 → (无))。"""
|
|
340
|
+
if runtime.memory is None:
|
|
341
|
+
return "(无)"
|
|
342
|
+
try:
|
|
343
|
+
items = await runtime.memory.recall(goal)
|
|
344
|
+
except Exception: # noqa: BLE001 - 记忆不可用不影响执行
|
|
345
|
+
return "(无)"
|
|
346
|
+
return "\n".join(f"- {s}" for s in items) or "(无)"
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _jump_json(text: str) -> dict:
|
|
350
|
+
"""剥离代码围栏后提取第一个 JSON 对象。"""
|
|
351
|
+
s = (text or "").strip()
|
|
352
|
+
if s.startswith("```"):
|
|
353
|
+
s = "\n".join(l for l in s.splitlines() if not l.strip().startswith("```"))
|
|
354
|
+
try:
|
|
355
|
+
begin, end = s.index("{"), s.rindex("}")
|
|
356
|
+
data = json.loads(s[begin : end + 1])
|
|
357
|
+
return data if isinstance(data, dict) else {}
|
|
358
|
+
except (ValueError, json.JSONDecodeError):
|
|
359
|
+
return {}
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _parse_next_action(text: str) -> tuple[str, str]:
|
|
363
|
+
data = _jump_json(text)
|
|
364
|
+
action = str(data.get("next_action", "") or "").strip()
|
|
365
|
+
source = str(data.get("expected_source", "") or "").strip().lower()
|
|
366
|
+
return action, source if source in SOURCE_KEYS else "default"
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _parse_done(text: str) -> bool:
|
|
370
|
+
return bool(_jump_json(text).get("done", False))
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _is_failed_finding(finding: str) -> bool:
|
|
374
|
+
"""规则预筛:判断一条子任务结果是否属失败/无输出(触发 verify 自检)。"""
|
|
375
|
+
f = (finding or "").strip()
|
|
376
|
+
return not f or f.startswith("子任务失败") or f == "(子任务无输出)"
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _apply_confirm(decision: dict, current_action: str, expected_source: str) -> dict:
|
|
380
|
+
"""把 HITL 决策落到 state 增量(纯函数,便于单测)。"""
|
|
381
|
+
verb = str((decision or {}).get("verb") or "proceed").lower()
|
|
382
|
+
if verb == "edit":
|
|
383
|
+
return {
|
|
384
|
+
"_confirm_verb": "edit",
|
|
385
|
+
"current_action": str(
|
|
386
|
+
(decision or {}).get("action") or current_action or ""
|
|
387
|
+
),
|
|
388
|
+
"expected_source": str(
|
|
389
|
+
(decision or {}).get("source") or expected_source or "default"
|
|
390
|
+
),
|
|
391
|
+
}
|
|
392
|
+
if verb == "skip":
|
|
393
|
+
return {"_confirm_verb": "skip"}
|
|
394
|
+
return {"_confirm_verb": "proceed"}
|
task_agent/prompts.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""自主任务 Agent 的提示词(纯函数式常量)。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
PLAN_PROMPT = """你是任务规划器。把用户目标分解为 2~8 个可独立执行的子任务。
|
|
5
|
+
要求:
|
|
6
|
+
1. 每个子任务是一个明确的、可独立完成的问题或动作;
|
|
7
|
+
2. 子任务按执行顺序排列(前面的先做);
|
|
8
|
+
3. 只输出 JSON 数组,形如:[{{"id": "1", "desc": "..."}}, ...],不要解释。
|
|
9
|
+
|
|
10
|
+
历史任务知识(仅作参考,不得编造):
|
|
11
|
+
{memory}
|
|
12
|
+
|
|
13
|
+
用户目标:
|
|
14
|
+
{goal}
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
FINAL_PROMPT = """你是结果整合器。基于已完成的子任务结果,回答用户的原始目标。
|
|
18
|
+
要求:
|
|
19
|
+
1. 引用各子任务结果,条理清晰、结构化;
|
|
20
|
+
2. 若某子任务无有效结果,明确说明;
|
|
21
|
+
3. 用中文,直接给出最终答案。
|
|
22
|
+
|
|
23
|
+
原始目标:
|
|
24
|
+
{goal}
|
|
25
|
+
|
|
26
|
+
子任务结果:
|
|
27
|
+
{findings}
|
|
28
|
+
"""
|
|
29
|
+
REPLAN_PROMPT = """你是自主任务的执行规划器。基于原始目标与已完成的步骤结果,决定**下一步**做什么。
|
|
30
|
+
可用信息来源:
|
|
31
|
+
- kb:自有知识库(公司/产品/文档等内部资料)——**优先**用它;
|
|
32
|
+
- db:数据库/时间/外部工具;
|
|
33
|
+
- web:实时/最新资讯(联网);
|
|
34
|
+
- code:计算/代码执行。
|
|
35
|
+
要求:
|
|
36
|
+
1. 公司/产品/内部资料信息优先选 kb,不要联网去查真实企业;
|
|
37
|
+
2. 只输出 JSON:{{"next_action": "具体下一步", "expected_source": "kb|db|web|code|default"}};
|
|
38
|
+
3. 已无必要动作时把 next_action 设为空字符串。
|
|
39
|
+
|
|
40
|
+
原始目标:
|
|
41
|
+
{goal}
|
|
42
|
+
|
|
43
|
+
已完成的发现:
|
|
44
|
+
{findings}
|
|
45
|
+
|
|
46
|
+
历史任务知识(仅作参考,不得编造):
|
|
47
|
+
{memory}
|
|
48
|
+
"""
|
|
49
|
+
CHECK_PROMPT = """你是任务完成度检查员。判断是否已**充分达成**原始目标(可基于已有发现回答)。
|
|
50
|
+
要求:只输出 JSON:{{"done": true/false}},不要解释。
|
|
51
|
+
|
|
52
|
+
原始目标:
|
|
53
|
+
{goal}
|
|
54
|
+
|
|
55
|
+
已有发现:
|
|
56
|
+
{findings}
|
|
57
|
+
"""
|
|
58
|
+
VERIFY_PROMPT = """你是子任务结果的质检员。上一步子任务**失败**了,判断是否值得重试。
|
|
59
|
+
规则:
|
|
60
|
+
1. 失败是临时性(网络/超时/工具未就绪/偶发生成错误) → "retry": true;
|
|
61
|
+
2. 失败是确定性(方法本身不可行/信息不存在/违反事实) → "retry": false;
|
|
62
|
+
3. 已重试多次仍失败、换方法成本更高 → "retry": false。
|
|
63
|
+
只输出 JSON:{{"retry": true/false, "reason": "一句话原因"}},不要解释。
|
|
64
|
+
|
|
65
|
+
原始目标:
|
|
66
|
+
{goal}
|
|
67
|
+
|
|
68
|
+
当前动作:
|
|
69
|
+
{action}
|
|
70
|
+
|
|
71
|
+
失败结果:
|
|
72
|
+
{finding}
|
|
73
|
+
|
|
74
|
+
已完成发现:
|
|
75
|
+
{findings}
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
COMPRESS_PROMPT = """你是任务执行记录的压缩器。把已完成的子任务结果压缩成一段简短摘要,保留关键事实与结论。
|
|
79
|
+
要求:
|
|
80
|
+
1. 中文,≤150 字;
|
|
81
|
+
2. 保留数字、专名、结论,不编造新信息;
|
|
82
|
+
3. 只输出摘要正文,不要解释。
|
|
83
|
+
|
|
84
|
+
已有摘要:
|
|
85
|
+
{summary}
|
|
86
|
+
|
|
87
|
+
新结果:
|
|
88
|
+
{findings}
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
EVAL_PROMPT = """你是自主任务执行质量的评估员。根据原始目标、执行记录与最终交付,给出质量评分。
|
|
92
|
+
评分维度(每项 0-5 分,只输出 JSON):
|
|
93
|
+
- goal_attainment:最终交付在多大程度上达成原始目标(0=完全未达成,5=充分达成)
|
|
94
|
+
- info_completeness:关键信息是否完整(缺失关键数字/结论要扣分)
|
|
95
|
+
- hallucination:交付中是否存在执行记录无法支撑的编造内容(0=无幻觉,5=严重幻觉)
|
|
96
|
+
- comment:一句话评价(中文)
|
|
97
|
+
|
|
98
|
+
原始目标:
|
|
99
|
+
{goal}
|
|
100
|
+
|
|
101
|
+
执行记录(findings):
|
|
102
|
+
{findings}
|
|
103
|
+
|
|
104
|
+
最终交付:
|
|
105
|
+
{final_answer}
|
|
106
|
+
|
|
107
|
+
只输出 JSON:{{"goal_attainment": 0-5, "info_completeness": 0-5, "hallucination": 0-5, "comment": "..."}}
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
TOOLCALL_PROMPT = """你是任务执行器。针对当前动作,选择调用工具或直接回答。
|
|
111
|
+
可用工具:
|
|
112
|
+
{tools}
|
|
113
|
+
|
|
114
|
+
输出 JSON(只输出一个):
|
|
115
|
+
- 调用工具:{{"tool": "工具名", "args": {{参数名: 值}}}}
|
|
116
|
+
- 直接回答:{{"answer": "回答内容"}}
|
|
117
|
+
|
|
118
|
+
当前动作:
|
|
119
|
+
{action}
|
|
120
|
+
"""
|
task_agent/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker file for PEP 561: package ships inline type hints.
|
task_agent/state.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""自主任务 Agent 的图状态(TaskState)。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Annotated, Optional, TypedDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _append_findings(a: Optional[list[str]], b: object) -> list[str]:
|
|
8
|
+
"""findings 的 reducer:以增量方式合并状态(节点返回新增片段,而非每次回写全量)。
|
|
9
|
+
|
|
10
|
+
特殊值 `{"_replace": [...]}`:超预算压缩时整体替换(见 nodes._append_finding)。
|
|
11
|
+
"""
|
|
12
|
+
if isinstance(b, dict) and b.get("_replace") is not None:
|
|
13
|
+
return list(b["_replace"])
|
|
14
|
+
return (a or []) + (b if isinstance(b, list) else [])
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TaskState(TypedDict):
|
|
18
|
+
"""Plan→Execute→Final 与 Replan→Execute→Check 循环的共享状态。"""
|
|
19
|
+
|
|
20
|
+
goal: str # 用户目标
|
|
21
|
+
# [fixed] 一次性计划:子任务列表 + 当前索引
|
|
22
|
+
plan: Optional[list[dict]] # [{id, desc, status, result}]
|
|
23
|
+
current_idx: Optional[int] # 当前子任务索引(fixed 用)
|
|
24
|
+
# [replan] 每步动态:当前动作 + 步数
|
|
25
|
+
current_action: Optional[str] # 下一步执行的动作(replan 用)
|
|
26
|
+
step: Optional[int] # 已完成步数(replan 用)
|
|
27
|
+
done: Optional[bool] # check/replan 判定是否完成
|
|
28
|
+
expected_source: Optional[str] # replan 标注的信息来源(kb/db/web/code/default)
|
|
29
|
+
# [verify 容错] 当前子任务失败后的重试次数(成功即归零,上限 MAX_RETRIES)
|
|
30
|
+
retries: Optional[int]
|
|
31
|
+
# [HITL 内部] 计划确认结果: proceed / edit / skip(仅节点内路由用,不下发)
|
|
32
|
+
_confirm_verb: Optional[str]
|
|
33
|
+
# 共有
|
|
34
|
+
findings: Annotated[list[str], _append_findings] # 已完成结果(节点只回增量,reducer 拼接)
|
|
35
|
+
findings_summary: Optional[str] # 超预算压缩后的历史摘要(默认空)
|
|
36
|
+
final_answer: Optional[str] # 最终交付
|
task_agent/telemetry.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""可观测适配:把 on_event 接到控制台 / Langfuse(可选 extra)。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Callable
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
EventSink = Callable[[str, dict], None]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def console_event_sink() -> EventSink:
|
|
13
|
+
"""控制台事件输出(零依赖)。"""
|
|
14
|
+
|
|
15
|
+
def sink(kind: str, data: dict) -> None:
|
|
16
|
+
print(f"[task-agent:{kind}] {data}")
|
|
17
|
+
|
|
18
|
+
return sink
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def langfuse_event_sink() -> EventSink:
|
|
22
|
+
"""Langfuse span 上报(需 `task-agent[observability]`)。
|
|
23
|
+
|
|
24
|
+
未安装 / 未配置时自动降级为控制台输出,不抛错。
|
|
25
|
+
"""
|
|
26
|
+
try:
|
|
27
|
+
from langfuse import Langfuse
|
|
28
|
+
|
|
29
|
+
lf = Langfuse()
|
|
30
|
+
|
|
31
|
+
def sink(kind: str, data: dict) -> None:
|
|
32
|
+
try:
|
|
33
|
+
if lf.get_current_observation_id() is None:
|
|
34
|
+
return # 无活动 trace 上下文,跳过以避免 Langfuse 噪音日志
|
|
35
|
+
with lf.start_as_current_observation(
|
|
36
|
+
name=f"task_agent.{kind}", type="SPAN", input=data
|
|
37
|
+
):
|
|
38
|
+
pass
|
|
39
|
+
except Exception: # noqa: BLE001 - 观测失败不影响执行
|
|
40
|
+
logger.debug("langfuse span 跳过: %s", kind, exc_info=True)
|
|
41
|
+
|
|
42
|
+
return sink
|
|
43
|
+
except Exception: # noqa: BLE001 - 依赖缺失降级
|
|
44
|
+
logger.warning("langfuse 未安装或未配置,事件降级为控制台输出")
|
|
45
|
+
return console_event_sink()
|