python-library-ai-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.
- ai_agent/__init__.py +66 -0
- ai_agent/agent.py +122 -0
- ai_agent/app/__init__.py +10 -0
- ai_agent/app/_workspace.py +127 -0
- ai_agent/app/app.py +321 -0
- ai_agent/app/harness_io.py +109 -0
- ai_agent/app/output_format.py +77 -0
- ai_agent/app/packet.py +39 -0
- ai_agent/app/session.py +742 -0
- ai_agent/app/session_store.py +85 -0
- ai_agent/builtin_tools/__init__.py +18 -0
- ai_agent/builtin_tools/current_time.py +39 -0
- ai_agent/builtin_tools/pack.py +20 -0
- ai_agent/builtin_tools/prefix.py +11 -0
- ai_agent/context.py +151 -0
- ai_agent/harness/__init__.py +3 -0
- ai_agent/harness/current_time.py +25 -0
- ai_agent/harness/harness.py +324 -0
- ai_agent/harness/process.py +115 -0
- ai_agent/harness/prompts.py +38 -0
- ai_agent/harness/sandbox.py +139 -0
- ai_agent/json_extract.py +70 -0
- ai_agent/listener.py +172 -0
- ai_agent/llm.py +39 -0
- ai_agent/llm_openai.py +117 -0
- ai_agent/loop.py +124 -0
- ai_agent/mcp_config.py +54 -0
- ai_agent/mcp_loader.py +110 -0
- ai_agent/memory/__init__.py +9 -0
- ai_agent/memory/compression_work.py +71 -0
- ai_agent/memory/compressor.py +339 -0
- ai_agent/memory/config.py +40 -0
- ai_agent/memory/context_builder.py +57 -0
- ai_agent/memory/memory_system.py +561 -0
- ai_agent/memory/models.py +76 -0
- ai_agent/memory/snapshot_merge.py +158 -0
- ai_agent/memory/store.py +107 -0
- ai_agent/memory/worker.py +227 -0
- ai_agent/plan/__init__.py +15 -0
- ai_agent/plan/complete.py +64 -0
- ai_agent/plan/delivery.py +41 -0
- ai_agent/plan/display.py +46 -0
- ai_agent/plan/models.py +44 -0
- ai_agent/plan/parse.py +39 -0
- ai_agent/plan/planner.py +204 -0
- ai_agent/plan/runner.py +281 -0
- ai_agent/react_tool_turn.py +39 -0
- ai_agent/rule/__init__.py +3 -0
- ai_agent/rule/rules.py +36 -0
- ai_agent/skill/__init__.py +5 -0
- ai_agent/skill/builtin_registry.py +56 -0
- ai_agent/skill/catalog.py +104 -0
- ai_agent/skill/frontmatter.py +83 -0
- ai_agent/skill/manager.py +486 -0
- ai_agent/skill/models.py +31 -0
- ai_agent/skill/roots.py +150 -0
- ai_agent/skill/skill_kit.py +80 -0
- ai_agent/skill/tool_declarations.py +68 -0
- ai_agent/tools.py +123 -0
- python_library_ai_agent-0.1.0.dist-info/METADATA +10 -0
- python_library_ai_agent-0.1.0.dist-info/RECORD +62 -0
- python_library_ai_agent-0.1.0.dist-info/WHEEL +4 -0
ai_agent/plan/models.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field, field_validator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PlanStep(BaseModel):
|
|
7
|
+
"""计划中的单步,按顺序串行执行。"""
|
|
8
|
+
|
|
9
|
+
id: str = Field(description="步骤唯一标识,如 step-1")
|
|
10
|
+
title: str = Field(description="短标题")
|
|
11
|
+
objective: str = Field(description="本步须完成的目标")
|
|
12
|
+
optional: bool = Field(default=False, description="执行时可由模型判定跳过")
|
|
13
|
+
hint_tools: list[str] = Field(default_factory=list, description="建议使用的工具名")
|
|
14
|
+
|
|
15
|
+
@field_validator("hint_tools", mode="before")
|
|
16
|
+
@classmethod
|
|
17
|
+
def _hint_tools_none_as_empty(cls, value: object) -> object:
|
|
18
|
+
if value is None:
|
|
19
|
+
return []
|
|
20
|
+
return value
|
|
21
|
+
|
|
22
|
+
required_tool: str | None = Field(
|
|
23
|
+
default=None,
|
|
24
|
+
description="本步应调用的工具名;仅作规划提示,执行阶段不强制",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Plan(BaseModel):
|
|
29
|
+
"""串行多步计划。"""
|
|
30
|
+
|
|
31
|
+
steps: list[PlanStep] = Field(min_length=1, description="按执行顺序排列")
|
|
32
|
+
summary: str | None = Field(default=None, description="规划说明,可选")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PlanRunResult(BaseModel):
|
|
36
|
+
"""一次 run_with_plan 的完整结果。"""
|
|
37
|
+
|
|
38
|
+
plan: Plan
|
|
39
|
+
step_outputs: dict[str, str] = Field(description="步骤 id 到该步完整输出")
|
|
40
|
+
final_output: str = Field(description="返回给用户的最终文本")
|
|
41
|
+
skipped_step_ids: tuple[str, ...] = Field(
|
|
42
|
+
default_factory=tuple,
|
|
43
|
+
description="执行时判定跳过的可选步 id",
|
|
44
|
+
)
|
ai_agent/plan/parse.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ai_agent.json_extract import extract_first_json_object
|
|
4
|
+
from ai_agent.plan.models import Plan, PlanStep
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PlanParseError(ValueError):
|
|
8
|
+
"""规划结果无法解析为合法计划。"""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def parse_plan_text(text: str) -> Plan:
|
|
12
|
+
"""
|
|
13
|
+
从模型文本中解析计划 JSON。
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
text: 模型返回的原始文本
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
校验后的计划对象
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
PlanParseError: JSON 无效或缺少 steps
|
|
23
|
+
"""
|
|
24
|
+
data = extract_first_json_object(text)
|
|
25
|
+
if data is None:
|
|
26
|
+
raise PlanParseError("规划输出不是合法 JSON 对象")
|
|
27
|
+
steps_raw = data.get("steps")
|
|
28
|
+
if not isinstance(steps_raw, list) or not steps_raw:
|
|
29
|
+
raise PlanParseError("steps 须为非空数组")
|
|
30
|
+
steps: list[PlanStep] = []
|
|
31
|
+
for item in steps_raw:
|
|
32
|
+
if not isinstance(item, dict):
|
|
33
|
+
raise PlanParseError("每个 step 须为对象")
|
|
34
|
+
steps.append(PlanStep.model_validate(item))
|
|
35
|
+
summary = data.get("summary")
|
|
36
|
+
return Plan(
|
|
37
|
+
summary=summary if isinstance(summary, str) else None,
|
|
38
|
+
steps=steps,
|
|
39
|
+
)
|
ai_agent/plan/planner.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
|
|
5
|
+
from ai_agent.context import ChatMessage, RunPhase, RunPhaseKind
|
|
6
|
+
from ai_agent.harness.prompts import PLANNING_SYSTEM_PROMPT
|
|
7
|
+
from ai_agent.listener import AgentListener
|
|
8
|
+
from ai_agent.llm import LLMClient
|
|
9
|
+
from ai_agent.plan.complete import complete_text
|
|
10
|
+
from ai_agent.plan.models import Plan
|
|
11
|
+
from ai_agent.plan.parse import PlanParseError, parse_plan_text
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PlanPlanner:
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
规划入口:用与执行相同的语言模型产出串行计划,不调用工具。
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
规划与执行共用 ``LLMClient``,由 ``PlanRunner`` 或测试注入。
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def __init__(self, llm: LLMClient) -> None:
|
|
31
|
+
|
|
32
|
+
self._llm = llm
|
|
33
|
+
|
|
34
|
+
self._planning_system_prompt = PLANNING_SYSTEM_PROMPT.strip()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def plan(
|
|
39
|
+
self,
|
|
40
|
+
*,
|
|
41
|
+
user_message: str,
|
|
42
|
+
business_system_prompt: str,
|
|
43
|
+
messages: list[ChatMessage],
|
|
44
|
+
extra_planning_context: str = "",
|
|
45
|
+
listeners: Sequence[AgentListener] | None = None,
|
|
46
|
+
) -> Plan:
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
根据用户请求与对话历史生成计划。
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
|
|
56
|
+
user_message: 本轮用户输入(已可能在 messages 末尾)
|
|
57
|
+
|
|
58
|
+
business_system_prompt: 业务系统提示(含规则与技能等自然语言说明)
|
|
59
|
+
|
|
60
|
+
messages: 送入规划的历史(通常含本轮用户消息)
|
|
61
|
+
|
|
62
|
+
extra_planning_context: 附加说明(如附件摘要)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
|
|
68
|
+
解析后的串行计划
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
|
|
74
|
+
PlanParseError: 模型输出无法解析且重试后仍失败
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
system = _build_planning_system(
|
|
79
|
+
|
|
80
|
+
self._planning_system_prompt,
|
|
81
|
+
|
|
82
|
+
business_system_prompt,
|
|
83
|
+
|
|
84
|
+
extra_planning_context,
|
|
85
|
+
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
history = _history_without_last_user(messages, user_message)
|
|
89
|
+
|
|
90
|
+
user_block = _format_planning_user(user_message, business_system_prompt)
|
|
91
|
+
|
|
92
|
+
last_error: PlanParseError | None = None
|
|
93
|
+
|
|
94
|
+
for attempt in range(2):
|
|
95
|
+
|
|
96
|
+
text = await complete_text(
|
|
97
|
+
self._llm,
|
|
98
|
+
system_prompt=system,
|
|
99
|
+
user_content=user_block
|
|
100
|
+
if attempt == 0
|
|
101
|
+
else f"{user_block}\n\n上次输出无法解析,请仅输出合法 JSON 对象。",
|
|
102
|
+
history=history,
|
|
103
|
+
listeners=listeners,
|
|
104
|
+
phase=RunPhase(kind=RunPhaseKind.PLANNING),
|
|
105
|
+
parse_from_answer_text_only=True,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
|
|
110
|
+
return parse_plan_text(text)
|
|
111
|
+
|
|
112
|
+
except PlanParseError as exc:
|
|
113
|
+
|
|
114
|
+
last_error = exc
|
|
115
|
+
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
assert last_error is not None
|
|
119
|
+
|
|
120
|
+
raise last_error
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _history_without_last_user(
|
|
127
|
+
|
|
128
|
+
messages: list[ChatMessage],
|
|
129
|
+
|
|
130
|
+
user_message: str,
|
|
131
|
+
|
|
132
|
+
) -> list[ChatMessage]:
|
|
133
|
+
|
|
134
|
+
if not messages:
|
|
135
|
+
|
|
136
|
+
return []
|
|
137
|
+
|
|
138
|
+
last = messages[-1]
|
|
139
|
+
|
|
140
|
+
if last.role == "user" and last.content.strip() == user_message.strip():
|
|
141
|
+
|
|
142
|
+
return list(messages[:-1])
|
|
143
|
+
|
|
144
|
+
return list(messages)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _format_planning_user(user_message: str, business_system_prompt: str) -> str:
|
|
151
|
+
|
|
152
|
+
lines = [
|
|
153
|
+
|
|
154
|
+
"## 用户请求",
|
|
155
|
+
|
|
156
|
+
user_message.strip(),
|
|
157
|
+
|
|
158
|
+
"",
|
|
159
|
+
|
|
160
|
+
"## 业务系统提示(供规划参考,含规则与技能)",
|
|
161
|
+
|
|
162
|
+
business_system_prompt.strip() or "(无)",
|
|
163
|
+
|
|
164
|
+
"",
|
|
165
|
+
|
|
166
|
+
"请结合上文规则与技能说明分解步骤,输出 JSON 计划。",
|
|
167
|
+
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
return "\n".join(lines)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _build_planning_system(
|
|
177
|
+
|
|
178
|
+
planning_base: str,
|
|
179
|
+
|
|
180
|
+
business_system_prompt: str,
|
|
181
|
+
|
|
182
|
+
extra_planning_context: str,
|
|
183
|
+
|
|
184
|
+
) -> str:
|
|
185
|
+
|
|
186
|
+
parts = [planning_base]
|
|
187
|
+
|
|
188
|
+
if business_system_prompt.strip():
|
|
189
|
+
|
|
190
|
+
parts.append("")
|
|
191
|
+
|
|
192
|
+
parts.append("## 业务背景")
|
|
193
|
+
|
|
194
|
+
parts.append(business_system_prompt.strip())
|
|
195
|
+
|
|
196
|
+
if extra_planning_context.strip():
|
|
197
|
+
|
|
198
|
+
parts.append("")
|
|
199
|
+
|
|
200
|
+
parts.append(extra_planning_context.strip())
|
|
201
|
+
|
|
202
|
+
return "\n\n".join(parts)
|
|
203
|
+
|
|
204
|
+
|
ai_agent/plan/runner.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
|
|
7
|
+
from ai_agent.app.output_format import (
|
|
8
|
+
run_output_instruction,
|
|
9
|
+
step_intermediate_instruction,
|
|
10
|
+
)
|
|
11
|
+
from ai_agent.context import ChatMessage, RunPhase, RunPhaseKind
|
|
12
|
+
from ai_agent.listener import (
|
|
13
|
+
AgentListener,
|
|
14
|
+
notify_plan_ready,
|
|
15
|
+
notify_plan_start,
|
|
16
|
+
notify_plan_step_end,
|
|
17
|
+
notify_plan_step_start,
|
|
18
|
+
)
|
|
19
|
+
from ai_agent.llm import LLMClient
|
|
20
|
+
from ai_agent.plan.models import Plan, PlanRunResult, PlanStep
|
|
21
|
+
from ai_agent.plan.planner import PlanPlanner
|
|
22
|
+
from ai_agent.plan.delivery import (
|
|
23
|
+
delivery_skill_refs_for_step,
|
|
24
|
+
plan_delivery_preload_note,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from ai_agent.app.session import AgentSession
|
|
29
|
+
|
|
30
|
+
SKIP_STEP_MARKER = "SKIP_STEP"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PlanStepFailedError(RuntimeError):
|
|
34
|
+
"""某必做步骤未产生有效输出时抛出。"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PlanRunner:
|
|
38
|
+
"""
|
|
39
|
+
编排规划与逐步执行:先 ``PlanPlanner``,再对每步调用会话上的 ReAct。
|
|
40
|
+
|
|
41
|
+
步间上下文携带已完成步骤的完整 output;可选步由执行模型自行判定是否跳过。
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
llm: LLMClient,
|
|
47
|
+
*,
|
|
48
|
+
listeners: Sequence[AgentListener] | None = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
self._planner = PlanPlanner(llm)
|
|
51
|
+
self._listeners: list[AgentListener] = list(listeners or [])
|
|
52
|
+
|
|
53
|
+
async def run(
|
|
54
|
+
self,
|
|
55
|
+
session: AgentSession,
|
|
56
|
+
*,
|
|
57
|
+
user_message: str,
|
|
58
|
+
speaker: str = "user",
|
|
59
|
+
extra_planning_context: str = "",
|
|
60
|
+
) -> PlanRunResult:
|
|
61
|
+
"""
|
|
62
|
+
规划并串行执行各步,返回最终回答。
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
session: 已装配 Harness 与工具的会话
|
|
66
|
+
user_message: 用户输入
|
|
67
|
+
speaker: 多用户场景下的讲述者名
|
|
68
|
+
extra_planning_context: 拼入规划阶段的附加说明
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
计划、各步完整输出与最终文本
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
PlanStepFailedError: 必做步骤无有效输出
|
|
75
|
+
"""
|
|
76
|
+
full_system = session.build_system_prompt()
|
|
77
|
+
planning_messages, merged_system = session._prepare_plan_run(
|
|
78
|
+
user_message=user_message,
|
|
79
|
+
system_prompt=full_system,
|
|
80
|
+
speaker=speaker,
|
|
81
|
+
)
|
|
82
|
+
await notify_plan_start(self._listeners)
|
|
83
|
+
plan = await self._planner.plan(
|
|
84
|
+
user_message=user_message,
|
|
85
|
+
business_system_prompt=merged_system,
|
|
86
|
+
messages=planning_messages,
|
|
87
|
+
extra_planning_context=extra_planning_context,
|
|
88
|
+
listeners=self._listeners,
|
|
89
|
+
)
|
|
90
|
+
await notify_plan_ready(self._listeners, plan)
|
|
91
|
+
skill_manager = session.skill_manager
|
|
92
|
+
if skill_manager is not None:
|
|
93
|
+
skill_manager.begin_plan()
|
|
94
|
+
try:
|
|
95
|
+
step_outputs: dict[str, str] = {}
|
|
96
|
+
skipped: list[str] = []
|
|
97
|
+
for index, step in enumerate(plan.steps):
|
|
98
|
+
await notify_plan_step_start(
|
|
99
|
+
self._listeners,
|
|
100
|
+
step_index=index,
|
|
101
|
+
step=step,
|
|
102
|
+
plan=plan,
|
|
103
|
+
)
|
|
104
|
+
output = await self._run_step(
|
|
105
|
+
session,
|
|
106
|
+
base_system=full_system,
|
|
107
|
+
plan=plan,
|
|
108
|
+
step=step,
|
|
109
|
+
step_index=index,
|
|
110
|
+
user_message=user_message,
|
|
111
|
+
prior_outputs=step_outputs,
|
|
112
|
+
)
|
|
113
|
+
skipped_step = step.optional and _is_skip_output(output)
|
|
114
|
+
if skipped_step:
|
|
115
|
+
skipped.append(step.id)
|
|
116
|
+
elif not output.strip() and not step.optional:
|
|
117
|
+
raise PlanStepFailedError(
|
|
118
|
+
f"步骤 {step.id}({step.title})未产生输出"
|
|
119
|
+
)
|
|
120
|
+
else:
|
|
121
|
+
step_outputs[step.id] = output
|
|
122
|
+
await notify_plan_step_end(
|
|
123
|
+
self._listeners,
|
|
124
|
+
step_index=index,
|
|
125
|
+
step=step,
|
|
126
|
+
plan=plan,
|
|
127
|
+
output=output,
|
|
128
|
+
skipped=skipped_step,
|
|
129
|
+
)
|
|
130
|
+
if skipped_step:
|
|
131
|
+
continue
|
|
132
|
+
final = _pick_final_output(plan, step_outputs, skipped)
|
|
133
|
+
session._finish_plan_run(
|
|
134
|
+
user_message=user_message,
|
|
135
|
+
final_output=final,
|
|
136
|
+
speaker=speaker,
|
|
137
|
+
)
|
|
138
|
+
return PlanRunResult(
|
|
139
|
+
plan=plan,
|
|
140
|
+
step_outputs=step_outputs,
|
|
141
|
+
final_output=final,
|
|
142
|
+
skipped_step_ids=tuple(skipped),
|
|
143
|
+
)
|
|
144
|
+
finally:
|
|
145
|
+
if skill_manager is not None:
|
|
146
|
+
skill_manager.end_plan()
|
|
147
|
+
|
|
148
|
+
async def _run_step(
|
|
149
|
+
self,
|
|
150
|
+
session: AgentSession,
|
|
151
|
+
*,
|
|
152
|
+
base_system: str,
|
|
153
|
+
plan: Plan,
|
|
154
|
+
step: PlanStep,
|
|
155
|
+
step_index: int,
|
|
156
|
+
user_message: str,
|
|
157
|
+
prior_outputs: dict[str, str],
|
|
158
|
+
) -> str:
|
|
159
|
+
is_last_step = step_index + 1 >= len(plan.steps)
|
|
160
|
+
delivery_refs: tuple[str, ...] = ()
|
|
161
|
+
preloaded_delivery = False
|
|
162
|
+
skill_manager = session.skill_manager
|
|
163
|
+
if skill_manager is not None:
|
|
164
|
+
if is_last_step:
|
|
165
|
+
delivery_refs = delivery_skill_refs_for_step(step)
|
|
166
|
+
skill_manager.set_plan_delivery_skills(delivery_refs)
|
|
167
|
+
preloaded_delivery = bool(delivery_refs)
|
|
168
|
+
else:
|
|
169
|
+
skill_manager.set_plan_delivery_skills(())
|
|
170
|
+
step_system = _build_step_system(
|
|
171
|
+
base_system,
|
|
172
|
+
plan=plan,
|
|
173
|
+
step=step,
|
|
174
|
+
step_index=step_index,
|
|
175
|
+
prior_outputs=prior_outputs,
|
|
176
|
+
is_last_step=is_last_step,
|
|
177
|
+
preloaded_delivery=preloaded_delivery,
|
|
178
|
+
delivery_skill_refs=delivery_refs,
|
|
179
|
+
)
|
|
180
|
+
step_user = _build_step_user(
|
|
181
|
+
user_message=user_message,
|
|
182
|
+
step=step,
|
|
183
|
+
step_index=step_index,
|
|
184
|
+
total=len(plan.steps),
|
|
185
|
+
is_last_step=is_last_step,
|
|
186
|
+
preloaded_delivery=preloaded_delivery,
|
|
187
|
+
)
|
|
188
|
+
return await session.run_with_messages(
|
|
189
|
+
messages=[ChatMessage(role="user", content=step_user)],
|
|
190
|
+
system_prompt=step_system,
|
|
191
|
+
phase=RunPhase(
|
|
192
|
+
kind=RunPhaseKind.STEP,
|
|
193
|
+
step_index=step_index,
|
|
194
|
+
step_id=step.id,
|
|
195
|
+
),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _is_skip_output(text: str) -> bool:
|
|
200
|
+
stripped = text.strip()
|
|
201
|
+
return stripped == SKIP_STEP_MARKER or stripped.startswith(f"{SKIP_STEP_MARKER}\n")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _pick_final_output(
|
|
205
|
+
plan: Plan,
|
|
206
|
+
step_outputs: dict[str, str],
|
|
207
|
+
skipped: list[str],
|
|
208
|
+
) -> str:
|
|
209
|
+
skipped_set = set(skipped)
|
|
210
|
+
for step in reversed(plan.steps):
|
|
211
|
+
if step.id in skipped_set:
|
|
212
|
+
continue
|
|
213
|
+
out = step_outputs.get(step.id, "").strip()
|
|
214
|
+
if out:
|
|
215
|
+
return out
|
|
216
|
+
return ""
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _build_step_system(
|
|
220
|
+
base_system: str,
|
|
221
|
+
*,
|
|
222
|
+
plan: Plan,
|
|
223
|
+
step: PlanStep,
|
|
224
|
+
step_index: int,
|
|
225
|
+
prior_outputs: dict[str, str],
|
|
226
|
+
is_last_step: bool,
|
|
227
|
+
preloaded_delivery: bool = False,
|
|
228
|
+
delivery_skill_refs: tuple[str, ...] = (),
|
|
229
|
+
) -> str:
|
|
230
|
+
lines = [base_system.rstrip(), "", "## 当前计划摘要"]
|
|
231
|
+
if plan.summary:
|
|
232
|
+
lines.append(plan.summary.strip())
|
|
233
|
+
lines.append(f"共 {len(plan.steps)} 步,当前为第 {step_index + 1} 步。")
|
|
234
|
+
if prior_outputs:
|
|
235
|
+
lines.append("")
|
|
236
|
+
lines.append("## 已完成步骤的完整输出")
|
|
237
|
+
for sid, out in prior_outputs.items():
|
|
238
|
+
lines.append(f"### {sid}")
|
|
239
|
+
lines.append(out)
|
|
240
|
+
lines.append("")
|
|
241
|
+
lines.append("## 本步任务")
|
|
242
|
+
lines.append(f"**{step.title}**")
|
|
243
|
+
lines.append(step.objective.strip())
|
|
244
|
+
if step.hint_tools:
|
|
245
|
+
lines.append(f"建议工具:{', '.join(step.hint_tools)}")
|
|
246
|
+
if step.required_tool and not preloaded_delivery:
|
|
247
|
+
lines.append(f"若需要请使用工具:{step.required_tool}")
|
|
248
|
+
if preloaded_delivery and delivery_skill_refs:
|
|
249
|
+
joined = ", ".join(delivery_skill_refs)
|
|
250
|
+
lines.append(f"终稿改写技能已预载:{joined};勿再调用 enable_skill。")
|
|
251
|
+
if step.optional:
|
|
252
|
+
lines.append(
|
|
253
|
+
f"本步为可选。若前述输出已足以回答用户,请仅回复 {SKIP_STEP_MARKER},"
|
|
254
|
+
"不要调用工具。"
|
|
255
|
+
)
|
|
256
|
+
if not is_last_step:
|
|
257
|
+
lines.append("本步为中间步骤:勿按最终交付 JSON 格式回复用户。")
|
|
258
|
+
return "\n".join(lines)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _build_step_user(
|
|
262
|
+
*,
|
|
263
|
+
user_message: str,
|
|
264
|
+
step: PlanStep,
|
|
265
|
+
step_index: int,
|
|
266
|
+
total: int,
|
|
267
|
+
is_last_step: bool,
|
|
268
|
+
preloaded_delivery: bool = False,
|
|
269
|
+
) -> str:
|
|
270
|
+
parts = [
|
|
271
|
+
f"用户原始请求:{user_message.strip()}",
|
|
272
|
+
"",
|
|
273
|
+
f"请完成计划第 {step_index + 1}/{total} 步:{step.title}",
|
|
274
|
+
]
|
|
275
|
+
if is_last_step:
|
|
276
|
+
parts.extend(["", run_output_instruction()])
|
|
277
|
+
if preloaded_delivery:
|
|
278
|
+
parts.extend(["", plan_delivery_preload_note()])
|
|
279
|
+
else:
|
|
280
|
+
parts.extend(["", step_intermediate_instruction()])
|
|
281
|
+
return "\n\n".join(parts)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ai_agent.builtin_tools.current_time import (
|
|
4
|
+
CURRENT_TIME_SHORT_NAME,
|
|
5
|
+
harness_current_time_tool_name,
|
|
6
|
+
)
|
|
7
|
+
from ai_agent.builtin_tools.prefix import builtin_tool_name
|
|
8
|
+
from ai_agent.context import ToolInvocation
|
|
9
|
+
|
|
10
|
+
_BUILTIN_CURRENT_TIME = builtin_tool_name(CURRENT_TIME_SHORT_NAME)
|
|
11
|
+
_HARNESS_CURRENT_TIME = harness_current_time_tool_name()
|
|
12
|
+
|
|
13
|
+
_SAME_TURN_DEFERRED_TOOL_REPLY = (
|
|
14
|
+
"未执行:本回合已与取时工具同时发起。请先阅读同回合取时工具的返回,"
|
|
15
|
+
"再在下一回合单独调用本工具并据取时结果重写参数(勿复用本回合已生成的 query)。"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_current_time_tool(tool_name: str) -> bool:
|
|
20
|
+
"""是否为应用或 Harness 的取时工具对外名。"""
|
|
21
|
+
return tool_name in (_BUILTIN_CURRENT_TIME, _HARNESS_CURRENT_TIME)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def batch_includes_current_time(batch: list[ToolInvocation]) -> bool:
|
|
25
|
+
return any(is_current_time_tool(inv.tool_name) for inv in batch)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def should_defer_tool_in_batch(inv: ToolInvocation, batch: list[ToolInvocation]) -> bool:
|
|
29
|
+
"""
|
|
30
|
+
同回合若同时请求取时与其它工具,仅执行取时;其它工具返回说明性结果供下回合重试。
|
|
31
|
+
"""
|
|
32
|
+
if is_current_time_tool(inv.tool_name):
|
|
33
|
+
return False
|
|
34
|
+
return batch_includes_current_time(batch)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def deferred_tool_reply() -> str:
|
|
38
|
+
"""推迟执行时写入 ``ToolInvocation.answer`` 的固定说明。"""
|
|
39
|
+
return _SAME_TURN_DEFERRED_TOOL_REPLY
|
ai_agent/rule/rules.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RuleSet:
|
|
8
|
+
"""
|
|
9
|
+
会话级规则:由调用方指定文本文件路径,启动时读入并拼成系统提示。
|
|
10
|
+
|
|
11
|
+
与 skill 不同,规则不注册工具,每轮运行固定注入模型上下文。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, paths: Sequence[Path | str] | None = None) -> None:
|
|
15
|
+
self._segments: list[tuple[Path, str]] = []
|
|
16
|
+
for raw in paths or ():
|
|
17
|
+
path = Path(raw).expanduser().resolve()
|
|
18
|
+
if not path.is_file():
|
|
19
|
+
raise ValueError(f"rule 文件不存在或不是文件: {path}")
|
|
20
|
+
text = path.read_text(encoding="utf-8").strip()
|
|
21
|
+
self._segments.append((path, text))
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def paths(self) -> tuple[Path, ...]:
|
|
25
|
+
"""已加载的规则文件绝对路径。"""
|
|
26
|
+
return tuple(path for path, _ in self._segments)
|
|
27
|
+
|
|
28
|
+
def build_system_prompt(self) -> str:
|
|
29
|
+
"""
|
|
30
|
+
将各规则文件正文按顺序拼接为系统提示。
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
无规则或文件均为空时返回空字符串
|
|
34
|
+
"""
|
|
35
|
+
parts = [text for _, text in self._segments if text]
|
|
36
|
+
return "\n\n".join(parts)
|