loom-agent 0.0.1__py3-none-any.whl → 0.0.3__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.
Potentially problematic release.
This version of loom-agent might be problematic. Click here for more details.
- loom/builtin/tools/calculator.py +4 -0
- loom/builtin/tools/document_search.py +5 -0
- loom/builtin/tools/glob.py +4 -0
- loom/builtin/tools/grep.py +4 -0
- loom/builtin/tools/http_request.py +5 -0
- loom/builtin/tools/python_repl.py +5 -0
- loom/builtin/tools/read_file.py +4 -0
- loom/builtin/tools/task.py +105 -0
- loom/builtin/tools/web_search.py +4 -0
- loom/builtin/tools/write_file.py +4 -0
- loom/components/agent.py +121 -5
- loom/core/agent_executor.py +777 -321
- loom/core/compression_manager.py +17 -10
- loom/core/context_assembly.py +437 -0
- loom/core/events.py +660 -0
- loom/core/execution_context.py +119 -0
- loom/core/tool_orchestrator.py +383 -0
- loom/core/turn_state.py +188 -0
- loom/core/types.py +15 -4
- loom/core/unified_coordination.py +389 -0
- loom/interfaces/event_producer.py +172 -0
- loom/interfaces/tool.py +22 -1
- loom/security/__init__.py +13 -0
- loom/security/models.py +85 -0
- loom/security/path_validator.py +128 -0
- loom/security/validator.py +346 -0
- loom/tasks/PHASE_1_FOUNDATION/task_1.1_agent_events.md +121 -0
- loom/tasks/PHASE_1_FOUNDATION/task_1.2_streaming_api.md +521 -0
- loom/tasks/PHASE_1_FOUNDATION/task_1.3_context_assembler.md +606 -0
- loom/tasks/PHASE_2_CORE_FEATURES/task_2.1_tool_orchestrator.md +743 -0
- loom/tasks/PHASE_2_CORE_FEATURES/task_2.2_security_validator.md +676 -0
- loom/tasks/README.md +109 -0
- loom/tasks/__init__.py +11 -0
- loom/tasks/sql_placeholder.py +100 -0
- loom_agent-0.0.3.dist-info/METADATA +292 -0
- {loom_agent-0.0.1.dist-info → loom_agent-0.0.3.dist-info}/RECORD +38 -19
- loom_agent-0.0.1.dist-info/METADATA +0 -457
- {loom_agent-0.0.1.dist-info → loom_agent-0.0.3.dist-info}/WHEEL +0 -0
- {loom_agent-0.0.1.dist-info → loom_agent-0.0.3.dist-info}/licenses/LICENSE +0 -0
loom/core/agent_executor.py
CHANGED
|
@@ -1,23 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent Executor with tt (Tail-Recursive) Control Loop
|
|
3
|
+
|
|
4
|
+
Core execution engine implementing recursive conversation management,
|
|
5
|
+
inspired by Claude Code's tt function design.
|
|
6
|
+
"""
|
|
7
|
+
|
|
1
8
|
from __future__ import annotations
|
|
2
9
|
|
|
3
10
|
import asyncio
|
|
4
|
-
|
|
11
|
+
import json
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import AsyncGenerator, Dict, List, Optional, Any
|
|
5
15
|
from uuid import uuid4
|
|
6
16
|
|
|
17
|
+
from loom.callbacks.base import BaseCallback
|
|
18
|
+
from loom.callbacks.metrics import MetricsCollector
|
|
19
|
+
from loom.core.context_assembly import ComponentPriority, ContextAssembler
|
|
20
|
+
from loom.core.events import AgentEvent, AgentEventType, ToolResult
|
|
21
|
+
from loom.core.execution_context import ExecutionContext
|
|
22
|
+
from loom.core.permissions import PermissionManager
|
|
7
23
|
from loom.core.steering_control import SteeringControl
|
|
24
|
+
from loom.core.tool_orchestrator import ToolOrchestrator
|
|
8
25
|
from loom.core.tool_pipeline import ToolExecutionPipeline
|
|
9
|
-
from loom.core.
|
|
10
|
-
from loom.core.
|
|
26
|
+
from loom.core.turn_state import TurnState
|
|
27
|
+
from loom.core.types import Message, ToolCall
|
|
11
28
|
from loom.interfaces.compressor import BaseCompressor
|
|
12
29
|
from loom.interfaces.llm import BaseLLM
|
|
13
30
|
from loom.interfaces.memory import BaseMemory
|
|
14
31
|
from loom.interfaces.tool import BaseTool
|
|
15
|
-
from loom.core.permissions import PermissionManager
|
|
16
|
-
from loom.callbacks.metrics import MetricsCollector
|
|
17
|
-
import time
|
|
18
32
|
from loom.utils.token_counter import count_messages_tokens
|
|
19
|
-
from loom.callbacks.base import BaseCallback
|
|
20
|
-
from loom.core.errors import ExecutionAbortedError
|
|
21
33
|
|
|
22
34
|
# RAG support
|
|
23
35
|
try:
|
|
@@ -25,9 +37,77 @@ try:
|
|
|
25
37
|
except ImportError:
|
|
26
38
|
ContextRetriever = None # type: ignore
|
|
27
39
|
|
|
40
|
+
# Unified coordination support
|
|
41
|
+
try:
|
|
42
|
+
from loom.core.unified_coordination import UnifiedExecutionContext, IntelligentCoordinator
|
|
43
|
+
except ImportError:
|
|
44
|
+
UnifiedExecutionContext = None # type: ignore
|
|
45
|
+
IntelligentCoordinator = None # type: ignore
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TaskHandler:
|
|
49
|
+
"""
|
|
50
|
+
任务处理器基类
|
|
51
|
+
|
|
52
|
+
开发者可以继承此类来实现自定义的任务处理逻辑
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def can_handle(self, task: str) -> bool:
|
|
56
|
+
"""
|
|
57
|
+
判断是否能处理给定的任务
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
task: 任务描述
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
bool: 是否能处理此任务
|
|
64
|
+
"""
|
|
65
|
+
raise NotImplementedError
|
|
66
|
+
|
|
67
|
+
def generate_guidance(
|
|
68
|
+
self,
|
|
69
|
+
original_task: str,
|
|
70
|
+
result_analysis: Dict[str, Any],
|
|
71
|
+
recursion_depth: int
|
|
72
|
+
) -> str:
|
|
73
|
+
"""
|
|
74
|
+
生成递归指导消息
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
original_task: 原始任务
|
|
78
|
+
result_analysis: 工具结果分析
|
|
79
|
+
recursion_depth: 递归深度
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
str: 生成的指导消息
|
|
83
|
+
"""
|
|
84
|
+
raise NotImplementedError
|
|
85
|
+
|
|
28
86
|
|
|
29
87
|
class AgentExecutor:
|
|
30
|
-
"""
|
|
88
|
+
"""
|
|
89
|
+
Agent Executor with tt Recursive Control Loop.
|
|
90
|
+
|
|
91
|
+
Core Design:
|
|
92
|
+
- tt() is the only execution method (tail-recursive)
|
|
93
|
+
- All other methods are thin wrappers around tt()
|
|
94
|
+
- No iteration loops - only recursion
|
|
95
|
+
- Immutable state (TurnState)
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
```python
|
|
99
|
+
executor = AgentExecutor(llm=llm, tools=tools)
|
|
100
|
+
|
|
101
|
+
# Initialize state
|
|
102
|
+
turn_state = TurnState.initial(max_iterations=10)
|
|
103
|
+
context = ExecutionContext.create()
|
|
104
|
+
messages = [Message(role="user", content="Hello")]
|
|
105
|
+
|
|
106
|
+
# Execute with tt recursion
|
|
107
|
+
async for event in executor.tt(messages, turn_state, context):
|
|
108
|
+
print(event)
|
|
109
|
+
```
|
|
110
|
+
"""
|
|
31
111
|
|
|
32
112
|
def __init__(
|
|
33
113
|
self,
|
|
@@ -35,7 +115,7 @@ class AgentExecutor:
|
|
|
35
115
|
tools: Dict[str, BaseTool] | None = None,
|
|
36
116
|
memory: BaseMemory | None = None,
|
|
37
117
|
compressor: BaseCompressor | None = None,
|
|
38
|
-
context_retriever: Optional["ContextRetriever"] = None,
|
|
118
|
+
context_retriever: Optional["ContextRetriever"] = None,
|
|
39
119
|
steering_control: SteeringControl | None = None,
|
|
40
120
|
max_iterations: int = 50,
|
|
41
121
|
max_context_tokens: int = 16000,
|
|
@@ -43,325 +123,632 @@ class AgentExecutor:
|
|
|
43
123
|
metrics: MetricsCollector | None = None,
|
|
44
124
|
system_instructions: Optional[str] = None,
|
|
45
125
|
callbacks: Optional[List[BaseCallback]] = None,
|
|
46
|
-
enable_steering: bool = False,
|
|
126
|
+
enable_steering: bool = False,
|
|
127
|
+
task_handlers: Optional[List[TaskHandler]] = None,
|
|
128
|
+
unified_context: Optional["UnifiedExecutionContext"] = None,
|
|
129
|
+
enable_unified_coordination: bool = True,
|
|
47
130
|
) -> None:
|
|
48
131
|
self.llm = llm
|
|
49
132
|
self.tools = tools or {}
|
|
50
133
|
self.memory = memory
|
|
51
134
|
self.compressor = compressor
|
|
52
|
-
self.context_retriever = context_retriever
|
|
135
|
+
self.context_retriever = context_retriever
|
|
53
136
|
self.steering_control = steering_control or SteeringControl()
|
|
54
137
|
self.max_iterations = max_iterations
|
|
55
138
|
self.max_context_tokens = max_context_tokens
|
|
56
139
|
self.metrics = metrics or MetricsCollector()
|
|
57
|
-
self.permission_manager = permission_manager or PermissionManager(
|
|
140
|
+
self.permission_manager = permission_manager or PermissionManager(
|
|
141
|
+
policy={"default": "allow"}
|
|
142
|
+
)
|
|
58
143
|
self.system_instructions = system_instructions
|
|
59
144
|
self.callbacks = callbacks or []
|
|
60
|
-
self.enable_steering = enable_steering
|
|
145
|
+
self.enable_steering = enable_steering
|
|
146
|
+
self.task_handlers = task_handlers or []
|
|
147
|
+
|
|
148
|
+
# Unified coordination
|
|
149
|
+
self.unified_context = unified_context
|
|
150
|
+
self.enable_unified_coordination = enable_unified_coordination
|
|
151
|
+
|
|
152
|
+
# Initialize unified coordination if enabled
|
|
153
|
+
if self.enable_unified_coordination and UnifiedExecutionContext and IntelligentCoordinator:
|
|
154
|
+
self._setup_unified_coordination()
|
|
155
|
+
|
|
156
|
+
# Tool execution (legacy pipeline for backward compatibility)
|
|
61
157
|
self.tool_pipeline = ToolExecutionPipeline(
|
|
62
|
-
self.tools,
|
|
158
|
+
self.tools,
|
|
159
|
+
permission_manager=self.permission_manager,
|
|
160
|
+
metrics=self.metrics,
|
|
63
161
|
)
|
|
64
162
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
163
|
+
def _setup_unified_coordination(self):
|
|
164
|
+
"""设置统一协调机制"""
|
|
165
|
+
if not self.unified_context:
|
|
166
|
+
# 创建默认的统一执行上下文
|
|
167
|
+
from loom.core.unified_coordination import CoordinationConfig
|
|
168
|
+
self.unified_context = UnifiedExecutionContext(
|
|
169
|
+
execution_id=f"exec_{int(time.time())}",
|
|
170
|
+
config=CoordinationConfig() # 使用默认配置
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# 集成四大核心能力
|
|
174
|
+
self._integrate_core_capabilities()
|
|
175
|
+
|
|
176
|
+
# 创建智能协调器
|
|
177
|
+
self.coordinator = IntelligentCoordinator(self.unified_context)
|
|
178
|
+
|
|
179
|
+
# 设置跨组件引用
|
|
180
|
+
self._setup_cross_component_references()
|
|
181
|
+
|
|
182
|
+
def _integrate_core_capabilities(self):
|
|
183
|
+
"""集成四大核心能力到统一上下文"""
|
|
184
|
+
|
|
185
|
+
config = self.unified_context.config
|
|
186
|
+
|
|
187
|
+
# 1. 集成 ContextAssembler
|
|
188
|
+
if not self.unified_context.context_assembler:
|
|
189
|
+
from loom.core.context_assembly import ContextAssembler
|
|
190
|
+
self.unified_context.context_assembler = ContextAssembler(
|
|
191
|
+
max_tokens=self.max_context_tokens,
|
|
192
|
+
enable_caching=True,
|
|
193
|
+
cache_size=config.context_cache_size
|
|
194
|
+
)
|
|
77
195
|
|
|
78
|
-
|
|
196
|
+
# 2. 集成 TaskTool
|
|
197
|
+
if "task" in self.tools and not self.unified_context.task_tool:
|
|
198
|
+
task_tool = self.tools["task"]
|
|
199
|
+
# 使用配置更新 TaskTool
|
|
200
|
+
task_tool.pool_size = config.subagent_pool_size
|
|
201
|
+
task_tool.enable_pooling = True
|
|
202
|
+
self.unified_context.task_tool = task_tool
|
|
203
|
+
|
|
204
|
+
# 3. 集成 EventProcessor
|
|
205
|
+
if not self.unified_context.event_processor:
|
|
206
|
+
from loom.core.events import EventFilter, EventProcessor, AgentEventType
|
|
207
|
+
|
|
208
|
+
# 创建智能事件过滤器,使用配置值
|
|
209
|
+
llm_filter = EventFilter(
|
|
210
|
+
allowed_types=[
|
|
211
|
+
AgentEventType.LLM_DELTA,
|
|
212
|
+
AgentEventType.TOOL_RESULT,
|
|
213
|
+
AgentEventType.AGENT_FINISH
|
|
214
|
+
],
|
|
215
|
+
enable_batching=True,
|
|
216
|
+
batch_size=config.event_batch_size,
|
|
217
|
+
batch_timeout=config.event_batch_timeout
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
self.unified_context.event_processor = EventProcessor(
|
|
221
|
+
filters=[llm_filter],
|
|
222
|
+
enable_stats=True
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# 4. 集成 TaskHandlers
|
|
226
|
+
if not self.unified_context.task_handlers:
|
|
227
|
+
self.unified_context.task_handlers = self.task_handlers or []
|
|
228
|
+
|
|
229
|
+
def _setup_cross_component_references(self):
|
|
230
|
+
"""
|
|
231
|
+
设置跨组件引用(已简化)
|
|
232
|
+
|
|
233
|
+
移除了魔法属性注入,改为通过协调器处理所有跨组件通信
|
|
234
|
+
"""
|
|
235
|
+
pass # 跨组件通信现在通过 IntelligentCoordinator 处理
|
|
236
|
+
|
|
237
|
+
# Tool orchestration (Loom 2.0 - intelligent parallel/sequential execution)
|
|
238
|
+
self.tool_orchestrator = ToolOrchestrator(
|
|
239
|
+
tools=self.tools,
|
|
240
|
+
permission_manager=self.permission_manager,
|
|
241
|
+
max_parallel=5,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# ==========================================
|
|
245
|
+
# CORE METHOD: tt (Tail-Recursive Control Loop)
|
|
246
|
+
# ==========================================
|
|
247
|
+
|
|
248
|
+
async def tt(
|
|
79
249
|
self,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
) ->
|
|
84
|
-
"""
|
|
250
|
+
messages: List[Message],
|
|
251
|
+
turn_state: TurnState,
|
|
252
|
+
context: ExecutionContext,
|
|
253
|
+
) -> AsyncGenerator[AgentEvent, None]:
|
|
254
|
+
"""
|
|
255
|
+
Tail-recursive control loop (inspired by Claude Code).
|
|
85
256
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
cancel_token: Optional Event to signal cancellation (US1)
|
|
89
|
-
correlation_id: Optional correlation ID for request tracing (US1)
|
|
257
|
+
This is the ONLY core execution method. It processes one turn of the
|
|
258
|
+
conversation, then recursively calls itself if tools were used.
|
|
90
259
|
|
|
91
|
-
|
|
92
|
-
|
|
260
|
+
Recursion Flow:
|
|
261
|
+
tt(messages, state_0, ctx)
|
|
262
|
+
→ LLM generates tool calls
|
|
263
|
+
→ Execute tools
|
|
264
|
+
→ tt(messages + tool_results, state_1, ctx) # Recursive call
|
|
265
|
+
→ LLM generates final answer
|
|
266
|
+
→ return (base case)
|
|
267
|
+
|
|
268
|
+
Base Cases (recursion terminates):
|
|
269
|
+
1. LLM returns final answer (no tools)
|
|
270
|
+
2. Maximum recursion depth reached
|
|
271
|
+
3. Execution cancelled
|
|
272
|
+
4. Error occurred
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
messages: New messages for this turn (not full history)
|
|
276
|
+
turn_state: Immutable turn state
|
|
277
|
+
context: Shared execution context
|
|
278
|
+
|
|
279
|
+
Yields:
|
|
280
|
+
AgentEvent: Events representing execution progress
|
|
281
|
+
|
|
282
|
+
Example:
|
|
283
|
+
```python
|
|
284
|
+
# Initial turn
|
|
285
|
+
state = TurnState.initial(max_iterations=10)
|
|
286
|
+
context = ExecutionContext.create()
|
|
287
|
+
messages = [Message(role="user", content="Search files")]
|
|
288
|
+
|
|
289
|
+
async for event in executor.tt(messages, state, context):
|
|
290
|
+
if event.type == AgentEventType.AGENT_FINISH:
|
|
291
|
+
print(f"Done: {event.content}")
|
|
292
|
+
```
|
|
93
293
|
"""
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
"
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
294
|
+
# ==========================================
|
|
295
|
+
# Phase 0: Recursion Control
|
|
296
|
+
# ==========================================
|
|
297
|
+
yield AgentEvent(
|
|
298
|
+
type=AgentEventType.ITERATION_START,
|
|
299
|
+
iteration=turn_state.turn_counter,
|
|
300
|
+
turn_id=turn_state.turn_id,
|
|
301
|
+
metadata={"parent_turn_id": turn_state.parent_turn_id},
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Base case 1: Maximum recursion depth reached
|
|
305
|
+
if turn_state.is_final:
|
|
306
|
+
yield AgentEvent(
|
|
307
|
+
type=AgentEventType.MAX_ITERATIONS_REACHED,
|
|
308
|
+
metadata={
|
|
309
|
+
"turn_counter": turn_state.turn_counter,
|
|
310
|
+
"max_iterations": turn_state.max_iterations,
|
|
311
|
+
},
|
|
312
|
+
)
|
|
313
|
+
await self._emit(
|
|
314
|
+
"max_iterations_reached",
|
|
315
|
+
{
|
|
316
|
+
"turn_counter": turn_state.turn_counter,
|
|
317
|
+
"max_iterations": turn_state.max_iterations,
|
|
318
|
+
},
|
|
319
|
+
)
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
# Base case 2: Execution cancelled
|
|
323
|
+
if context.is_cancelled():
|
|
324
|
+
yield AgentEvent(
|
|
325
|
+
type=AgentEventType.EXECUTION_CANCELLED,
|
|
326
|
+
metadata={"correlation_id": context.correlation_id},
|
|
327
|
+
)
|
|
328
|
+
await self._emit(
|
|
329
|
+
"execution_cancelled",
|
|
330
|
+
{"correlation_id": context.correlation_id},
|
|
331
|
+
)
|
|
332
|
+
return
|
|
333
|
+
|
|
334
|
+
# ==========================================
|
|
335
|
+
# Phase 1: Context Assembly
|
|
336
|
+
# ==========================================
|
|
337
|
+
yield AgentEvent.phase_start("context_assembly")
|
|
113
338
|
|
|
339
|
+
# Load conversation history from memory
|
|
114
340
|
history = await self._load_history()
|
|
115
341
|
|
|
116
|
-
#
|
|
117
|
-
|
|
342
|
+
# RAG retrieval (if configured)
|
|
343
|
+
rag_context = None
|
|
118
344
|
if self.context_retriever:
|
|
119
|
-
|
|
120
|
-
if retrieved_docs:
|
|
121
|
-
# 注入检索到的文档上下文
|
|
122
|
-
if self.context_retriever.inject_as == "system":
|
|
123
|
-
doc_context = self.context_retriever.format_documents(retrieved_docs)
|
|
124
|
-
history.append(Message(
|
|
125
|
-
role="system",
|
|
126
|
-
content=doc_context,
|
|
127
|
-
metadata={"type": "retrieved_context", "doc_count": len(retrieved_docs)}
|
|
128
|
-
))
|
|
129
|
-
# 记录检索指标
|
|
130
|
-
self.metrics.metrics.retrievals = getattr(self.metrics.metrics, "retrievals", 0) + 1
|
|
131
|
-
await self._emit("retrieval_complete", {"doc_count": len(retrieved_docs), "source": "execute"})
|
|
132
|
-
|
|
133
|
-
# Step 2: 添加用户消息
|
|
134
|
-
history.append(Message(role="user", content=user_input))
|
|
135
|
-
|
|
136
|
-
# Step 3: 压缩检查
|
|
137
|
-
history = await self._maybe_compress(history)
|
|
138
|
-
|
|
139
|
-
# Step 4: 动态生成系统提示
|
|
140
|
-
context = {"retrieved_docs_count": len(retrieved_docs)} if retrieved_docs else None
|
|
141
|
-
system_prompt = build_system_prompt(self.tools, self.system_instructions, context)
|
|
142
|
-
history = self._inject_system_prompt(history, system_prompt)
|
|
143
|
-
|
|
144
|
-
if not self.llm.supports_tools or not self.tools:
|
|
145
|
-
try:
|
|
146
|
-
# Create LLM task that can be cancelled
|
|
147
|
-
llm_task = asyncio.create_task(self.llm.generate([m.__dict__ for m in history]))
|
|
148
|
-
|
|
149
|
-
# Poll for cancellation while waiting for LLM
|
|
150
|
-
while not llm_task.done():
|
|
151
|
-
if cancel_token and cancel_token.is_set():
|
|
152
|
-
llm_task.cancel()
|
|
153
|
-
try:
|
|
154
|
-
await llm_task
|
|
155
|
-
except asyncio.CancelledError:
|
|
156
|
-
pass
|
|
157
|
-
partial_result = "Execution interrupted during LLM call"
|
|
158
|
-
await self._emit("agent_finish", {
|
|
159
|
-
"content": partial_result,
|
|
160
|
-
"source": "execute",
|
|
161
|
-
"correlation_id": correlation_id,
|
|
162
|
-
"interrupted": True,
|
|
163
|
-
})
|
|
164
|
-
return partial_result
|
|
165
|
-
await asyncio.sleep(0.1) # Check every 100ms
|
|
166
|
-
|
|
167
|
-
text = await llm_task
|
|
168
|
-
except asyncio.CancelledError:
|
|
169
|
-
partial_result = "Execution interrupted during LLM call"
|
|
170
|
-
await self._emit("agent_finish", {
|
|
171
|
-
"content": partial_result,
|
|
172
|
-
"source": "execute",
|
|
173
|
-
"correlation_id": correlation_id,
|
|
174
|
-
"interrupted": True,
|
|
175
|
-
})
|
|
176
|
-
return partial_result
|
|
177
|
-
except Exception as e:
|
|
178
|
-
self.metrics.metrics.total_errors += 1
|
|
179
|
-
await self._emit("error", {"stage": "llm_generate", "message": str(e)})
|
|
180
|
-
raise
|
|
181
|
-
self.metrics.metrics.llm_calls += 1
|
|
182
|
-
if self.memory:
|
|
183
|
-
await self.memory.add_message(Message(role="assistant", content=text))
|
|
184
|
-
await self._emit("agent_finish", {"content": text, "source": "execute"})
|
|
185
|
-
return text
|
|
186
|
-
|
|
187
|
-
tools_spec = self._serialize_tools()
|
|
188
|
-
iterations = 0
|
|
189
|
-
final_text = ""
|
|
190
|
-
while iterations < self.max_iterations:
|
|
191
|
-
# 🆕 US1: Check cancellation before each iteration
|
|
192
|
-
if cancel_token and cancel_token.is_set():
|
|
193
|
-
partial_result = f"Execution interrupted after {iterations} iterations. Partial progress: {final_text or '(in progress)'}"
|
|
194
|
-
await self._emit("agent_finish", {
|
|
195
|
-
"content": partial_result,
|
|
196
|
-
"source": "execute",
|
|
197
|
-
"correlation_id": correlation_id,
|
|
198
|
-
"interrupted": True,
|
|
199
|
-
"iterations_completed": iterations,
|
|
200
|
-
})
|
|
201
|
-
return partial_result
|
|
345
|
+
yield AgentEvent(type=AgentEventType.RETRIEVAL_START)
|
|
202
346
|
|
|
203
347
|
try:
|
|
204
|
-
|
|
348
|
+
# Extract user query from last message
|
|
349
|
+
user_query = ""
|
|
350
|
+
for msg in reversed(messages):
|
|
351
|
+
if msg.role == "user":
|
|
352
|
+
user_query = msg.content
|
|
353
|
+
break
|
|
354
|
+
|
|
355
|
+
if user_query:
|
|
356
|
+
retrieved_docs = await self.context_retriever.retrieve_for_query(
|
|
357
|
+
user_query
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
if retrieved_docs:
|
|
361
|
+
rag_context = self.context_retriever.format_documents(
|
|
362
|
+
retrieved_docs
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
# Emit retrieval progress
|
|
366
|
+
for doc in retrieved_docs:
|
|
367
|
+
yield AgentEvent(
|
|
368
|
+
type=AgentEventType.RETRIEVAL_PROGRESS,
|
|
369
|
+
metadata={
|
|
370
|
+
"doc_title": doc.metadata.get("title", "Unknown"),
|
|
371
|
+
"relevance_score": doc.metadata.get("score", 0.0),
|
|
372
|
+
},
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
yield AgentEvent(
|
|
376
|
+
type=AgentEventType.RETRIEVAL_COMPLETE,
|
|
377
|
+
metadata={"doc_count": len(retrieved_docs)},
|
|
378
|
+
)
|
|
379
|
+
self.metrics.metrics.retrievals = (
|
|
380
|
+
getattr(self.metrics.metrics, "retrievals", 0) + 1
|
|
381
|
+
)
|
|
382
|
+
|
|
205
383
|
except Exception as e:
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
]
|
|
226
|
-
await self._emit("tool_calls_start", {"tool_calls": meta, "source": "execute", "iteration": iterations})
|
|
227
|
-
except Exception:
|
|
228
|
-
pass
|
|
229
|
-
# 执行工具并把结果写回消息
|
|
230
|
-
try:
|
|
231
|
-
for tr in await self._execute_tool_batch(tool_calls):
|
|
232
|
-
tool_msg = Message(role="tool", content=tr.content, tool_call_id=tr.tool_call_id)
|
|
233
|
-
history.append(tool_msg)
|
|
234
|
-
if self.memory:
|
|
235
|
-
await self.memory.add_message(tool_msg)
|
|
236
|
-
await self._emit("tool_result", {"tool_call_id": tr.tool_call_id, "content": tr.content, "source": "execute", "iteration": iterations})
|
|
237
|
-
except Exception as e:
|
|
238
|
-
self.metrics.metrics.total_errors += 1
|
|
239
|
-
await self._emit("error", {"stage": "tool_execute", "message": str(e), "source": "execute", "iteration": iterations})
|
|
240
|
-
raise
|
|
241
|
-
iterations += 1
|
|
242
|
-
self.metrics.metrics.total_iterations += 1
|
|
243
|
-
history = await self._maybe_compress(history)
|
|
244
|
-
continue
|
|
245
|
-
|
|
246
|
-
# 无工具调用:认为生成最终答案
|
|
247
|
-
final_text = content
|
|
248
|
-
if self.memory:
|
|
249
|
-
await self.memory.add_message(Message(role="assistant", content=final_text))
|
|
250
|
-
await self._emit("agent_finish", {
|
|
251
|
-
"content": final_text,
|
|
252
|
-
"source": "execute",
|
|
253
|
-
"correlation_id": correlation_id, # 🆕 US1
|
|
254
|
-
})
|
|
255
|
-
break
|
|
256
|
-
|
|
257
|
-
return final_text
|
|
258
|
-
|
|
259
|
-
async def stream(self, user_input: str) -> AsyncGenerator[StreamEvent, None]:
|
|
260
|
-
"""流式执行:输出 text_delta/agent_finish 事件。后续可接入 tool_calls。"""
|
|
261
|
-
yield StreamEvent(type="request_start")
|
|
262
|
-
await self._emit("request_start", {"input": user_input, "source": "stream", "iteration": 0})
|
|
263
|
-
history = await self._load_history()
|
|
384
|
+
yield AgentEvent.error(e, retrieval_failed=True)
|
|
385
|
+
|
|
386
|
+
# Add new messages to history
|
|
387
|
+
history.extend(messages)
|
|
388
|
+
|
|
389
|
+
# Compression check
|
|
390
|
+
old_len = len(history)
|
|
391
|
+
history_compacted = await self._maybe_compress(history)
|
|
392
|
+
compacted_this_turn = len(history_compacted) < old_len
|
|
393
|
+
|
|
394
|
+
if compacted_this_turn:
|
|
395
|
+
history = history_compacted
|
|
396
|
+
yield AgentEvent(
|
|
397
|
+
type=AgentEventType.COMPRESSION_APPLIED,
|
|
398
|
+
metadata={
|
|
399
|
+
"messages_before": old_len,
|
|
400
|
+
"messages_after": len(history),
|
|
401
|
+
},
|
|
402
|
+
)
|
|
264
403
|
|
|
265
|
-
#
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
404
|
+
# 使用统一协调的智能上下文组装
|
|
405
|
+
if self.enable_unified_coordination and hasattr(self, 'coordinator'):
|
|
406
|
+
# 使用智能协调器进行上下文组装
|
|
407
|
+
execution_plan = self.coordinator.coordinate_tt_recursion(
|
|
408
|
+
messages, turn_state, context
|
|
409
|
+
)
|
|
410
|
+
final_system_prompt = execution_plan.get("context", "")
|
|
411
|
+
# 使用统一协调器的 assembler
|
|
412
|
+
assembler = self.unified_context.context_assembler
|
|
413
|
+
else:
|
|
414
|
+
# 传统方式组装系统提示
|
|
415
|
+
assembler = ContextAssembler(max_tokens=self.max_context_tokens)
|
|
416
|
+
|
|
417
|
+
# Add base instructions (critical priority)
|
|
418
|
+
if self.system_instructions:
|
|
419
|
+
assembler.add_component(
|
|
420
|
+
name="base_instructions",
|
|
421
|
+
content=self.system_instructions,
|
|
422
|
+
priority=ComponentPriority.CRITICAL,
|
|
423
|
+
truncatable=False,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Add RAG context (high priority)
|
|
427
|
+
if rag_context:
|
|
428
|
+
assembler.add_component(
|
|
429
|
+
name="retrieved_context",
|
|
430
|
+
content=rag_context,
|
|
431
|
+
priority=ComponentPriority.HIGH,
|
|
432
|
+
truncatable=True,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
# Add tool definitions (medium priority)
|
|
436
|
+
if self.tools:
|
|
437
|
+
tools_spec = self._serialize_tools()
|
|
438
|
+
tools_prompt = f"Available tools:\n{json.dumps(tools_spec, indent=2)}"
|
|
439
|
+
assembler.add_component(
|
|
440
|
+
name="tool_definitions",
|
|
441
|
+
content=tools_prompt,
|
|
442
|
+
priority=ComponentPriority.MEDIUM,
|
|
443
|
+
truncatable=False,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
# Assemble final system prompt
|
|
447
|
+
final_system_prompt = assembler.assemble()
|
|
448
|
+
|
|
449
|
+
# Inject system prompt into history
|
|
450
|
+
if history and history[0].role == "system":
|
|
451
|
+
history[0] = Message(role="system", content=final_system_prompt)
|
|
452
|
+
else:
|
|
453
|
+
history.insert(0, Message(role="system", content=final_system_prompt))
|
|
454
|
+
|
|
455
|
+
# Emit context assembly summary
|
|
456
|
+
summary = assembler.get_summary()
|
|
457
|
+
yield AgentEvent.phase_end(
|
|
458
|
+
"context_assembly",
|
|
459
|
+
tokens_used=summary["total_tokens"],
|
|
460
|
+
metadata={
|
|
461
|
+
"components": len(summary["components"]),
|
|
462
|
+
"utilization": summary["utilization"],
|
|
463
|
+
},
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
# ==========================================
|
|
467
|
+
# Phase 2: LLM Call
|
|
468
|
+
# ==========================================
|
|
469
|
+
yield AgentEvent(type=AgentEventType.LLM_START)
|
|
470
|
+
|
|
471
|
+
try:
|
|
472
|
+
if self.llm.supports_tools and self.tools:
|
|
473
|
+
# LLM with tool support
|
|
474
|
+
tools_spec = self._serialize_tools()
|
|
475
|
+
response = await self.llm.generate_with_tools(
|
|
476
|
+
[m.__dict__ for m in history], tools_spec
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
content = response.get("content", "")
|
|
480
|
+
tool_calls = response.get("tool_calls", [])
|
|
481
|
+
|
|
482
|
+
# Emit LLM content if available
|
|
483
|
+
if content:
|
|
484
|
+
yield AgentEvent(type=AgentEventType.LLM_DELTA, content=content)
|
|
485
|
+
|
|
486
|
+
else:
|
|
487
|
+
# Simple LLM generation (streaming)
|
|
488
|
+
content_parts = []
|
|
297
489
|
async for delta in self.llm.stream([m.__dict__ for m in history]):
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
490
|
+
content_parts.append(delta)
|
|
491
|
+
yield AgentEvent(type=AgentEventType.LLM_DELTA, content=delta)
|
|
492
|
+
|
|
493
|
+
content = "".join(content_parts)
|
|
494
|
+
tool_calls = []
|
|
495
|
+
|
|
496
|
+
yield AgentEvent(type=AgentEventType.LLM_COMPLETE)
|
|
497
|
+
|
|
498
|
+
except Exception as e:
|
|
499
|
+
self.metrics.metrics.total_errors += 1
|
|
500
|
+
yield AgentEvent.error(e, llm_failed=True)
|
|
501
|
+
await self._emit("error", {"stage": "llm_call", "message": str(e)})
|
|
304
502
|
return
|
|
305
503
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
504
|
+
self.metrics.metrics.llm_calls += 1
|
|
505
|
+
|
|
506
|
+
# ==========================================
|
|
507
|
+
# Phase 3: Decision Point (Base Case or Recurse)
|
|
508
|
+
# ==========================================
|
|
509
|
+
|
|
510
|
+
if not tool_calls:
|
|
511
|
+
# Base case: No tools → Conversation complete
|
|
512
|
+
yield AgentEvent(
|
|
513
|
+
type=AgentEventType.AGENT_FINISH,
|
|
514
|
+
content=content,
|
|
515
|
+
metadata={
|
|
516
|
+
"turn_counter": turn_state.turn_counter,
|
|
517
|
+
"total_llm_calls": self.metrics.metrics.llm_calls,
|
|
518
|
+
},
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
# Save to memory
|
|
522
|
+
if self.memory and content:
|
|
523
|
+
await self.memory.add_message(
|
|
524
|
+
Message(role="assistant", content=content)
|
|
326
525
|
)
|
|
327
|
-
|
|
328
|
-
try:
|
|
329
|
-
async for tr in self._execute_tool_calls_async(tc_models):
|
|
330
|
-
yield StreamEvent(type="tool_result", result=tr)
|
|
331
|
-
await self._emit("tool_result", {"tool_call_id": tr.tool_call_id, "content": tr.content, "source": "stream", "iteration": iterations})
|
|
332
|
-
tool_msg = Message(role="tool", content=tr.content, tool_call_id=tr.tool_call_id)
|
|
333
|
-
history.append(tool_msg)
|
|
334
|
-
if self.memory:
|
|
335
|
-
await self.memory.add_message(tool_msg)
|
|
336
|
-
except Exception as e:
|
|
337
|
-
self.metrics.metrics.total_errors += 1
|
|
338
|
-
await self._emit("error", {"stage": "tool_execute", "message": str(e), "source": "stream", "iteration": iterations})
|
|
339
|
-
raise
|
|
340
|
-
iterations += 1
|
|
341
|
-
self.metrics.metrics.total_iterations += 1
|
|
342
|
-
# 每轮结束后做压缩检查
|
|
343
|
-
history = await self._maybe_compress(history)
|
|
344
|
-
continue
|
|
345
|
-
|
|
346
|
-
# 无工具调用:输出最终文本并结束
|
|
347
|
-
if content:
|
|
348
|
-
yield StreamEvent(type="text_delta", content=content)
|
|
349
|
-
yield StreamEvent(type="agent_finish")
|
|
526
|
+
|
|
350
527
|
await self._emit("agent_finish", {"content": content})
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
528
|
+
return
|
|
529
|
+
|
|
530
|
+
# ==========================================
|
|
531
|
+
# Phase 4: Tool Execution
|
|
532
|
+
# ==========================================
|
|
533
|
+
yield AgentEvent(
|
|
534
|
+
type=AgentEventType.LLM_TOOL_CALLS,
|
|
535
|
+
metadata={
|
|
536
|
+
"tool_count": len(tool_calls),
|
|
537
|
+
"tool_names": [tc.get("name") for tc in tool_calls],
|
|
538
|
+
},
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
# Convert to ToolCall models
|
|
542
|
+
tc_models = [self._to_tool_call(tc) for tc in tool_calls]
|
|
543
|
+
|
|
544
|
+
# Execute tools using ToolOrchestrator
|
|
545
|
+
tool_results: List[ToolResult] = []
|
|
546
|
+
try:
|
|
547
|
+
async for event in self.tool_orchestrator.execute_batch(tc_models):
|
|
548
|
+
yield event # Forward all tool events
|
|
549
|
+
|
|
550
|
+
if event.type == AgentEventType.TOOL_RESULT:
|
|
551
|
+
tool_results.append(event.tool_result)
|
|
552
|
+
|
|
553
|
+
# Add to memory
|
|
554
|
+
tool_msg = Message(
|
|
555
|
+
role="tool",
|
|
556
|
+
content=event.tool_result.content,
|
|
557
|
+
tool_call_id=event.tool_result.tool_call_id,
|
|
558
|
+
)
|
|
559
|
+
if self.memory:
|
|
560
|
+
await self.memory.add_message(tool_msg)
|
|
561
|
+
|
|
562
|
+
elif event.type == AgentEventType.TOOL_ERROR:
|
|
563
|
+
# Collect error results too
|
|
564
|
+
if event.tool_result:
|
|
565
|
+
tool_results.append(event.tool_result)
|
|
566
|
+
|
|
567
|
+
except Exception as e:
|
|
568
|
+
self.metrics.metrics.total_errors += 1
|
|
569
|
+
yield AgentEvent.error(e, tool_execution_failed=True)
|
|
570
|
+
await self._emit("error", {"stage": "tool_execution", "message": str(e)})
|
|
571
|
+
return
|
|
572
|
+
|
|
573
|
+
yield AgentEvent(
|
|
574
|
+
type=AgentEventType.TOOL_CALLS_COMPLETE,
|
|
575
|
+
metadata={"results_count": len(tool_results)},
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
self.metrics.metrics.total_iterations += 1
|
|
579
|
+
|
|
580
|
+
# ==========================================
|
|
581
|
+
# Phase 5: Recursive Call (Tail Recursion)
|
|
582
|
+
# ==========================================
|
|
583
|
+
|
|
584
|
+
# Prepare next turn state
|
|
585
|
+
next_state = turn_state.next_turn(compacted=compacted_this_turn)
|
|
586
|
+
|
|
587
|
+
# Prepare next turn messages with intelligent context guidance
|
|
588
|
+
next_messages = self._prepare_recursive_messages(
|
|
589
|
+
messages, tool_results, turn_state, context
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
# Add tool results
|
|
593
|
+
for r in tool_results:
|
|
594
|
+
next_messages.append(
|
|
595
|
+
Message(
|
|
596
|
+
role="tool",
|
|
597
|
+
content=r.content,
|
|
598
|
+
tool_call_id=r.tool_call_id,
|
|
599
|
+
)
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
# Emit recursion event
|
|
603
|
+
yield AgentEvent(
|
|
604
|
+
type=AgentEventType.RECURSION,
|
|
605
|
+
metadata={
|
|
606
|
+
"from_turn": turn_state.turn_id,
|
|
607
|
+
"to_turn": next_state.turn_id,
|
|
608
|
+
"depth": next_state.turn_counter,
|
|
609
|
+
},
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
# 🔥 Tail-recursive call
|
|
613
|
+
async for event in self.tt(next_messages, next_state, context):
|
|
614
|
+
yield event
|
|
615
|
+
|
|
616
|
+
# ==========================================
|
|
617
|
+
# Intelligent Recursion Methods
|
|
618
|
+
# ==========================================
|
|
619
|
+
|
|
620
|
+
def _prepare_recursive_messages(
|
|
621
|
+
self,
|
|
622
|
+
messages: List[Message],
|
|
623
|
+
tool_results: List[ToolResult],
|
|
624
|
+
turn_state: TurnState,
|
|
625
|
+
context: ExecutionContext,
|
|
626
|
+
) -> List[Message]:
|
|
627
|
+
"""
|
|
628
|
+
智能准备递归调用的消息
|
|
629
|
+
|
|
630
|
+
基于工具结果类型、任务上下文和递归深度,生成合适的用户指导消息
|
|
631
|
+
"""
|
|
632
|
+
# 分析工具结果
|
|
633
|
+
result_analysis = self._analyze_tool_results(tool_results)
|
|
634
|
+
|
|
635
|
+
# 获取原始任务
|
|
636
|
+
original_task = self._extract_original_task(messages)
|
|
637
|
+
|
|
638
|
+
# 生成智能指导消息
|
|
639
|
+
guidance_message = self._generate_recursion_guidance(
|
|
640
|
+
original_task, result_analysis, turn_state.turn_counter
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
return [Message(role="user", content=guidance_message)]
|
|
644
|
+
|
|
645
|
+
def _analyze_tool_results(self, tool_results: List[ToolResult]) -> Dict[str, Any]:
|
|
646
|
+
"""分析工具结果类型和质量"""
|
|
647
|
+
analysis = {
|
|
648
|
+
"has_data": False,
|
|
649
|
+
"has_errors": False,
|
|
650
|
+
"suggests_completion": False,
|
|
651
|
+
"result_types": [],
|
|
652
|
+
"completeness_score": 0.0
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
for result in tool_results:
|
|
656
|
+
content = result.content.lower()
|
|
657
|
+
|
|
658
|
+
# 检查数据类型
|
|
659
|
+
if any(keyword in content for keyword in ["data", "found", "retrieved", "table", "schema", "获取到", "表结构", "结构"]):
|
|
660
|
+
analysis["has_data"] = True
|
|
661
|
+
analysis["result_types"].append("data")
|
|
662
|
+
analysis["completeness_score"] += 0.3
|
|
663
|
+
|
|
664
|
+
# 检查错误
|
|
665
|
+
if any(keyword in content for keyword in ["error", "failed", "exception", "not found"]):
|
|
666
|
+
analysis["has_errors"] = True
|
|
667
|
+
analysis["result_types"].append("error")
|
|
668
|
+
|
|
669
|
+
# 检查完成建议
|
|
670
|
+
if any(keyword in content for keyword in ["complete", "finished", "done", "ready"]):
|
|
671
|
+
analysis["suggests_completion"] = True
|
|
672
|
+
analysis["result_types"].append("completion")
|
|
673
|
+
analysis["completeness_score"] += 0.5
|
|
674
|
+
|
|
675
|
+
# 检查分析结果
|
|
676
|
+
if any(keyword in content for keyword in ["analysis", "summary", "conclusion", "insights"]):
|
|
677
|
+
analysis["result_types"].append("analysis")
|
|
678
|
+
analysis["completeness_score"] += 0.4
|
|
679
|
+
|
|
680
|
+
analysis["completeness_score"] = min(analysis["completeness_score"], 1.0)
|
|
681
|
+
return analysis
|
|
682
|
+
|
|
683
|
+
def _extract_original_task(self, messages: List[Message]) -> str:
|
|
684
|
+
"""从消息历史中提取原始任务"""
|
|
685
|
+
# 查找第一个用户消息作为原始任务
|
|
686
|
+
for message in messages:
|
|
687
|
+
if message.role == "user" and message.content:
|
|
688
|
+
# 过滤掉系统生成的递归消息
|
|
689
|
+
if not any(keyword in message.content.lower() for keyword in [
|
|
690
|
+
"工具调用已完成", "请基于工具返回的结果", "不要继续调用工具"
|
|
691
|
+
]):
|
|
692
|
+
return message.content
|
|
693
|
+
return "处理用户请求"
|
|
694
|
+
|
|
695
|
+
def _generate_recursion_guidance(
|
|
696
|
+
self,
|
|
697
|
+
original_task: str,
|
|
698
|
+
result_analysis: Dict[str, Any],
|
|
699
|
+
recursion_depth: int
|
|
700
|
+
) -> str:
|
|
701
|
+
"""生成递归指导消息"""
|
|
702
|
+
|
|
703
|
+
# 使用可扩展的任务处理器
|
|
704
|
+
if hasattr(self, 'task_handlers') and self.task_handlers:
|
|
705
|
+
for handler in self.task_handlers:
|
|
706
|
+
if handler.can_handle(original_task):
|
|
707
|
+
return handler.generate_guidance(original_task, result_analysis, recursion_depth)
|
|
708
|
+
|
|
709
|
+
# 默认处理
|
|
710
|
+
return self._generate_default_guidance(original_task, result_analysis, recursion_depth)
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def _generate_default_guidance(
|
|
714
|
+
self,
|
|
715
|
+
original_task: str,
|
|
716
|
+
result_analysis: Dict[str, Any],
|
|
717
|
+
recursion_depth: int
|
|
718
|
+
) -> str:
|
|
719
|
+
"""生成默认的递归指导"""
|
|
720
|
+
|
|
721
|
+
if result_analysis["suggests_completion"] or recursion_depth >= 6:
|
|
722
|
+
return f"""工具调用已完成。请基于返回的结果完成任务:{original_task}
|
|
723
|
+
|
|
724
|
+
请提供完整、准确的最终答案。"""
|
|
725
|
+
|
|
726
|
+
elif result_analysis["has_errors"]:
|
|
727
|
+
return f"""工具执行遇到问题。请重新尝试完成任务:{original_task}
|
|
728
|
+
|
|
729
|
+
建议:
|
|
730
|
+
- 检查工具参数是否正确
|
|
731
|
+
- 尝试使用不同的工具或方法
|
|
732
|
+
- 如果问题持续,请说明具体错误"""
|
|
733
|
+
|
|
734
|
+
else:
|
|
735
|
+
return f"""继续处理任务:{original_task}
|
|
736
|
+
|
|
737
|
+
当前进度:{result_analysis['completeness_score']:.0%}
|
|
738
|
+
建议:使用更多工具收集信息或分析已获得的结果"""
|
|
739
|
+
|
|
740
|
+
# ==========================================
|
|
741
|
+
# Helper Methods
|
|
742
|
+
# ==========================================
|
|
354
743
|
|
|
355
744
|
async def _load_history(self) -> List[Message]:
|
|
745
|
+
"""Load conversation history from memory."""
|
|
356
746
|
if not self.memory:
|
|
357
747
|
return []
|
|
358
748
|
return await self.memory.get_messages()
|
|
359
749
|
|
|
360
750
|
async def _maybe_compress(self, history: List[Message]) -> List[Message]:
|
|
361
|
-
"""Check if compression needed and apply if threshold reached.
|
|
362
|
-
|
|
363
|
-
US2: Automatic compression at 92% threshold with 8-segment summarization.
|
|
364
|
-
"""
|
|
751
|
+
"""Check if compression needed and apply if threshold reached."""
|
|
365
752
|
if not self.compressor:
|
|
366
753
|
return history
|
|
367
754
|
|
|
@@ -369,16 +756,19 @@ class AgentExecutor:
|
|
|
369
756
|
|
|
370
757
|
# Check if compression should be triggered (92% threshold)
|
|
371
758
|
if self.compressor.should_compress(tokens_before, self.max_context_tokens):
|
|
372
|
-
# Attempt compression
|
|
373
759
|
try:
|
|
374
760
|
compressed_messages, metadata = await self.compressor.compress(history)
|
|
375
761
|
|
|
376
762
|
# Update metrics
|
|
377
|
-
self.metrics.metrics.compressions =
|
|
763
|
+
self.metrics.metrics.compressions = (
|
|
764
|
+
getattr(self.metrics.metrics, "compressions", 0) + 1
|
|
765
|
+
)
|
|
378
766
|
if metadata.key_topics == ["fallback"]:
|
|
379
|
-
self.metrics.metrics.compression_fallbacks =
|
|
767
|
+
self.metrics.metrics.compression_fallbacks = (
|
|
768
|
+
getattr(self.metrics.metrics, "compression_fallbacks", 0) + 1
|
|
769
|
+
)
|
|
380
770
|
|
|
381
|
-
# Emit compression event
|
|
771
|
+
# Emit compression event
|
|
382
772
|
await self._emit(
|
|
383
773
|
"compression_applied",
|
|
384
774
|
{
|
|
@@ -395,17 +785,17 @@ class AgentExecutor:
|
|
|
395
785
|
return compressed_messages
|
|
396
786
|
|
|
397
787
|
except Exception as e:
|
|
398
|
-
# Compression failed - continue without compression
|
|
399
788
|
self.metrics.metrics.total_errors += 1
|
|
400
|
-
await self._emit(
|
|
401
|
-
"
|
|
402
|
-
"message": str(e),
|
|
403
|
-
|
|
789
|
+
await self._emit(
|
|
790
|
+
"error",
|
|
791
|
+
{"stage": "compression", "message": str(e)},
|
|
792
|
+
)
|
|
404
793
|
return history
|
|
405
794
|
|
|
406
795
|
return history
|
|
407
796
|
|
|
408
797
|
def _serialize_tools(self) -> List[Dict]:
|
|
798
|
+
"""Serialize tools to LLM-compatible format."""
|
|
409
799
|
tools_spec: List[Dict] = []
|
|
410
800
|
for t in self.tools.values():
|
|
411
801
|
schema = {}
|
|
@@ -413,6 +803,7 @@ class AgentExecutor:
|
|
|
413
803
|
schema = t.args_schema.model_json_schema() # type: ignore[attr-defined]
|
|
414
804
|
except Exception:
|
|
415
805
|
schema = {"type": "object", "properties": {}}
|
|
806
|
+
|
|
416
807
|
tools_spec.append(
|
|
417
808
|
{
|
|
418
809
|
"type": "function",
|
|
@@ -426,25 +817,90 @@ class AgentExecutor:
|
|
|
426
817
|
return tools_spec
|
|
427
818
|
|
|
428
819
|
def _to_tool_call(self, raw: Dict) -> ToolCall:
|
|
429
|
-
|
|
430
|
-
return ToolCall(
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
820
|
+
"""Convert raw dict to ToolCall model."""
|
|
821
|
+
return ToolCall(
|
|
822
|
+
id=str(raw.get("id", "call_0")),
|
|
823
|
+
name=raw["name"],
|
|
824
|
+
arguments=raw.get("arguments", {}),
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
async def _emit(self, event_type: str, payload: Dict) -> None:
|
|
828
|
+
"""Emit event to callbacks."""
|
|
829
|
+
if not self.callbacks:
|
|
830
|
+
return
|
|
831
|
+
|
|
832
|
+
enriched = dict(payload)
|
|
833
|
+
enriched.setdefault("ts", time.time())
|
|
834
|
+
enriched.setdefault("type", event_type)
|
|
835
|
+
|
|
836
|
+
for cb in self.callbacks:
|
|
837
|
+
try:
|
|
838
|
+
await cb.on_event(event_type, enriched)
|
|
839
|
+
except Exception:
|
|
840
|
+
# Best-effort; don't fail execution on callback errors
|
|
841
|
+
pass
|
|
842
|
+
|
|
843
|
+
# ==========================================
|
|
844
|
+
# Backward Compatibility Wrappers
|
|
845
|
+
# ==========================================
|
|
846
|
+
|
|
847
|
+
async def execute(
|
|
848
|
+
self,
|
|
849
|
+
user_input: str,
|
|
850
|
+
cancel_token: Optional[asyncio.Event] = None,
|
|
851
|
+
correlation_id: Optional[str] = None,
|
|
852
|
+
) -> str:
|
|
853
|
+
"""
|
|
854
|
+
Execute agent and return final response (backward compatible wrapper).
|
|
855
|
+
|
|
856
|
+
This method wraps the new tt() recursive API and extracts the final
|
|
857
|
+
response for backward compatibility with existing code.
|
|
858
|
+
|
|
859
|
+
Args:
|
|
860
|
+
user_input: User input text
|
|
861
|
+
cancel_token: Optional cancellation event
|
|
862
|
+
correlation_id: Optional correlation ID for tracing
|
|
863
|
+
|
|
864
|
+
Returns:
|
|
865
|
+
str: Final response text
|
|
866
|
+
|
|
867
|
+
Example:
|
|
868
|
+
```python
|
|
869
|
+
executor = AgentExecutor(llm=llm, tools=tools)
|
|
870
|
+
response = await executor.execute("Hello")
|
|
871
|
+
print(response)
|
|
872
|
+
```
|
|
873
|
+
"""
|
|
874
|
+
# Initialize state and context
|
|
875
|
+
turn_state = TurnState.initial(max_iterations=self.max_iterations)
|
|
876
|
+
context = ExecutionContext.create(
|
|
877
|
+
correlation_id=correlation_id,
|
|
878
|
+
cancel_token=cancel_token,
|
|
879
|
+
)
|
|
880
|
+
messages = [Message(role="user", content=user_input)]
|
|
881
|
+
|
|
882
|
+
# Execute with tt and collect result
|
|
883
|
+
final_content = ""
|
|
884
|
+
async for event in self.tt(messages, turn_state, context):
|
|
885
|
+
# Accumulate LLM deltas
|
|
886
|
+
if event.type == AgentEventType.LLM_DELTA:
|
|
887
|
+
final_content += event.content or ""
|
|
888
|
+
|
|
889
|
+
# Return on finish
|
|
890
|
+
elif event.type == AgentEventType.AGENT_FINISH:
|
|
891
|
+
return event.content or final_content
|
|
892
|
+
|
|
893
|
+
# Handle cancellation
|
|
894
|
+
elif event.type == AgentEventType.EXECUTION_CANCELLED:
|
|
895
|
+
return "cancelled"
|
|
896
|
+
|
|
897
|
+
# Handle max iterations
|
|
898
|
+
elif event.type == AgentEventType.MAX_ITERATIONS_REACHED:
|
|
899
|
+
return final_content or "Max iterations reached"
|
|
900
|
+
|
|
901
|
+
# Raise on error
|
|
902
|
+
elif event.type == AgentEventType.ERROR:
|
|
903
|
+
if event.error:
|
|
904
|
+
raise event.error
|
|
905
|
+
|
|
906
|
+
return final_content
|