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/graph.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""自主任务 Agent 的 LangGraph 图(fixed 一期 / replan 二期,依赖注入)。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
from langgraph.errors import NodeError, NodeTimeoutError
|
|
8
|
+
from langgraph.graph import END, START, StateGraph
|
|
9
|
+
from langgraph.types import Command, RetryPolicy
|
|
10
|
+
|
|
11
|
+
from task_agent.config import TaskAgentConfig
|
|
12
|
+
from task_agent.executor import DefaultExecutor, Executor
|
|
13
|
+
from task_agent.llm import LLMFactory
|
|
14
|
+
from task_agent.memory import TaskMemory
|
|
15
|
+
from task_agent.nodes import Runtime, _is_failed_finding, make_nodes
|
|
16
|
+
from task_agent.state import TaskState
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ---------------- 条件路由 ----------------
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _route_fixed(state: dict) -> str:
|
|
25
|
+
"""fixed:子任务未执行完则继续 execute,否则跳 final。"""
|
|
26
|
+
return "final" if state["current_idx"] >= len(state["plan"]) else "continue"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _route_replan(state: dict) -> str:
|
|
30
|
+
"""check 之后:done 则 final,否则回 replan 重新规划下一步。"""
|
|
31
|
+
return "final" if state.get("done") else "replan"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _route_after_replan(state: dict) -> str:
|
|
35
|
+
"""replan 之后:空动作(或已判 done)则直接结束,否则交 HITL 确认(关闭时透传执行)。"""
|
|
36
|
+
return "final" if state.get("done") or not state.get("current_action") else "confirm"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _route_after_confirm(state: dict) -> str:
|
|
40
|
+
"""HITL 确认之后:skip 则进 check(重新判断/重规划),否则执行该动作。"""
|
|
41
|
+
return "check" if (state.get("_confirm_verb") or "proceed") == "skip" else "execute"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _route_after_execute(config: TaskAgentConfig = TaskAgentConfig()):
|
|
45
|
+
"""执行之后:失败且未到重试上限 → 自检 verify;否则直接进 check。"""
|
|
46
|
+
|
|
47
|
+
def route(state: dict) -> str:
|
|
48
|
+
findings = state.get("findings") or []
|
|
49
|
+
last = findings[-1] if findings else ""
|
|
50
|
+
retries = int(state.get("retries") or 0)
|
|
51
|
+
if _is_failed_finding(last) and retries < config.max_retries:
|
|
52
|
+
return "verify"
|
|
53
|
+
return "check"
|
|
54
|
+
|
|
55
|
+
return route
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _route_after_verify(state: dict) -> str:
|
|
59
|
+
"""自检之后:判定值得重试 → 回执行;否则进 check。"""
|
|
60
|
+
return "execute" if state.get("should_retry") else "check"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------- 节点级 retry + timeout + error_handler ----------------
|
|
64
|
+
# LLM 节点失败 → retry_policy 重试(瞬时错误) → 耗尽后 error_handler 降级(返回 Command 才能续跑)。
|
|
65
|
+
# execute/execute_action 是"业务子任务"(失败标记 finding),保留节点内 try/except,不参与重试。
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _is_transient(exc: BaseException) -> bool:
|
|
69
|
+
"""判断 LLM 节点失败是否为"瞬时错误"(值得重试)。
|
|
70
|
+
|
|
71
|
+
官方默认 retry_on 对 OSError 子类、HTTP 库非 5xx 错误不重试,而 LLM 网络/连接/超时
|
|
72
|
+
异常多属此类,故这里显式匹配:节点超时、网络/连接、限流、5xx。确定性错误不重试。
|
|
73
|
+
"""
|
|
74
|
+
if isinstance(exc, NodeTimeoutError):
|
|
75
|
+
return True
|
|
76
|
+
code = getattr(exc, "status_code", None) or getattr(
|
|
77
|
+
getattr(exc, "response", None), "status_code", None
|
|
78
|
+
)
|
|
79
|
+
if code and 500 <= int(code) < 600:
|
|
80
|
+
return True
|
|
81
|
+
name = type(exc).__name__
|
|
82
|
+
if name in {"ValueError", "TypeError", "ArithmeticError", "KeyError", "StopIteration"}:
|
|
83
|
+
return False
|
|
84
|
+
s = f"{name} {exc}".lower()
|
|
85
|
+
return any(
|
|
86
|
+
k in s
|
|
87
|
+
for k in (
|
|
88
|
+
"timeout",
|
|
89
|
+
"connection",
|
|
90
|
+
"network",
|
|
91
|
+
"unavailable",
|
|
92
|
+
"temporary",
|
|
93
|
+
"rate limit",
|
|
94
|
+
"too many",
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _llm_retry() -> RetryPolicy:
|
|
100
|
+
"""瞬时错误重试(max_attempts=2)。"""
|
|
101
|
+
return RetryPolicy(max_attempts=2, retry_on=_is_transient)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _llm_timeout(config: TaskAgentConfig) -> float:
|
|
105
|
+
"""节点超时:留足客户端重试空间(llm_max_retries 次)后仍有限。"""
|
|
106
|
+
return config.llm_timeout * (config.llm_max_retries + 1)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _err_plan(state: dict, error: NodeError) -> Command:
|
|
110
|
+
"""plan LLM 失败 → 回退单一子任务(直接回答目标),继续 execute。"""
|
|
111
|
+
return Command(
|
|
112
|
+
update={
|
|
113
|
+
"plan": [
|
|
114
|
+
{
|
|
115
|
+
"id": "1",
|
|
116
|
+
"desc": "请直接回答:" + state["goal"],
|
|
117
|
+
"status": "pending",
|
|
118
|
+
"result": "",
|
|
119
|
+
}
|
|
120
|
+
],
|
|
121
|
+
"current_idx": 0,
|
|
122
|
+
"findings": [],
|
|
123
|
+
},
|
|
124
|
+
goto="execute",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _err_replan(state: dict, error: NodeError) -> Command:
|
|
129
|
+
"""replan LLM 失败 → 视为可完成(安全收敛),进 final 整合。"""
|
|
130
|
+
return Command(update={"current_action": "", "done": True, "retries": 0}, goto="final")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _err_check(state: dict, error: NodeError) -> Command:
|
|
134
|
+
"""check LLM 失败 → 保守判完成,进 final。"""
|
|
135
|
+
return Command(update={"done": True}, goto="final")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _err_final(state: dict, error: NodeError) -> Command:
|
|
139
|
+
"""final LLM 失败 → 兜底交付,结束。"""
|
|
140
|
+
return Command(update={"final_answer": "任务已执行,但结果整合失败。"}, goto=END)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _err_verify(state: dict, error: NodeError) -> Command:
|
|
144
|
+
"""verify LLM 失败 → 放弃重试、进 check。"""
|
|
145
|
+
return Command(
|
|
146
|
+
update={"should_retry": False, "retries": int(state.get("retries") or 0)},
|
|
147
|
+
goto="check",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ---------------- 构建 ----------------
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def build_agent(
|
|
155
|
+
config: TaskAgentConfig,
|
|
156
|
+
llm_factory: LLMFactory,
|
|
157
|
+
checkpointer_provider: Callable[[], Any | None] = lambda: None,
|
|
158
|
+
executor: Executor | None = None,
|
|
159
|
+
on_event: Callable[[str, dict], None] | None = None,
|
|
160
|
+
memory: TaskMemory | None = None,
|
|
161
|
+
) -> Any:
|
|
162
|
+
"""构建编译后的 LangGraph。
|
|
163
|
+
|
|
164
|
+
- config:模式/容错/步数等运行配置;
|
|
165
|
+
- llm_factory:每次 LLM 调用时调用(返回带 async ainvoke 的对象);
|
|
166
|
+
- checkpointer_provider:返回 LangGraph checkpointer 或 None(无状态/HITL 降级);
|
|
167
|
+
- executor:每步执行器,缺省为纯 LLM 直答(DefaultExecutor)。
|
|
168
|
+
- on_event:可选回调,收到 (kind, data) 事件(plan/replan/execute/check/verify/hitl/final)。
|
|
169
|
+
- memory:可选跨任务记忆(任务开始召回历史结论,结束沉淀 final_answer)。
|
|
170
|
+
"""
|
|
171
|
+
if executor is None:
|
|
172
|
+
executor = DefaultExecutor(llm_factory)
|
|
173
|
+
runtime = Runtime(
|
|
174
|
+
config=config,
|
|
175
|
+
llm_factory=llm_factory,
|
|
176
|
+
executor=executor,
|
|
177
|
+
checkpointer_provider=checkpointer_provider,
|
|
178
|
+
on_event=on_event,
|
|
179
|
+
memory=memory,
|
|
180
|
+
)
|
|
181
|
+
nodes = make_nodes(runtime)
|
|
182
|
+
checkpointer = checkpointer_provider()
|
|
183
|
+
retry = _llm_retry()
|
|
184
|
+
timeout = _llm_timeout(config)
|
|
185
|
+
|
|
186
|
+
if config.mode.lower() == "fixed":
|
|
187
|
+
g = StateGraph(TaskState)
|
|
188
|
+
g.add_node(
|
|
189
|
+
"plan",
|
|
190
|
+
nodes["plan_node"],
|
|
191
|
+
retry_policy=retry,
|
|
192
|
+
timeout=timeout,
|
|
193
|
+
error_handler=_err_plan,
|
|
194
|
+
)
|
|
195
|
+
g.add_node("execute", nodes["execute_node"])
|
|
196
|
+
g.add_node(
|
|
197
|
+
"final",
|
|
198
|
+
nodes["final_node"],
|
|
199
|
+
retry_policy=retry,
|
|
200
|
+
timeout=timeout,
|
|
201
|
+
error_handler=_err_final,
|
|
202
|
+
)
|
|
203
|
+
g.add_edge(START, "plan")
|
|
204
|
+
g.add_edge("plan", "execute")
|
|
205
|
+
g.add_conditional_edges(
|
|
206
|
+
"execute", _route_fixed, {"continue": "execute", "final": "final"}
|
|
207
|
+
)
|
|
208
|
+
g.add_edge("final", END)
|
|
209
|
+
return g.compile(checkpointer=checkpointer)
|
|
210
|
+
|
|
211
|
+
g = StateGraph(TaskState)
|
|
212
|
+
g.add_node(
|
|
213
|
+
"replan",
|
|
214
|
+
nodes["replan_node"],
|
|
215
|
+
retry_policy=retry,
|
|
216
|
+
timeout=timeout,
|
|
217
|
+
error_handler=_err_replan,
|
|
218
|
+
)
|
|
219
|
+
g.add_node("confirm", nodes["human_confirm_node"]) # 节点级 HITL(关闭时透传)
|
|
220
|
+
g.add_node("execute", nodes["execute_action_node"])
|
|
221
|
+
g.add_node(
|
|
222
|
+
"verify",
|
|
223
|
+
nodes["verify_node"],
|
|
224
|
+
retry_policy=retry,
|
|
225
|
+
timeout=timeout,
|
|
226
|
+
error_handler=_err_verify,
|
|
227
|
+
)
|
|
228
|
+
g.add_node(
|
|
229
|
+
"check",
|
|
230
|
+
nodes["check_node"],
|
|
231
|
+
retry_policy=retry,
|
|
232
|
+
timeout=timeout,
|
|
233
|
+
error_handler=_err_check,
|
|
234
|
+
)
|
|
235
|
+
g.add_node(
|
|
236
|
+
"final",
|
|
237
|
+
nodes["final_node"],
|
|
238
|
+
retry_policy=retry,
|
|
239
|
+
timeout=timeout,
|
|
240
|
+
error_handler=_err_final,
|
|
241
|
+
)
|
|
242
|
+
g.add_edge(START, "replan")
|
|
243
|
+
g.add_conditional_edges(
|
|
244
|
+
"replan", _route_after_replan, {"final": "final", "confirm": "confirm"}
|
|
245
|
+
)
|
|
246
|
+
g.add_conditional_edges(
|
|
247
|
+
"confirm", _route_after_confirm, {"execute": "execute", "check": "check"}
|
|
248
|
+
)
|
|
249
|
+
g.add_conditional_edges(
|
|
250
|
+
"execute",
|
|
251
|
+
_route_after_execute(config),
|
|
252
|
+
{"verify": "verify", "check": "check"},
|
|
253
|
+
)
|
|
254
|
+
g.add_conditional_edges(
|
|
255
|
+
"verify", _route_after_verify, {"execute": "execute", "check": "check"}
|
|
256
|
+
)
|
|
257
|
+
g.add_conditional_edges(
|
|
258
|
+
"check", _route_replan, {"final": "final", "replan": "replan"}
|
|
259
|
+
)
|
|
260
|
+
g.add_edge("final", END)
|
|
261
|
+
return g.compile(checkpointer=checkpointer)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
async def list_task_history(graph: Any, session_id: str, limit: int = 30) -> list[dict]:
|
|
265
|
+
"""Time Travel:列出自主任务线程的 checkpoint 历史(新→旧)。
|
|
266
|
+
|
|
267
|
+
需传入带 checkpointer 的编译图;graph 为 None 或无状态图时返回空。
|
|
268
|
+
每条含 checkpoint_id(可 replay / fork)、parent_checkpoint_id、创建时间、
|
|
269
|
+
next(待执行节点)、摘要(final_answer 或最后一条 finding)、是否处于中断等待确认。
|
|
270
|
+
"""
|
|
271
|
+
if graph is None or not session_id:
|
|
272
|
+
return []
|
|
273
|
+
result: list[dict] = []
|
|
274
|
+
try:
|
|
275
|
+
async for snap in graph.aget_state_history(
|
|
276
|
+
{"configurable": {"thread_id": session_id}}
|
|
277
|
+
):
|
|
278
|
+
cfg = (snap.config or {}).get("configurable", {}) or {}
|
|
279
|
+
parent_cfg = (snap.parent_config or {}).get("configurable", {}) or {}
|
|
280
|
+
values = snap.values or {}
|
|
281
|
+
summary = values.get("final_answer") or _last_finding(values.get("findings"))
|
|
282
|
+
created = getattr(snap, "created_at", None)
|
|
283
|
+
iso = (
|
|
284
|
+
created.isoformat()
|
|
285
|
+
if created is not None and hasattr(created, "isoformat")
|
|
286
|
+
else None
|
|
287
|
+
)
|
|
288
|
+
result.append(
|
|
289
|
+
{
|
|
290
|
+
"checkpoint_id": cfg.get("checkpoint_id"),
|
|
291
|
+
"checkpoint_ns": cfg.get("checkpoint_ns") or "",
|
|
292
|
+
"parent_checkpoint_id": parent_cfg.get("checkpoint_id"),
|
|
293
|
+
"created_at": iso,
|
|
294
|
+
"next": list(snap.next) if snap.next else [],
|
|
295
|
+
"summary": (summary or "")[:150],
|
|
296
|
+
"task_count": len(getattr(snap, "tasks", []) or []),
|
|
297
|
+
"interrupted": any(
|
|
298
|
+
getattr(t, "interrupts", None)
|
|
299
|
+
for t in (getattr(snap, "tasks", []) or [])
|
|
300
|
+
),
|
|
301
|
+
}
|
|
302
|
+
)
|
|
303
|
+
if len(result) >= limit:
|
|
304
|
+
break
|
|
305
|
+
except Exception as exc: # noqa: BLE001 - 读取失败安全返回空
|
|
306
|
+
logger.warning("读取自主任务 checkpoint 历史失败: %s", exc)
|
|
307
|
+
return []
|
|
308
|
+
return result
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _last_finding(findings: object) -> str:
|
|
312
|
+
"""取 findings 最后一条作为摘要(供历史列表展示)。"""
|
|
313
|
+
items = findings if isinstance(findings, list) else []
|
|
314
|
+
return items[-1] if items and isinstance(items[-1], str) else ""
|
task_agent/judge.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""任务级质量评估(LLM-judge,自包含)。
|
|
2
|
+
|
|
3
|
+
对一次任务执行的目标达成度 / 信息完整性 / 幻觉打分(0-1),供基准与宿主评估复用。
|
|
4
|
+
解析失败或 LLM 异常时安全返回 0(不中断调用方)。
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from task_agent.llm import LLM, llm_text
|
|
9
|
+
from task_agent.nodes import _fmt_findings, _jump_json
|
|
10
|
+
from task_agent.prompts import EVAL_PROMPT
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _norm(score: object) -> float:
|
|
14
|
+
"""0-5 分归一化到 0-1(非法/缺失 → 0.0)。"""
|
|
15
|
+
try:
|
|
16
|
+
s = float(score) # type: ignore[arg-type] - 运行时校验非法值
|
|
17
|
+
except (TypeError, ValueError):
|
|
18
|
+
return 0.0
|
|
19
|
+
return max(0.0, min(1.0, s / 5.0))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def judge_task(
|
|
23
|
+
llm: LLM,
|
|
24
|
+
goal: str,
|
|
25
|
+
findings: list[str],
|
|
26
|
+
final_answer: str,
|
|
27
|
+
) -> dict:
|
|
28
|
+
"""评估一次任务执行;返回 {goal_attainment, info_completeness, hallucination, comment}。"""
|
|
29
|
+
try:
|
|
30
|
+
text = (
|
|
31
|
+
await llm_text(
|
|
32
|
+
llm,
|
|
33
|
+
EVAL_PROMPT.format(
|
|
34
|
+
goal=goal,
|
|
35
|
+
findings=_fmt_findings(findings) or "(无)",
|
|
36
|
+
final_answer=final_answer or "(无)",
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
).strip()
|
|
40
|
+
data = _jump_json(text)
|
|
41
|
+
except Exception: # noqa: BLE001 - 评估失败不中断基准
|
|
42
|
+
data = {}
|
|
43
|
+
return {
|
|
44
|
+
"goal_attainment": _norm(data.get("goal_attainment")),
|
|
45
|
+
"info_completeness": _norm(data.get("info_completeness")),
|
|
46
|
+
"hallucination": _norm(data.get("hallucination")),
|
|
47
|
+
"comment": str(data.get("comment") or ""),
|
|
48
|
+
}
|
task_agent/llm.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""LLM 最小协议与文本抽取工具(不绑定具体厂商 SDK)。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Callable, Protocol
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LLM(Protocol):
|
|
8
|
+
"""宿主/默认实现只需提供 async ainvoke(prompt) -> 带 .content 的响应。"""
|
|
9
|
+
|
|
10
|
+
async def ainvoke(self, prompt: str) -> Any: ...
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
LLMFactory = Callable[[], LLM]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def llm_text(llm: LLM, prompt: str) -> str:
|
|
17
|
+
"""单次 LLM 调用并抽取文本;失败直接抛异常(由节点级 retry/error_handler 处理)。"""
|
|
18
|
+
resp = await llm.ainvoke(prompt)
|
|
19
|
+
return resp.content if isinstance(resp.content, str) else str(resp.content)
|
task_agent/memory.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""跨任务记忆:任务结论沉淀与召回(零依赖接口 + 内存实现)。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from typing import Protocol
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TaskMemory(Protocol):
|
|
9
|
+
"""跨任务记忆接口:按目标召回历史结论、保存本次任务结论。"""
|
|
10
|
+
|
|
11
|
+
async def recall(self, goal: str) -> list[str]: ...
|
|
12
|
+
|
|
13
|
+
async def remember(self, goal: str, summary: str) -> None: ...
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _tokens(text: str) -> set[str]:
|
|
17
|
+
"""中文按二元组、英文按词切分(用于简单召回匹配)。"""
|
|
18
|
+
out: set[str] = set()
|
|
19
|
+
for m in re.findall(r"[A-Za-z0-9_]+|[\u4e00-\u9fff]+", text or ""):
|
|
20
|
+
if m.isascii():
|
|
21
|
+
if len(m) >= 2:
|
|
22
|
+
out.add(m.lower())
|
|
23
|
+
else:
|
|
24
|
+
for i in range(len(m) - 1):
|
|
25
|
+
out.add(m[i : i + 2])
|
|
26
|
+
return out
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class InMemoryMemory:
|
|
30
|
+
"""进程内实现:按目标关键词重叠召回最近结论(供 demo/测试/单进程宿主)。"""
|
|
31
|
+
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
self._items: list[tuple[str, str]] = []
|
|
34
|
+
|
|
35
|
+
async def recall(self, goal: str) -> list[str]:
|
|
36
|
+
tokens = _tokens(goal)
|
|
37
|
+
hits = [s for g, s in self._items if tokens & _tokens(g)]
|
|
38
|
+
return hits[-3:]
|
|
39
|
+
|
|
40
|
+
async def remember(self, goal: str, summary: str) -> None:
|
|
41
|
+
self._items = [(g, s) for g, s in self._items if g != goal]
|
|
42
|
+
self._items.append((goal, summary))
|
|
43
|
+
|
|
44
|
+
def items(self) -> list[tuple[str, str]]:
|
|
45
|
+
return list(self._items)
|