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
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentchat-task-agent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 自主任务 Agent:面向模糊长目标的多步自主执行引擎(LangGraph),零业务依赖
|
|
5
|
+
Author: Zhuliqx
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Zhuliqx/task-agent
|
|
8
|
+
Project-URL: Repository, https://github.com/Zhuliqx/task-agent
|
|
9
|
+
Project-URL: Issues, https://github.com/Zhuliqx/task-agent/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/Zhuliqx/task-agent/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: llm,agent,langgraph,rag,autonomous-agent
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: langgraph>=1.0
|
|
25
|
+
Provides-Extra: openai
|
|
26
|
+
Requires-Dist: openai>=1.0; extra == "openai"
|
|
27
|
+
Provides-Extra: observability
|
|
28
|
+
Requires-Dist: langfuse>=3.0; extra == "observability"
|
|
29
|
+
Provides-Extra: all
|
|
30
|
+
Requires-Dist: openai>=1.0; extra == "all"
|
|
31
|
+
Requires-Dist: langfuse>=3.0; extra == "all"
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
34
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
35
|
+
Requires-Dist: pyright>=1.1; extra == "dev"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# 自主任务 Agent(task-agent)
|
|
39
|
+
|
|
40
|
+
> 最后校验:2026-08-30(独立包;宿主集成见 Agentchat 主仓库)
|
|
41
|
+
|
|
42
|
+
> 独立的**长任务自主执行器**:接收模糊复杂目标 → LLM 分解 / 每步重规划 → 循环执行(注入 Executor)→ 结构化交付。
|
|
43
|
+
> 零业务依赖的独立 Python 包;宿主应用通过适配器注入 LLM / Checkpointer / 每步执行器。
|
|
44
|
+
|
|
45
|
+
## 一句话
|
|
46
|
+
|
|
47
|
+
面向"**模糊长目标**"的 Agentic 编排:把 LangGraph 的**交互式 HITL / 容错 / 时间旅行 / 状态管理**组合成一套可用的自主任务引擎。
|
|
48
|
+
|
|
49
|
+
## 核心能力
|
|
50
|
+
|
|
51
|
+
| 能力 | 说明 |
|
|
52
|
+
|------|------|
|
|
53
|
+
| 每步动态重规划 | `replan`(默认);也可 `mode="fixed"` 一次计划 |
|
|
54
|
+
| 独立完成度判断 | `check` 判是否达成 + `max_steps` 防循环 |
|
|
55
|
+
| 信息源感知 | replan 标注 `expected_source`(kb/db/web/code),宿主按源收紧开关 |
|
|
56
|
+
| 节点级 HITL | `interrupt` / resume,proceed/edit/skip;无 checkpointer 自动降级全自主 |
|
|
57
|
+
| verify 自检重试 | 子任务失败 → LLM 判是否重试(不计步数,`max_retries` 上限) |
|
|
58
|
+
| 节点级容错 | `retry_policy` + `timeout` + `error_handler`(返回 `Command`) |
|
|
59
|
+
| Time Travel | `list_task_history` + `checkpoint_id` 分叉 / 重放 |
|
|
60
|
+
| 状态治理 | `findings` reducer 增量合并 |
|
|
61
|
+
| 工具调用执行器 | `ToolCallingExecutor` + 内置 calculator/time/random(零依赖) |
|
|
62
|
+
| 跨任务记忆 | 任务结论沉淀与召回(`InMemoryMemory` 或宿主实现 `TaskMemory`) |
|
|
63
|
+
|
|
64
|
+
## 安装
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install agentchat-task-agent # 从 PyPI 安装(发行名)
|
|
68
|
+
pip install 'agentchat-task-agent[all]' # openai + observability(Langfuse)
|
|
69
|
+
# 仓库内开发:
|
|
70
|
+
pip install -e . # 或 pip install -e <仓库路径>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 快速体验(离线 demo,无需 API key)
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
python -m task_agent.demo
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
输出 replan 全流程的 findings 与 final_answer。设置 `TASK_AGENT_OPENAI_API_KEY`(可选 `TASK_AGENT_OPENAI_BASE_URL` / `TASK_AGENT_OPENAI_MODEL`)后自动切换真实 OpenAI 兼容端点。
|
|
80
|
+
|
|
81
|
+
## 编程接口
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from task_agent import TaskAgentConfig, build_agent
|
|
85
|
+
from task_agent.executor import ExecuteRequest, StepResult
|
|
86
|
+
|
|
87
|
+
async def my_executor(request: ExecuteRequest) -> StepResult:
|
|
88
|
+
# 宿主在此提供真实工具能力(检索/数据库/搜索/代码…)
|
|
89
|
+
return StepResult(answer=f"执行了 {request.action}")
|
|
90
|
+
|
|
91
|
+
agent = build_agent(
|
|
92
|
+
config=TaskAgentConfig(mode="replan", hitl=True),
|
|
93
|
+
llm_factory=llm_factory, # Callable[[], LLM]
|
|
94
|
+
checkpointer_provider=checkpointer_provider, # Callable[[], Any | None]
|
|
95
|
+
executor=my_executor, # 缺省为纯 LLM 直答
|
|
96
|
+
on_event=on_event, # 可选: (kind, data) 事件回调
|
|
97
|
+
)
|
|
98
|
+
result = await agent.ainvoke({"goal": "..."}) # -> {findings, final_answer, ...}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
接口缝(详见 `src/task_agent/`):
|
|
102
|
+
- `TaskAgentConfig`:mode / hitl / max_retries / max_steps / llm_timeout / llm_max_retries /
|
|
103
|
+
findings_budget(findings 保留上限,超限自动压缩历史);
|
|
104
|
+
- `LLM` / `LLMFactory`:仅需 `async ainvoke(prompt)`;
|
|
105
|
+
- `CheckpointerProvider`:返回 LangGraph checkpointer 或 None(无状态降级);
|
|
106
|
+
- `Executor` / `ExecuteRequest(action, source)` / `StepResult(answer)`:每步执行端口;
|
|
107
|
+
- `memory`:可选跨任务记忆(`build_agent(..., memory=...)`)。
|
|
108
|
+
|
|
109
|
+
## 工具调用执行器(开箱即用)
|
|
110
|
+
|
|
111
|
+
不依赖宿主也能"自己干活":`ToolCallingExecutor` 让 LLM 决定调工具或直答,内置纯计算工具
|
|
112
|
+
(calculator / current_time / random_number,全部零依赖、AST 白名单求值):
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from task_agent import TaskAgentConfig, build_agent
|
|
116
|
+
from task_agent.tools import ToolCallingExecutor, builtin_tools
|
|
117
|
+
|
|
118
|
+
agent = build_agent(
|
|
119
|
+
config=TaskAgentConfig(mode="replan", hitl=False),
|
|
120
|
+
llm_factory=llm_factory,
|
|
121
|
+
executor=ToolCallingExecutor(llm_factory, builtin_tools),
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
宿主也可实现自己的 `Executor` 注入(接口缝不变)。工具声明用零依赖的 `Tool` dataclass
|
|
126
|
+
(name / description / parameters / func),支持同步与异步函数。
|
|
127
|
+
|
|
128
|
+
## CLI
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
task-agent run "介绍一下公司并计算质数和" --llm openai --tools --event --json
|
|
132
|
+
task-agent run "目标" --mode fixed --findings-budget 5
|
|
133
|
+
task-agent demo
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`--llm fake`(默认)离线脚本化;`--tools` 启用内置工具;`--memory` 启用跨任务记忆;
|
|
137
|
+
`--event` 打印执行过程。
|
|
138
|
+
|
|
139
|
+
## 跨任务记忆
|
|
140
|
+
|
|
141
|
+
`build_agent(..., memory=...)` 传入实现 `TaskMemory` 的对象:
|
|
142
|
+
任务开始时按目标召回历史结论(注入 replan/plan 上下文),结束后把 `final_answer` 沉淀回记忆。
|
|
143
|
+
内置 `InMemoryMemory`(关键词召回,进程内);宿主可对接自己的 Store 实现。
|
|
144
|
+
|
|
145
|
+
## 可观测
|
|
146
|
+
|
|
147
|
+
`on_event` 是唯一接入点,可接任意可观测后端:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from task_agent.telemetry import langfuse_event_sink
|
|
151
|
+
|
|
152
|
+
agent = build_agent(..., on_event=langfuse_event_sink()) # 需 task-agent[observability]
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
未安装 Langfuse 时自动降级为控制台输出,不抛错。
|
|
156
|
+
|
|
157
|
+
## 发布到 PyPI
|
|
158
|
+
|
|
159
|
+
变更记录见 [CHANGELOG](https://github.com/Zhuliqx/task-agent/blob/main/CHANGELOG.md)。
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
python -m build
|
|
163
|
+
python -m twine upload --repository testpypi dist/* # 先发 TestPyPI 验证
|
|
164
|
+
python -m twine upload dist/* # 正式 PyPI(项目级令牌)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
发行名 `agentchat-task-agent`(import 名 `task_agent`,CLI 命令 `task-agent`)。
|
|
168
|
+
打 `v*` 标签可触发 CI 自动发布(需在仓库 Secrets 配置 `PYPI_API_TOKEN`,见
|
|
169
|
+
`.github/workflows/publish.yml`)。
|
|
170
|
+
|
|
171
|
+
## 事件回调(过程可见)
|
|
172
|
+
|
|
173
|
+
`build_agent(on_event=...)` 会收到生命周期事件:`plan / replan / execute / check / verify / hitl / final`,
|
|
174
|
+
数据形如 `{"action": ..., "source": ..., "ok": ...}`。宿主可接 SSE 或日志:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
def on_event(kind: str, data: dict) -> None:
|
|
178
|
+
print(f"[{kind}] {data}")
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## 长任务记忆治理(findings 压缩)
|
|
182
|
+
|
|
183
|
+
长任务 `findings` 会持续累积。设 `TaskAgentConfig(findings_budget=N)` 后,超过 N 条时把历史
|
|
184
|
+
交给 LLM 压缩进 `findings_summary`,仅保留最新一条——控制后续 replan/check/final 的上下文
|
|
185
|
+
与 token 成本(LLM 失败自动退化为截断拼接,不中断执行)。
|
|
186
|
+
|
|
187
|
+
## 基准与容错
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
cd task-agent
|
|
191
|
+
python benchmarks/bench_task_agent.py # fixed vs replan 结构指标对比(离线)
|
|
192
|
+
python benchmarks/bench_task_agent.py --judge # 追加质量评估(离线=规则代理)
|
|
193
|
+
python benchmarks/bench_task_agent.py --llm openai --judge --out results/bench.json # 真实 LLM 指标 + LLM-judge 打分
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
离线模式统计完成率 / 答案命中 / 平均执行步数 / 平均重试 / 平均耗时;真实质量指标用
|
|
197
|
+
`--llm openai`(配 `TASK_AGENT_OPENAI_API_KEY`);`--judge` 开启 LLM-judge 质量评估
|
|
198
|
+
(目标达成度 / 信息完整性 / 幻觉,0-1,见 `src/task_agent/judge.py`)。容错通过混沌测试验证
|
|
199
|
+
(`tests/test_resilience.py`:执行器随机失败 / 永久失败 / LLM 永久失败均能收敛交付)。
|
|
200
|
+
宿主侧评估(真实 LLM + judge)位于 Agentchat 主仓库的 `backend/scripts/eval_task_agent.py`,
|
|
201
|
+
本仓库用 `python benchmarks/bench_task_agent.py --llm openai --judge` 做等价质量评估。
|
|
202
|
+
|
|
203
|
+
### 实测快照(真实 LLM:DeepSeek-chat,3 自包含任务 × 2 轮,2026-08-29)
|
|
204
|
+
|
|
205
|
+
| 模式 | 完成率 | 目标达成 | 信息完整 | 幻觉 | 平均步数 | 平均耗时 |
|
|
206
|
+
|------|--------|----------|----------|------|----------|----------|
|
|
207
|
+
| replan | 1.000 | 1.000 | 1.000 | 0.000 | **1.0** | **6.0s** |
|
|
208
|
+
| fixed | 1.000 | 1.000 | 1.000 | 0.000 | 3.0 | 12.0s |
|
|
209
|
+
|
|
210
|
+
**关键发现**:简单自包含任务上,replan 一步收敛(LLM 直答 → check 判完成),fixed 因
|
|
211
|
+
"先拆计划"多出 2 个执行步、耗时翻倍;而离线脚本化基准中 fixed 反而更快(脚本化计划恰好 2 步)。
|
|
212
|
+
结论:**脚本化基准只能测结构,真实 LLM 才能暴露行为差异**——两者都要跑,别只用离线数字下结论。
|
|
213
|
+
|
|
214
|
+
## 目录
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
task-agent/
|
|
218
|
+
├── pyproject.toml
|
|
219
|
+
├── benchmarks/bench_task_agent.py # fixed vs replan 基准
|
|
220
|
+
├── src/task_agent/
|
|
221
|
+
│ ├── config.py # TaskAgentConfig(运行配置)
|
|
222
|
+
│ ├── llm.py # LLM 协议 + 文本抽取
|
|
223
|
+
│ ├── executor.py # ExecuteRequest / StepResult / Executor / DefaultExecutor
|
|
224
|
+
│ ├── state.py # TaskState(findings reducer)
|
|
225
|
+
│ ├── prompts.py # PLAN/FINAL/REPLAN/CHECK/VERIFY/COMPRESS 提示词
|
|
226
|
+
│ ├── judge.py # 任务级质量评估(LLM-judge,0-1 三指标)
|
|
227
|
+
│ ├── tools.py # ToolCallingExecutor + 内置工具(零依赖)
|
|
228
|
+
│ ├── memory.py # TaskMemory / InMemoryMemory(跨任务记忆)
|
|
229
|
+
│ ├── telemetry.py # 控制台 / Langfuse 事件接入
|
|
230
|
+
│ ├── cli.py # task-agent 命令行入口
|
|
231
|
+
│ ├── nodes.py # 节点(闭包注入 Runtime)
|
|
232
|
+
│ ├── graph.py # build_agent / list_task_history / 路由 / error_handler
|
|
233
|
+
│ └── demo.py # 离线 demo(脚本化 FakeLLM)
|
|
234
|
+
└── tests/
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## 测试
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
cd task-agent && pytest -q
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
覆盖:解析 / 路由 / HITL(含无 checkpointer 降级)/ verify / error_handler / 执行节点 /
|
|
244
|
+
默认执行器 / Time Travel / fixed 全流程 / demo 离线全流程 / 容错混沌注入 / findings 压缩 / 事件流。
|
|
245
|
+
|
|
246
|
+
## 宿主集成(Agentchat 主仓库)
|
|
247
|
+
|
|
248
|
+
本包零业务依赖、可独立使用;作为 Agentchat 平台的"项目 2",宿主通过适配器注入
|
|
249
|
+
LLM / Checkpointer / 执行器,并提供 `/api/agent-tasks/run`、`/api/agent-tasks/run/stream`(SSE)、
|
|
250
|
+
`/api/agent-tasks/history`、`/api/agent-tasks/confirm` 等 API。宿主仓库地址:
|
|
251
|
+
`https://github.com/Zhuliqx/Agentchat`。
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
agentchat_task_agent-0.1.0.dist-info/licenses/LICENSE,sha256=WZHe1XVNDnqb4kXlXmV0ab6Q0u27AfxWuf2Uq6iN3VE,1085
|
|
2
|
+
task_agent/__init__.py,sha256=ZE8cik2PrD8_E8Cfm-zd5zIpZJtgzCTpVhqSrQW6DGo,1413
|
|
3
|
+
task_agent/cli.py,sha256=67nbtDZkCRHMNR09dT_3ozRFHeJ9PWHrGGyxc7FRR8E,3500
|
|
4
|
+
task_agent/config.py,sha256=PtZz85JkhUbtzkKzKncMNJ_hhQEkXmDRFelZ7-bRfDw,1734
|
|
5
|
+
task_agent/demo.py,sha256=Bue9Nbxys5xYyYj7irLLtT0y-zbqLTIbVRn7UgNGvVw,4830
|
|
6
|
+
task_agent/executor.py,sha256=TzGbvzoD03rsGU8jBTr-qQtz97soFem7q5pbIixhLJE,1339
|
|
7
|
+
task_agent/graph.py,sha256=N1KsGRGaN-23cIfYCB2nrt9TFqTDvMPhkhrwGWtL274,11697
|
|
8
|
+
task_agent/judge.py,sha256=ETcBs-_fOYIxc5ClgZKltsxpe0D_xXTXzzjMbYiJWiQ,1675
|
|
9
|
+
task_agent/llm.py,sha256=8uZ564PrHg6FHkXTyGzyVU8xQqoN0hCoDECHV2STeVI,663
|
|
10
|
+
task_agent/memory.py,sha256=rGQxgZ74dwQAwRaJehw3gwpZGOJxky2p61Oj6KnrxO8,1517
|
|
11
|
+
task_agent/nodes.py,sha256=6CciGHwOIcQFFPvM_IM6vlcB7kSyH4DEFbqJvM357ZA,15804
|
|
12
|
+
task_agent/prompts.py,sha256=uynTUF7EleLvLCSurb9z4_QzkrOGcv-eic0PAZFqmLk,3980
|
|
13
|
+
task_agent/py.typed,sha256=wA_3LnN5vepX3IBp_v0QcoM3IcA_GbNzJJP8E8oHjAk,60
|
|
14
|
+
task_agent/state.py,sha256=3xHNUmPIb7lp0Lu_UrXw4ooIcqQQljvjczqA8-RIHt0,1940
|
|
15
|
+
task_agent/telemetry.py,sha256=xcF7rT9ezECIcQGBope68kkGdLzJjtOn0AKmcgoBXIw,1504
|
|
16
|
+
task_agent/tools.py,sha256=jq4223kX03Pa4bVdoZOs5lbH8NPxi8ZmLmTvJEJb4w8,5915
|
|
17
|
+
agentchat_task_agent-0.1.0.dist-info/METADATA,sha256=JxQl_5tq22jWmNLfmU2GbRe6arn9S8XI-t7hW-GaNMs,11793
|
|
18
|
+
agentchat_task_agent-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
19
|
+
agentchat_task_agent-0.1.0.dist-info/entry_points.txt,sha256=Exz9qttpb6NCkEGEHDPXGnS-NrxFF3uwM-gPY-Pq0pE,90
|
|
20
|
+
agentchat_task_agent-0.1.0.dist-info/top_level.txt,sha256=3ddbQYmRr6d_vmp-nvGuLLxJWP7ckOHqOzd4waec6Uo,11
|
|
21
|
+
agentchat_task_agent-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zhuliqx
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
task_agent
|
task_agent/__init__.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""自主任务 Agent(独立包,零业务依赖)。
|
|
2
|
+
|
|
3
|
+
面向模糊长目标的多步自主执行引擎:接收目标 → 每步重规划(或一次计划)→
|
|
4
|
+
循环执行(注入 Executor)→ 完成度检查 → 结构化交付。
|
|
5
|
+
|
|
6
|
+
使用:
|
|
7
|
+
from task_agent import TaskAgentConfig, build_agent
|
|
8
|
+
|
|
9
|
+
agent = build_agent(
|
|
10
|
+
config=TaskAgentConfig(mode="replan"),
|
|
11
|
+
llm_factory=llm_factory, # Callable[[], LLM]
|
|
12
|
+
checkpointer_provider=checkpointer_provider, # Callable[[], Any | None]
|
|
13
|
+
executor=executor, # Callable[[ExecuteRequest], Awaitable[StepResult]]
|
|
14
|
+
)
|
|
15
|
+
result = await agent.ainvoke({"goal": "..."})
|
|
16
|
+
"""
|
|
17
|
+
from task_agent.config import TaskAgentConfig
|
|
18
|
+
from task_agent.executor import (
|
|
19
|
+
DefaultExecutor,
|
|
20
|
+
ExecuteRequest,
|
|
21
|
+
Executor,
|
|
22
|
+
SOURCE_KEYS,
|
|
23
|
+
StepResult,
|
|
24
|
+
)
|
|
25
|
+
from task_agent.graph import build_agent, list_task_history
|
|
26
|
+
from task_agent.judge import judge_task
|
|
27
|
+
from task_agent.memory import InMemoryMemory
|
|
28
|
+
from task_agent.tools import Tool, ToolCallingExecutor, builtin_tools
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"TaskAgentConfig",
|
|
32
|
+
"build_agent",
|
|
33
|
+
"list_task_history",
|
|
34
|
+
"ExecuteRequest",
|
|
35
|
+
"StepResult",
|
|
36
|
+
"Executor",
|
|
37
|
+
"DefaultExecutor",
|
|
38
|
+
"SOURCE_KEYS",
|
|
39
|
+
"Tool",
|
|
40
|
+
"ToolCallingExecutor",
|
|
41
|
+
"builtin_tools",
|
|
42
|
+
"InMemoryMemory",
|
|
43
|
+
"judge_task",
|
|
44
|
+
]
|
task_agent/cli.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""task-agent 命令行入口。
|
|
2
|
+
|
|
3
|
+
用法:
|
|
4
|
+
task-agent run "目标" [--mode replan|fixed] [--llm fake|openai] [--tools] [--memory] [--event] [--json]
|
|
5
|
+
task-agent demo
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import asyncio
|
|
11
|
+
import json
|
|
12
|
+
from typing import Callable
|
|
13
|
+
|
|
14
|
+
from task_agent.config import TaskAgentConfig
|
|
15
|
+
from task_agent.demo import _OpenAICompatLLM, _ScriptedLLM
|
|
16
|
+
from task_agent.graph import build_agent
|
|
17
|
+
from task_agent.memory import InMemoryMemory
|
|
18
|
+
from task_agent.tools import ToolCallingExecutor, builtin_tools
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _llm_factory(kind: str):
|
|
22
|
+
if kind == "openai":
|
|
23
|
+
llm = _OpenAICompatLLM()
|
|
24
|
+
return lambda: llm
|
|
25
|
+
scripted = _ScriptedLLM()
|
|
26
|
+
return lambda: scripted
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def run_task(goal: str, args: argparse.Namespace) -> dict:
|
|
30
|
+
"""构建图并执行(供 CLI 与测试复用)。"""
|
|
31
|
+
config = TaskAgentConfig(
|
|
32
|
+
mode=args.mode,
|
|
33
|
+
hitl=args.hitl,
|
|
34
|
+
max_steps=args.max_steps,
|
|
35
|
+
findings_budget=args.findings_budget,
|
|
36
|
+
)
|
|
37
|
+
executor = (
|
|
38
|
+
ToolCallingExecutor(_llm_factory(args.llm), builtin_tools)
|
|
39
|
+
if args.tools
|
|
40
|
+
else None
|
|
41
|
+
)
|
|
42
|
+
on_event: Callable[[str, dict], None] | None = None
|
|
43
|
+
if args.event:
|
|
44
|
+
|
|
45
|
+
def _on_event(kind: str, data: dict) -> None:
|
|
46
|
+
print(f" [event] {kind}: {data}")
|
|
47
|
+
|
|
48
|
+
on_event = _on_event
|
|
49
|
+
|
|
50
|
+
agent = build_agent(
|
|
51
|
+
config=config,
|
|
52
|
+
llm_factory=_llm_factory(args.llm),
|
|
53
|
+
checkpointer_provider=lambda: None,
|
|
54
|
+
executor=executor,
|
|
55
|
+
on_event=on_event,
|
|
56
|
+
memory=InMemoryMemory() if args.memory else None,
|
|
57
|
+
)
|
|
58
|
+
return await agent.ainvoke({"goal": goal})
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main(argv: list[str] | None = None) -> int:
|
|
62
|
+
ap = argparse.ArgumentParser(prog="task-agent", description="自主任务 Agent 命令行")
|
|
63
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
64
|
+
|
|
65
|
+
p_run = sub.add_parser("run", help="执行一个目标任务")
|
|
66
|
+
p_run.add_argument("goal")
|
|
67
|
+
p_run.add_argument("--mode", choices=["replan", "fixed"], default="replan")
|
|
68
|
+
p_run.add_argument("--hitl", action="store_true", help="开启节点级人工确认(需 checkpointer)")
|
|
69
|
+
p_run.add_argument("--max-steps", type=int, default=8)
|
|
70
|
+
p_run.add_argument("--findings-budget", type=int, default=None, help="findings 压缩上限")
|
|
71
|
+
p_run.add_argument("--llm", choices=["fake", "openai"], default="fake", help="fake=脚本化离线 / openai=真实端点")
|
|
72
|
+
p_run.add_argument("--tools", action="store_true", help="启用内置工具(calculator/time/random)")
|
|
73
|
+
p_run.add_argument("--memory", action="store_true", help="启用跨任务记忆(进程内)")
|
|
74
|
+
p_run.add_argument("--event", action="store_true", help="打印执行事件")
|
|
75
|
+
p_run.add_argument("--json", action="store_true", help="以 JSON 输出结果")
|
|
76
|
+
|
|
77
|
+
sub.add_parser("demo", help="离线 demo")
|
|
78
|
+
|
|
79
|
+
args = ap.parse_args(argv)
|
|
80
|
+
if args.cmd == "demo":
|
|
81
|
+
from task_agent.demo import main as demo_main
|
|
82
|
+
|
|
83
|
+
demo_main()
|
|
84
|
+
return 0
|
|
85
|
+
|
|
86
|
+
result = asyncio.run(run_task(args.goal, args))
|
|
87
|
+
if args.json:
|
|
88
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
89
|
+
else:
|
|
90
|
+
print("\nfindings:")
|
|
91
|
+
for i, f in enumerate(result.get("findings") or [], 1):
|
|
92
|
+
print(f" [{i}] {f}")
|
|
93
|
+
print("\nfinal_answer:", result.get("final_answer"))
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
raise SystemExit(main())
|
task_agent/config.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""任务 Agent 配置(替代原宿主 app.config 的硬依赖)。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class TaskAgentConfig:
|
|
9
|
+
"""自主任务 Agent 运行配置。
|
|
10
|
+
|
|
11
|
+
与宿主环境变量的对应关系(由宿主适配器负责映射):
|
|
12
|
+
- mode ← TASK_AGENT_MODE
|
|
13
|
+
- hitl ← TASK_AGENT_HITL
|
|
14
|
+
- max_retries ← TASK_AGENT_MAX_RETRIES
|
|
15
|
+
- llm_timeout / llm_max_retries ← 宿主 LLM_TIMEOUT / LLM_MAX_RETRIES(节点级重试策略)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
mode: str = "replan" # replan=每步动态重规划(默认) / fixed=一次性计划
|
|
19
|
+
hitl: bool = True # 节点级人工确认(依赖 checkpointer,无则自动降级全自主)
|
|
20
|
+
max_retries: int = 2 # verify 容错:单个子任务失败后自检的最大重试次数
|
|
21
|
+
max_steps: int = 8 # replan 模式步数上限(防循环)
|
|
22
|
+
llm_timeout: float = 60.0 # 单次 LLM 调用超时(节点级 timeout 用)
|
|
23
|
+
llm_max_retries: int = 2 # 节点级瞬时错误重试次数
|
|
24
|
+
findings_budget: int | None = None # findings 保留条数上限;超限把历史压缩进 findings_summary(None=不限)
|
|
25
|
+
|
|
26
|
+
def __post_init__(self) -> None:
|
|
27
|
+
if self.mode not in ("replan", "fixed"):
|
|
28
|
+
raise ValueError(f"未知任务模式: {self.mode!r}(支持 replan / fixed)")
|
|
29
|
+
if self.max_retries < 0 or self.max_steps <= 0:
|
|
30
|
+
raise ValueError("max_retries >= 0 且 max_steps > 0")
|
|
31
|
+
if self.findings_budget is not None and self.findings_budget <= 0:
|
|
32
|
+
raise ValueError("findings_budget 必须为正整数或 None(不限制)")
|
task_agent/demo.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""离线可运行 demo(无需 API key,脚本化 LLM)。
|
|
2
|
+
|
|
3
|
+
用法:
|
|
4
|
+
python -m task_agent.demo # 离线 FakeLLM
|
|
5
|
+
TASK_AGENT_OPENAI_API_KEY=xxx python -m task_agent.demo # 真实 OpenAI 兼容端点
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from functools import lru_cache
|
|
13
|
+
from types import SimpleNamespace
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from task_agent.config import TaskAgentConfig
|
|
17
|
+
from task_agent.graph import build_agent
|
|
18
|
+
from task_agent.llm import LLM
|
|
19
|
+
|
|
20
|
+
DEMO_GOAL = "介绍一下公司(知识库)并计算 1 到 100 所有质数的和"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _ScriptedLLM:
|
|
24
|
+
"""按提示词特征返回脚本化 JSON 的假 LLM(确定性、可离线复现)。"""
|
|
25
|
+
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
self._replan_calls = 0
|
|
28
|
+
self._check_calls = 0
|
|
29
|
+
|
|
30
|
+
async def ainvoke(self, prompt: str) -> Any:
|
|
31
|
+
if "可用信息来源" in prompt: # REPLAN_PROMPT 特征
|
|
32
|
+
self._replan_calls += 1
|
|
33
|
+
if self._replan_calls == 1:
|
|
34
|
+
return SimpleNamespace(
|
|
35
|
+
content='{"next_action": "查询知识库中公司的成立年份", "expected_source": "kb"}'
|
|
36
|
+
)
|
|
37
|
+
return SimpleNamespace(
|
|
38
|
+
content='{"next_action": "计算 1 到 100 所有质数的和", "expected_source": "code"}'
|
|
39
|
+
)
|
|
40
|
+
if "完成度检查员" in prompt: # CHECK_PROMPT 特征
|
|
41
|
+
self._check_calls += 1
|
|
42
|
+
return SimpleNamespace(
|
|
43
|
+
content='{"done": false}'
|
|
44
|
+
if self._check_calls == 1
|
|
45
|
+
else '{"done": true}'
|
|
46
|
+
)
|
|
47
|
+
if "结果整合器" in prompt: # FINAL_PROMPT 特征
|
|
48
|
+
return SimpleNamespace(
|
|
49
|
+
content="已完成目标:公司成立于 2020 年;1 到 100 质数和为 1060。"
|
|
50
|
+
)
|
|
51
|
+
if "可用工具" in prompt: # TOOLCALL_PROMPT 特征 → 直接回答
|
|
52
|
+
return SimpleNamespace(
|
|
53
|
+
content='{"answer": "根据知识库,公司成立于 2020 年;质数和为 1060。"}'
|
|
54
|
+
)
|
|
55
|
+
# 执行步(DefaultExecutor 直答)
|
|
56
|
+
return SimpleNamespace(content="根据知识库,公司成立于 2020 年;质数和为 1060。")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class _OpenAICompatLLM:
|
|
60
|
+
"""可选:配置了 TASK_AGENT_OPENAI_API_KEY 时使用真实 OpenAI 兼容端点。"""
|
|
61
|
+
|
|
62
|
+
def __init__(self) -> None:
|
|
63
|
+
from openai import AsyncOpenAI
|
|
64
|
+
|
|
65
|
+
self._client = AsyncOpenAI(
|
|
66
|
+
api_key=os.environ.get("TASK_AGENT_OPENAI_API_KEY"),
|
|
67
|
+
base_url=os.environ.get("TASK_AGENT_OPENAI_BASE_URL")
|
|
68
|
+
or "https://api.deepseek.com",
|
|
69
|
+
)
|
|
70
|
+
self._model = os.environ.get("TASK_AGENT_OPENAI_MODEL") or "deepseek-chat"
|
|
71
|
+
|
|
72
|
+
async def ainvoke(self, prompt: str) -> Any:
|
|
73
|
+
resp = await self._client.chat.completions.create(
|
|
74
|
+
model=self._model,
|
|
75
|
+
messages=[{"role": "user", "content": prompt}],
|
|
76
|
+
temperature=0.0,
|
|
77
|
+
)
|
|
78
|
+
return SimpleNamespace(content=resp.choices[0].message.content or "")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@lru_cache(maxsize=1)
|
|
82
|
+
def _openai_llm() -> _OpenAICompatLLM:
|
|
83
|
+
return _OpenAICompatLLM()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def run_demo_flow(
|
|
87
|
+
goal: str = DEMO_GOAL, on_event=None
|
|
88
|
+
) -> dict:
|
|
89
|
+
"""跑一遍完整 replan 流程(无 checkpointer → 无 HITL)。返回图执行结果。"""
|
|
90
|
+
scripted = _ScriptedLLM()
|
|
91
|
+
|
|
92
|
+
def _factory() -> LLM:
|
|
93
|
+
if os.environ.get("TASK_AGENT_OPENAI_API_KEY"):
|
|
94
|
+
return _openai_llm()
|
|
95
|
+
return scripted
|
|
96
|
+
|
|
97
|
+
agent = build_agent(
|
|
98
|
+
config=TaskAgentConfig(mode="replan", hitl=False),
|
|
99
|
+
llm_factory=_factory,
|
|
100
|
+
checkpointer_provider=lambda: None,
|
|
101
|
+
on_event=on_event,
|
|
102
|
+
)
|
|
103
|
+
return await agent.ainvoke({"goal": goal})
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def main() -> None:
|
|
107
|
+
reconfigure = getattr(sys.stdout, "reconfigure", None)
|
|
108
|
+
if reconfigure is not None:
|
|
109
|
+
try:
|
|
110
|
+
reconfigure(encoding="utf-8", errors="replace")
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
print("== task-agent demo(自主任务 Agent)==")
|
|
114
|
+
print(f"目标: {DEMO_GOAL}")
|
|
115
|
+
if os.environ.get("TASK_AGENT_OPENAI_API_KEY"):
|
|
116
|
+
print("LLM: OpenAI 兼容端点(TASK_AGENT_OPENAI_*)")
|
|
117
|
+
else:
|
|
118
|
+
print("LLM: 脚本化 FakeLLM(离线,无需 key)")
|
|
119
|
+
def _on_event(kind: str, data: dict) -> None:
|
|
120
|
+
payload = " ".join(f"{k}={v}" for k, v in data.items())
|
|
121
|
+
print(f" [event] {kind}: {payload}")
|
|
122
|
+
|
|
123
|
+
print("\n执行过程:")
|
|
124
|
+
result = asyncio.run(run_demo_flow(on_event=_on_event))
|
|
125
|
+
print("\nfindings:")
|
|
126
|
+
for i, f in enumerate(result.get("findings") or [], 1):
|
|
127
|
+
print(f" [{i}] {f}")
|
|
128
|
+
print("\nfinal_answer:", result.get("final_answer"))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
main()
|
task_agent/executor.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""每步执行接口与默认实现(纯 LLM 直答)。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Literal, Protocol
|
|
6
|
+
|
|
7
|
+
from task_agent.llm import LLMFactory, llm_text
|
|
8
|
+
|
|
9
|
+
Source = Literal["kb", "db", "web", "code", "default"]
|
|
10
|
+
SOURCE_KEYS: tuple[Source, ...] = ("kb", "db", "web", "code", "default")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class ExecuteRequest:
|
|
15
|
+
"""一步动作及其信息源提示(供宿主决定启用哪些工具/开关)。"""
|
|
16
|
+
|
|
17
|
+
action: str
|
|
18
|
+
source: Source = "default"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class StepResult:
|
|
23
|
+
"""一步执行的结构化结果(answer 为供整合的文本)。"""
|
|
24
|
+
|
|
25
|
+
answer: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Executor(Protocol):
|
|
29
|
+
"""执行一步动作。失败应抛异常(由节点捕获并标记为该步失败)。"""
|
|
30
|
+
|
|
31
|
+
async def __call__(self, request: ExecuteRequest) -> StepResult: ...
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DefaultExecutor:
|
|
35
|
+
"""默认执行器:不调用任何工具,直接由 LLM 回答当前动作(纯 LLM 直答)。"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, llm_factory: LLMFactory) -> None:
|
|
38
|
+
self._llm_factory = llm_factory
|
|
39
|
+
|
|
40
|
+
async def __call__(self, request: ExecuteRequest) -> StepResult:
|
|
41
|
+
answer = (await llm_text(self._llm_factory(), request.action)).strip()
|
|
42
|
+
return StepResult(answer=answer)
|