agent-runtime-core 0.7.0__py3-none-any.whl → 0.8.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.
- agent_runtime_core/__init__.py +109 -2
- agent_runtime_core/agentic_loop.py +254 -0
- agent_runtime_core/config.py +54 -4
- agent_runtime_core/config_schema.py +307 -0
- agent_runtime_core/files/__init__.py +88 -0
- agent_runtime_core/files/base.py +343 -0
- agent_runtime_core/files/ocr.py +406 -0
- agent_runtime_core/files/processors.py +508 -0
- agent_runtime_core/files/tools.py +317 -0
- agent_runtime_core/files/vision.py +360 -0
- agent_runtime_core/interfaces.py +106 -0
- agent_runtime_core/json_runtime.py +509 -0
- agent_runtime_core/llm/__init__.py +80 -7
- agent_runtime_core/llm/anthropic.py +133 -12
- agent_runtime_core/llm/models_config.py +180 -0
- agent_runtime_core/memory/__init__.py +70 -0
- agent_runtime_core/memory/manager.py +554 -0
- agent_runtime_core/memory/mixin.py +294 -0
- agent_runtime_core/multi_agent.py +569 -0
- agent_runtime_core/persistence/__init__.py +2 -0
- agent_runtime_core/persistence/file.py +277 -0
- agent_runtime_core/rag/__init__.py +65 -0
- agent_runtime_core/rag/chunking.py +224 -0
- agent_runtime_core/rag/indexer.py +253 -0
- agent_runtime_core/rag/retriever.py +261 -0
- agent_runtime_core/runner.py +193 -15
- agent_runtime_core/tool_calling_agent.py +88 -130
- agent_runtime_core/tools.py +179 -0
- agent_runtime_core/vectorstore/__init__.py +193 -0
- agent_runtime_core/vectorstore/base.py +138 -0
- agent_runtime_core/vectorstore/embeddings.py +242 -0
- agent_runtime_core/vectorstore/sqlite_vec.py +328 -0
- agent_runtime_core/vectorstore/vertex.py +295 -0
- {agent_runtime_core-0.7.0.dist-info → agent_runtime_core-0.8.0.dist-info}/METADATA +236 -1
- agent_runtime_core-0.8.0.dist-info/RECORD +63 -0
- agent_runtime_core-0.7.0.dist-info/RECORD +0 -39
- {agent_runtime_core-0.7.0.dist-info → agent_runtime_core-0.8.0.dist-info}/WHEEL +0 -0
- {agent_runtime_core-0.7.0.dist-info → agent_runtime_core-0.8.0.dist-info}/licenses/LICENSE +0 -0
agent_runtime_core/runner.py
CHANGED
|
@@ -8,14 +8,17 @@ The runner handles:
|
|
|
8
8
|
- Managing state and checkpoints
|
|
9
9
|
- Handling errors and retries
|
|
10
10
|
- Cancellation
|
|
11
|
+
- Loading conversation history (enabled by default)
|
|
12
|
+
- Persisting messages after runs (enabled by default)
|
|
11
13
|
"""
|
|
12
14
|
|
|
13
15
|
import asyncio
|
|
16
|
+
import logging
|
|
14
17
|
import traceback
|
|
15
18
|
from dataclasses import dataclass, field
|
|
16
19
|
from datetime import datetime, timezone
|
|
17
20
|
from typing import Optional
|
|
18
|
-
from uuid import UUID
|
|
21
|
+
from uuid import UUID, uuid4
|
|
19
22
|
|
|
20
23
|
from agent_runtime_core.config import get_config
|
|
21
24
|
from agent_runtime_core.events.base import EventBus
|
|
@@ -30,6 +33,16 @@ from agent_runtime_core.interfaces import (
|
|
|
30
33
|
from agent_runtime_core.queue.base import RunQueue, QueuedRun
|
|
31
34
|
from agent_runtime_core.registry import get_runtime
|
|
32
35
|
from agent_runtime_core.state.base import StateStore
|
|
36
|
+
from agent_runtime_core.persistence import (
|
|
37
|
+
PersistenceManager,
|
|
38
|
+
ConversationStore,
|
|
39
|
+
Conversation,
|
|
40
|
+
ConversationMessage,
|
|
41
|
+
ToolCall,
|
|
42
|
+
get_persistence_manager,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
logger = logging.getLogger(__name__)
|
|
33
46
|
|
|
34
47
|
|
|
35
48
|
@dataclass
|
|
@@ -127,26 +140,31 @@ class RunContextImpl:
|
|
|
127
140
|
class AgentRunner:
|
|
128
141
|
"""
|
|
129
142
|
Runs agent executions with full lifecycle management.
|
|
130
|
-
|
|
143
|
+
|
|
131
144
|
The runner:
|
|
132
145
|
1. Claims runs from the queue
|
|
133
146
|
2. Looks up the appropriate runtime
|
|
134
|
-
3.
|
|
135
|
-
4.
|
|
136
|
-
5.
|
|
147
|
+
3. Loads conversation history (enabled by default)
|
|
148
|
+
4. Executes the runtime with a context
|
|
149
|
+
5. Handles errors, retries, and cancellation
|
|
150
|
+
6. Persists messages after successful runs
|
|
151
|
+
7. Emits events throughout
|
|
137
152
|
"""
|
|
138
|
-
|
|
153
|
+
|
|
139
154
|
def __init__(
|
|
140
155
|
self,
|
|
141
156
|
queue: RunQueue,
|
|
142
157
|
event_bus: EventBus,
|
|
143
158
|
state_store: StateStore,
|
|
144
159
|
config: Optional[RunnerConfig] = None,
|
|
160
|
+
persistence_manager: Optional[PersistenceManager] = None,
|
|
145
161
|
):
|
|
146
162
|
self.queue = queue
|
|
147
163
|
self.event_bus = event_bus
|
|
148
164
|
self.state_store = state_store
|
|
149
165
|
self.config = config or RunnerConfig()
|
|
166
|
+
self._persistence = persistence_manager or get_persistence_manager()
|
|
167
|
+
self._runtime_config = get_config()
|
|
150
168
|
self._running = False
|
|
151
169
|
self._current_run: Optional[UUID] = None
|
|
152
170
|
|
|
@@ -205,9 +223,9 @@ class AgentRunner:
|
|
|
205
223
|
|
|
206
224
|
# Update status
|
|
207
225
|
await self.state_store.update_run_status(run_id, "running")
|
|
208
|
-
|
|
209
|
-
# Build context
|
|
210
|
-
ctx = self._build_context(queued_run)
|
|
226
|
+
|
|
227
|
+
# Build context (loads conversation history if enabled)
|
|
228
|
+
ctx = await self._build_context(queued_run)
|
|
211
229
|
|
|
212
230
|
# Emit started event
|
|
213
231
|
await ctx.emit(EventType.RUN_STARTED, {
|
|
@@ -246,14 +264,25 @@ class AgentRunner:
|
|
|
246
264
|
finally:
|
|
247
265
|
self._current_run = None
|
|
248
266
|
|
|
249
|
-
def _build_context(self, queued_run: QueuedRun) -> RunContextImpl:
|
|
267
|
+
async def _build_context(self, queued_run: QueuedRun) -> RunContextImpl:
|
|
250
268
|
"""Build a RunContext for a queued run."""
|
|
251
269
|
input_data = queued_run.input
|
|
252
|
-
|
|
270
|
+
new_messages = input_data.get("messages", [])
|
|
271
|
+
conversation_id = input_data.get("conversation_id")
|
|
272
|
+
|
|
273
|
+
# Parse conversation_id if it's a string
|
|
274
|
+
if conversation_id and isinstance(conversation_id, str):
|
|
275
|
+
conversation_id = UUID(conversation_id)
|
|
276
|
+
|
|
277
|
+
# Load conversation history if enabled
|
|
278
|
+
messages = await self._load_conversation_history(
|
|
279
|
+
conversation_id, new_messages
|
|
280
|
+
)
|
|
281
|
+
|
|
253
282
|
return RunContextImpl(
|
|
254
283
|
run_id=queued_run.run_id,
|
|
255
|
-
conversation_id=
|
|
256
|
-
input_messages=
|
|
284
|
+
conversation_id=conversation_id,
|
|
285
|
+
input_messages=messages,
|
|
257
286
|
params=input_data.get("params", {}),
|
|
258
287
|
metadata=queued_run.metadata,
|
|
259
288
|
tool_registry=ToolRegistry(), # TODO: Load from config
|
|
@@ -261,6 +290,90 @@ class AgentRunner:
|
|
|
261
290
|
state_store=self.state_store,
|
|
262
291
|
queue=self.queue,
|
|
263
292
|
)
|
|
293
|
+
|
|
294
|
+
async def _load_conversation_history(
|
|
295
|
+
self,
|
|
296
|
+
conversation_id: Optional[UUID],
|
|
297
|
+
new_messages: list[Message],
|
|
298
|
+
) -> list[Message]:
|
|
299
|
+
"""
|
|
300
|
+
Load conversation history and prepend to new messages.
|
|
301
|
+
|
|
302
|
+
This enables multi-turn conversations by default. When a run is part of
|
|
303
|
+
a conversation, previous messages from that conversation are automatically
|
|
304
|
+
included in the context.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
conversation_id: The conversation ID (if any)
|
|
308
|
+
new_messages: The new messages for this run
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Combined list of history + new messages
|
|
312
|
+
"""
|
|
313
|
+
# Check if conversation history is enabled
|
|
314
|
+
if not self._runtime_config.include_conversation_history:
|
|
315
|
+
logger.debug("Conversation history disabled by config")
|
|
316
|
+
return new_messages
|
|
317
|
+
|
|
318
|
+
if not conversation_id:
|
|
319
|
+
logger.debug("No conversation_id, skipping history load")
|
|
320
|
+
return new_messages
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
# Get conversation from persistence
|
|
324
|
+
conversation = await self._persistence.conversations.get(conversation_id)
|
|
325
|
+
|
|
326
|
+
if conversation is None:
|
|
327
|
+
logger.debug(f"Conversation {conversation_id} not found")
|
|
328
|
+
return new_messages
|
|
329
|
+
|
|
330
|
+
# Convert stored messages to the Message format
|
|
331
|
+
history: list[Message] = []
|
|
332
|
+
for msg in conversation.messages:
|
|
333
|
+
message_dict: Message = {
|
|
334
|
+
"role": msg.role,
|
|
335
|
+
"content": msg.content,
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
# Include tool_calls for assistant messages
|
|
339
|
+
if msg.tool_calls:
|
|
340
|
+
message_dict["tool_calls"] = [
|
|
341
|
+
{
|
|
342
|
+
"id": tc.id,
|
|
343
|
+
"type": "function",
|
|
344
|
+
"function": {
|
|
345
|
+
"name": tc.name,
|
|
346
|
+
"arguments": tc.arguments if isinstance(tc.arguments, str) else str(tc.arguments),
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
for tc in msg.tool_calls
|
|
350
|
+
]
|
|
351
|
+
|
|
352
|
+
# Include tool_call_id for tool messages
|
|
353
|
+
if msg.tool_call_id:
|
|
354
|
+
message_dict["tool_call_id"] = msg.tool_call_id
|
|
355
|
+
|
|
356
|
+
history.append(message_dict)
|
|
357
|
+
|
|
358
|
+
if not history:
|
|
359
|
+
logger.debug("No conversation history found")
|
|
360
|
+
return new_messages
|
|
361
|
+
|
|
362
|
+
# Apply message limit if configured
|
|
363
|
+
max_messages = self._runtime_config.max_history_messages
|
|
364
|
+
if max_messages and len(history) > max_messages:
|
|
365
|
+
logger.debug(f"Limiting history from {len(history)} to {max_messages} messages")
|
|
366
|
+
history = history[-max_messages:]
|
|
367
|
+
|
|
368
|
+
logger.debug(f"Loaded {len(history)} history messages for conversation {conversation_id}")
|
|
369
|
+
|
|
370
|
+
# Combine history with new messages
|
|
371
|
+
return history + new_messages
|
|
372
|
+
|
|
373
|
+
except Exception as e:
|
|
374
|
+
logger.warning(f"Failed to load conversation history: {e}")
|
|
375
|
+
# Fall back to just new messages on error
|
|
376
|
+
return new_messages
|
|
264
377
|
|
|
265
378
|
async def _heartbeat_loop(self, run_id: UUID) -> None:
|
|
266
379
|
"""Send periodic heartbeats to extend the lease."""
|
|
@@ -293,18 +406,83 @@ class AgentRunner:
|
|
|
293
406
|
) -> None:
|
|
294
407
|
"""Handle successful run completion."""
|
|
295
408
|
await self.state_store.update_run_status(queued_run.run_id, "succeeded")
|
|
296
|
-
|
|
409
|
+
|
|
410
|
+
# Persist messages to conversation store if enabled
|
|
411
|
+
if self._runtime_config.auto_persist_messages and ctx.conversation_id:
|
|
412
|
+
await self._persist_messages(ctx.conversation_id, result.final_messages)
|
|
413
|
+
|
|
297
414
|
await ctx.emit(EventType.RUN_SUCCEEDED, {
|
|
298
415
|
"final_output": result.final_output,
|
|
299
416
|
"usage": result.usage,
|
|
300
417
|
})
|
|
301
|
-
|
|
418
|
+
|
|
302
419
|
await self.queue.release(
|
|
303
420
|
run_id=queued_run.run_id,
|
|
304
421
|
worker_id=self.config.worker_id,
|
|
305
422
|
success=True,
|
|
306
423
|
output=result.final_output,
|
|
307
424
|
)
|
|
425
|
+
|
|
426
|
+
async def _persist_messages(
|
|
427
|
+
self,
|
|
428
|
+
conversation_id: UUID,
|
|
429
|
+
messages: list[Message],
|
|
430
|
+
) -> None:
|
|
431
|
+
"""
|
|
432
|
+
Persist messages to the conversation store.
|
|
433
|
+
|
|
434
|
+
This saves the final_messages from a run to enable conversation history
|
|
435
|
+
for future runs.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
conversation_id: The conversation to save to
|
|
439
|
+
messages: The messages to persist (typically result.final_messages)
|
|
440
|
+
"""
|
|
441
|
+
try:
|
|
442
|
+
# Get or create conversation
|
|
443
|
+
conversation = await self._persistence.conversations.get(conversation_id)
|
|
444
|
+
|
|
445
|
+
if conversation is None:
|
|
446
|
+
# Create new conversation
|
|
447
|
+
conversation = Conversation(
|
|
448
|
+
id=conversation_id,
|
|
449
|
+
title="",
|
|
450
|
+
messages=[],
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
# Convert messages to ConversationMessage format
|
|
454
|
+
for msg in messages:
|
|
455
|
+
# Skip system messages - they're added by the agent each run
|
|
456
|
+
if msg.get("role") == "system":
|
|
457
|
+
continue
|
|
458
|
+
|
|
459
|
+
# Parse tool_calls if present
|
|
460
|
+
tool_calls = []
|
|
461
|
+
if msg.get("tool_calls"):
|
|
462
|
+
for tc in msg["tool_calls"]:
|
|
463
|
+
tool_calls.append(ToolCall(
|
|
464
|
+
id=tc.get("id", ""),
|
|
465
|
+
name=tc.get("function", {}).get("name", ""),
|
|
466
|
+
arguments=tc.get("function", {}).get("arguments", "{}"),
|
|
467
|
+
timestamp=datetime.now(timezone.utc),
|
|
468
|
+
))
|
|
469
|
+
|
|
470
|
+
conv_message = ConversationMessage(
|
|
471
|
+
id=uuid4(),
|
|
472
|
+
role=msg.get("role", "user"),
|
|
473
|
+
content=msg.get("content", ""),
|
|
474
|
+
timestamp=datetime.now(timezone.utc),
|
|
475
|
+
tool_calls=tool_calls,
|
|
476
|
+
tool_call_id=msg.get("tool_call_id"),
|
|
477
|
+
)
|
|
478
|
+
conversation.messages.append(conv_message)
|
|
479
|
+
|
|
480
|
+
# Save conversation
|
|
481
|
+
await self._persistence.conversations.save(conversation)
|
|
482
|
+
logger.debug(f"Persisted {len(messages)} messages to conversation {conversation_id}")
|
|
483
|
+
|
|
484
|
+
except Exception as e:
|
|
485
|
+
logger.warning(f"Failed to persist messages: {e}")
|
|
308
486
|
|
|
309
487
|
async def _handle_timeout(
|
|
310
488
|
self,
|
|
@@ -4,12 +4,13 @@ ToolCallingAgent - A base class for agents that use tool calling.
|
|
|
4
4
|
This eliminates the boilerplate of implementing the tool-calling loop
|
|
5
5
|
in every agent. Just define your system prompt and tools, and the base
|
|
6
6
|
class handles the rest.
|
|
7
|
+
|
|
8
|
+
Uses run_agentic_loop internally for the actual loop logic.
|
|
7
9
|
"""
|
|
8
10
|
|
|
9
|
-
import json
|
|
10
11
|
import logging
|
|
11
12
|
from abc import abstractmethod
|
|
12
|
-
from typing import Optional
|
|
13
|
+
from typing import Any, Optional
|
|
13
14
|
|
|
14
15
|
from agent_runtime_core.interfaces import (
|
|
15
16
|
AgentRuntime,
|
|
@@ -19,6 +20,7 @@ from agent_runtime_core.interfaces import (
|
|
|
19
20
|
ToolRegistry,
|
|
20
21
|
LLMClient,
|
|
21
22
|
)
|
|
23
|
+
from agent_runtime_core.agentic_loop import run_agentic_loop
|
|
22
24
|
|
|
23
25
|
logger = logging.getLogger(__name__)
|
|
24
26
|
|
|
@@ -26,231 +28,187 @@ logger = logging.getLogger(__name__)
|
|
|
26
28
|
class ToolCallingAgent(AgentRuntime):
|
|
27
29
|
"""
|
|
28
30
|
Base class for agents that use tool calling.
|
|
29
|
-
|
|
31
|
+
|
|
30
32
|
Handles the standard tool-calling loop so you don't have to implement it
|
|
31
33
|
in every agent. Just override the abstract properties and you're done.
|
|
32
|
-
|
|
34
|
+
|
|
35
|
+
Uses run_agentic_loop internally, with hooks for customization.
|
|
36
|
+
|
|
33
37
|
Example:
|
|
34
38
|
class MyAgent(ToolCallingAgent):
|
|
35
39
|
@property
|
|
36
40
|
def key(self) -> str:
|
|
37
41
|
return "my-agent"
|
|
38
|
-
|
|
42
|
+
|
|
39
43
|
@property
|
|
40
44
|
def system_prompt(self) -> str:
|
|
41
45
|
return "You are a helpful assistant..."
|
|
42
|
-
|
|
46
|
+
|
|
43
47
|
@property
|
|
44
48
|
def tools(self) -> ToolRegistry:
|
|
45
49
|
return create_my_tools()
|
|
46
50
|
"""
|
|
47
|
-
|
|
51
|
+
|
|
48
52
|
@property
|
|
49
53
|
@abstractmethod
|
|
50
54
|
def system_prompt(self) -> str:
|
|
51
55
|
"""
|
|
52
56
|
System prompt for the agent.
|
|
53
|
-
|
|
57
|
+
|
|
54
58
|
This is prepended to the conversation messages.
|
|
55
59
|
"""
|
|
56
60
|
...
|
|
57
|
-
|
|
61
|
+
|
|
58
62
|
@property
|
|
59
63
|
@abstractmethod
|
|
60
64
|
def tools(self) -> ToolRegistry:
|
|
61
65
|
"""
|
|
62
66
|
Tools available to the agent.
|
|
63
|
-
|
|
67
|
+
|
|
64
68
|
Return a ToolRegistry with all tools registered.
|
|
65
69
|
"""
|
|
66
70
|
...
|
|
67
|
-
|
|
71
|
+
|
|
68
72
|
@property
|
|
69
73
|
def max_iterations(self) -> int:
|
|
70
74
|
"""
|
|
71
75
|
Maximum number of tool-calling iterations.
|
|
72
|
-
|
|
76
|
+
|
|
73
77
|
Override to change the default limit.
|
|
74
78
|
"""
|
|
75
|
-
return
|
|
76
|
-
|
|
79
|
+
return 15
|
|
80
|
+
|
|
77
81
|
@property
|
|
78
82
|
def model(self) -> Optional[str]:
|
|
79
83
|
"""
|
|
80
84
|
Model to use for this agent.
|
|
81
|
-
|
|
85
|
+
|
|
82
86
|
If None, uses the default model from configuration.
|
|
83
87
|
Override to use a specific model.
|
|
84
88
|
"""
|
|
85
89
|
return None
|
|
86
|
-
|
|
90
|
+
|
|
87
91
|
@property
|
|
88
92
|
def temperature(self) -> Optional[float]:
|
|
89
93
|
"""
|
|
90
94
|
Temperature for LLM generation.
|
|
91
|
-
|
|
95
|
+
|
|
92
96
|
If None, uses the LLM client's default.
|
|
93
97
|
Override to set a specific temperature.
|
|
94
98
|
"""
|
|
95
99
|
return None
|
|
96
|
-
|
|
100
|
+
|
|
97
101
|
def get_llm_client(self) -> LLMClient:
|
|
98
102
|
"""
|
|
99
103
|
Get the LLM client to use.
|
|
100
|
-
|
|
104
|
+
|
|
101
105
|
Override to customize LLM client selection.
|
|
102
106
|
Default uses the configured client.
|
|
103
107
|
"""
|
|
104
108
|
from agent_runtime_core.llm import get_llm_client
|
|
105
109
|
return get_llm_client()
|
|
106
|
-
|
|
110
|
+
|
|
107
111
|
async def before_run(self, ctx: RunContext) -> None:
|
|
108
112
|
"""
|
|
109
113
|
Hook called before the agent run starts.
|
|
110
|
-
|
|
114
|
+
|
|
111
115
|
Override to add custom initialization logic.
|
|
112
116
|
"""
|
|
113
117
|
pass
|
|
114
|
-
|
|
118
|
+
|
|
115
119
|
async def after_run(self, ctx: RunContext, result: RunResult) -> RunResult:
|
|
116
120
|
"""
|
|
117
121
|
Hook called after the agent run completes.
|
|
118
|
-
|
|
122
|
+
|
|
119
123
|
Override to add custom finalization logic.
|
|
120
124
|
Can modify the result before returning.
|
|
121
125
|
"""
|
|
122
126
|
return result
|
|
123
|
-
|
|
127
|
+
|
|
124
128
|
async def on_tool_call(self, ctx: RunContext, tool_name: str, tool_args: dict) -> None:
|
|
125
129
|
"""
|
|
126
130
|
Hook called before each tool execution.
|
|
127
|
-
|
|
131
|
+
|
|
128
132
|
Override to add custom logic (logging, validation, etc.).
|
|
129
133
|
"""
|
|
130
134
|
pass
|
|
131
|
-
|
|
132
|
-
async def on_tool_result(self, ctx: RunContext, tool_name: str, result:
|
|
135
|
+
|
|
136
|
+
async def on_tool_result(self, ctx: RunContext, tool_name: str, result: Any) -> Any:
|
|
133
137
|
"""
|
|
134
138
|
Hook called after each tool execution.
|
|
135
|
-
|
|
139
|
+
|
|
136
140
|
Override to transform or validate tool results.
|
|
137
141
|
Can return a modified result.
|
|
138
142
|
"""
|
|
139
143
|
return result
|
|
140
|
-
|
|
144
|
+
|
|
141
145
|
async def run(self, ctx: RunContext) -> RunResult:
|
|
142
146
|
"""
|
|
143
147
|
Execute the agent with tool calling support.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
1. Build messages with system prompt
|
|
147
|
-
2. Call LLM with tools
|
|
148
|
-
3. If tool calls, execute them and loop
|
|
149
|
-
4. If no tool calls, return final response
|
|
148
|
+
|
|
149
|
+
Uses run_agentic_loop internally with hooks for customization.
|
|
150
150
|
"""
|
|
151
|
-
logger.
|
|
152
|
-
|
|
151
|
+
logger.debug(f"[{self.key}] Starting run, input messages: {len(ctx.input_messages)}")
|
|
152
|
+
|
|
153
153
|
# Call before_run hook
|
|
154
154
|
await self.before_run(ctx)
|
|
155
|
-
|
|
155
|
+
|
|
156
156
|
# Get LLM client
|
|
157
157
|
llm = self.get_llm_client()
|
|
158
|
-
|
|
158
|
+
|
|
159
159
|
# Build messages with system prompt
|
|
160
|
+
# Use system_prompt_with_memory if available (from MemoryEnabledAgent mixin)
|
|
161
|
+
prompt = (
|
|
162
|
+
self.system_prompt_with_memory
|
|
163
|
+
if hasattr(self, 'system_prompt_with_memory')
|
|
164
|
+
else self.system_prompt
|
|
165
|
+
)
|
|
160
166
|
messages = [
|
|
161
|
-
{"role": "system", "content":
|
|
162
|
-
] + ctx.input_messages
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
# Call before_tool_call hook
|
|
200
|
-
await self.on_tool_call(ctx, tool_call["function"]["name"], json.loads(tool_call["function"]["arguments"]))
|
|
201
|
-
|
|
202
|
-
# Execute the tool
|
|
203
|
-
result = await self.tools.execute(
|
|
204
|
-
tool_call["function"]["name"],
|
|
205
|
-
json.loads(tool_call["function"]["arguments"]),
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
# Call after_tool_result hook
|
|
209
|
-
result = await self.on_tool_result(ctx, tool_call["function"]["name"], result)
|
|
210
|
-
|
|
211
|
-
tool_results.append({
|
|
212
|
-
"tool_call_id": tool_call["id"],
|
|
213
|
-
"result": result,
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
# Emit tool result event
|
|
217
|
-
await ctx.emit(EventType.TOOL_RESULT, {
|
|
218
|
-
"tool_name": tool_call["function"]["name"],
|
|
219
|
-
"tool_call_id": tool_call["id"],
|
|
220
|
-
"result": result,
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
# Add tool results to messages
|
|
224
|
-
for tr in tool_results:
|
|
225
|
-
messages.append({
|
|
226
|
-
"role": "tool",
|
|
227
|
-
"tool_call_id": tr["tool_call_id"],
|
|
228
|
-
"content": str(tr["result"]),
|
|
229
|
-
})
|
|
230
|
-
else:
|
|
231
|
-
# No tool calls - we have the final response
|
|
232
|
-
final_response = response.message["content"]
|
|
233
|
-
logger.info(f"[{self.key}] Final response received: {final_response[:100] if final_response else 'None'}...")
|
|
234
|
-
break
|
|
235
|
-
|
|
236
|
-
# Emit the final assistant message
|
|
237
|
-
if final_response:
|
|
238
|
-
logger.info(f"[{self.key}] Emitting ASSISTANT_MESSAGE event")
|
|
239
|
-
await ctx.emit(EventType.ASSISTANT_MESSAGE, {
|
|
240
|
-
"content": final_response,
|
|
241
|
-
})
|
|
242
|
-
logger.info(f"[{self.key}] Event emitted successfully")
|
|
243
|
-
else:
|
|
244
|
-
logger.warning(f"[{self.key}] No final response to emit!")
|
|
245
|
-
|
|
246
|
-
logger.info(f"[{self.key}] Returning RunResult")
|
|
167
|
+
{"role": "system", "content": prompt}
|
|
168
|
+
] + list(ctx.input_messages)
|
|
169
|
+
|
|
170
|
+
# Create tool executor that calls our hooks
|
|
171
|
+
async def execute_tool(name: str, args: dict) -> Any:
|
|
172
|
+
# Call before hook
|
|
173
|
+
await self.on_tool_call(ctx, name, args)
|
|
174
|
+
|
|
175
|
+
# Execute the tool
|
|
176
|
+
result = await self.tools.execute(name, args)
|
|
177
|
+
|
|
178
|
+
# Call after hook (can transform result)
|
|
179
|
+
result = await self.on_tool_result(ctx, name, result)
|
|
180
|
+
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
# Get tool schemas
|
|
184
|
+
tool_schemas = self.tools.to_openai_format() if self.tools.list_tools() else None
|
|
185
|
+
|
|
186
|
+
# Build LLM kwargs
|
|
187
|
+
llm_kwargs = {}
|
|
188
|
+
if self.temperature is not None:
|
|
189
|
+
llm_kwargs["temperature"] = self.temperature
|
|
190
|
+
|
|
191
|
+
# Run the agentic loop
|
|
192
|
+
# Note: agentic_loop emits ASSISTANT_MESSAGE for the final response
|
|
193
|
+
loop_result = await run_agentic_loop(
|
|
194
|
+
llm=llm,
|
|
195
|
+
messages=messages,
|
|
196
|
+
tools=tool_schemas,
|
|
197
|
+
execute_tool=execute_tool,
|
|
198
|
+
ctx=ctx,
|
|
199
|
+
model=self.model,
|
|
200
|
+
max_iterations=self.max_iterations,
|
|
201
|
+
emit_events=True,
|
|
202
|
+
**llm_kwargs,
|
|
203
|
+
)
|
|
204
|
+
|
|
247
205
|
result = RunResult(
|
|
248
|
-
final_output={"response":
|
|
249
|
-
final_messages=messages,
|
|
250
|
-
usage=
|
|
206
|
+
final_output={"response": loop_result.final_content},
|
|
207
|
+
final_messages=loop_result.messages,
|
|
208
|
+
usage=loop_result.usage,
|
|
251
209
|
)
|
|
252
|
-
|
|
210
|
+
|
|
253
211
|
# Call after_run hook
|
|
254
212
|
result = await self.after_run(ctx, result)
|
|
255
|
-
|
|
213
|
+
|
|
256
214
|
return result
|