agnt5 0.1.0__cp39-abi3-macosx_11_0_arm64.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.
- agnt5/__init__.py +307 -0
- agnt5/__pycache__/__init__.cpython-311.pyc +0 -0
- agnt5/__pycache__/agent.cpython-311.pyc +0 -0
- agnt5/__pycache__/context.cpython-311.pyc +0 -0
- agnt5/__pycache__/durable.cpython-311.pyc +0 -0
- agnt5/__pycache__/extraction.cpython-311.pyc +0 -0
- agnt5/__pycache__/memory.cpython-311.pyc +0 -0
- agnt5/__pycache__/reflection.cpython-311.pyc +0 -0
- agnt5/__pycache__/runtime.cpython-311.pyc +0 -0
- agnt5/__pycache__/task.cpython-311.pyc +0 -0
- agnt5/__pycache__/tool.cpython-311.pyc +0 -0
- agnt5/__pycache__/tracing.cpython-311.pyc +0 -0
- agnt5/__pycache__/types.cpython-311.pyc +0 -0
- agnt5/__pycache__/workflow.cpython-311.pyc +0 -0
- agnt5/_core.abi3.so +0 -0
- agnt5/agent.py +1086 -0
- agnt5/context.py +406 -0
- agnt5/durable.py +1050 -0
- agnt5/extraction.py +410 -0
- agnt5/llm/__init__.py +179 -0
- agnt5/llm/__pycache__/__init__.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/anthropic.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/azure.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/base.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/google.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/mistral.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/openai.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/together.cpython-311.pyc +0 -0
- agnt5/llm/anthropic.py +319 -0
- agnt5/llm/azure.py +348 -0
- agnt5/llm/base.py +315 -0
- agnt5/llm/google.py +373 -0
- agnt5/llm/mistral.py +330 -0
- agnt5/llm/model_registry.py +467 -0
- agnt5/llm/models.json +227 -0
- agnt5/llm/openai.py +334 -0
- agnt5/llm/together.py +377 -0
- agnt5/memory.py +746 -0
- agnt5/reflection.py +514 -0
- agnt5/runtime.py +699 -0
- agnt5/task.py +476 -0
- agnt5/testing.py +451 -0
- agnt5/tool.py +516 -0
- agnt5/tracing.py +624 -0
- agnt5/types.py +210 -0
- agnt5/workflow.py +897 -0
- agnt5-0.1.0.dist-info/METADATA +93 -0
- agnt5-0.1.0.dist-info/RECORD +49 -0
- agnt5-0.1.0.dist-info/WHEEL +4 -0
agnt5/agent.py
ADDED
|
@@ -0,0 +1,1086 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent implementation for the AGNT5 SDK.
|
|
3
|
+
|
|
4
|
+
Agents are the primary building blocks for conversational AI applications.
|
|
5
|
+
They manage conversations, tool usage, and state persistence.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import List, Optional, Union, AsyncIterator, Dict, Any, Callable
|
|
9
|
+
import asyncio
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
from .types import (
|
|
16
|
+
AgentConfig,
|
|
17
|
+
ExecutionContext,
|
|
18
|
+
ExecutionState,
|
|
19
|
+
)
|
|
20
|
+
from .tool import Tool
|
|
21
|
+
from .context import Context, get_context
|
|
22
|
+
from .memory import Memory
|
|
23
|
+
from .durable import durable
|
|
24
|
+
from .tracing import (
|
|
25
|
+
trace_agent_run, span, traced,
|
|
26
|
+
set_span_attribute, add_span_event, log, TraceLevel,
|
|
27
|
+
trace_tool_call, trace_llm_call, trace_memory_operation
|
|
28
|
+
)
|
|
29
|
+
from .llm import (
|
|
30
|
+
LanguageModel,
|
|
31
|
+
LanguageModelType,
|
|
32
|
+
LanguageModelResponse,
|
|
33
|
+
Message,
|
|
34
|
+
Role as MessageRole,
|
|
35
|
+
ToolCall,
|
|
36
|
+
ToolResult,
|
|
37
|
+
TokenUsage,
|
|
38
|
+
create_llm,
|
|
39
|
+
LLMError,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class MockLanguageModel(LanguageModel):
|
|
47
|
+
"""Mock LLM for testing and fallback."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, model: str, system_prompt: Optional[str] = None):
|
|
50
|
+
# Create a mock model type
|
|
51
|
+
super().__init__(
|
|
52
|
+
llm_model=LanguageModelType.GPT_4O, # Default mock model
|
|
53
|
+
system_prompt=system_prompt
|
|
54
|
+
)
|
|
55
|
+
self.mock_model = model
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def provider_name(self) -> str:
|
|
59
|
+
return "mock"
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def model_name(self) -> str:
|
|
63
|
+
return self.mock_model
|
|
64
|
+
|
|
65
|
+
async def generate(
|
|
66
|
+
self,
|
|
67
|
+
messages: List[Message],
|
|
68
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
69
|
+
max_tokens: int = 1024,
|
|
70
|
+
temperature: float = 0.7,
|
|
71
|
+
top_p: float = 1.0,
|
|
72
|
+
stream: bool = False,
|
|
73
|
+
**kwargs
|
|
74
|
+
) -> Union[LanguageModelResponse, AsyncIterator[LanguageModelResponse]]:
|
|
75
|
+
"""Generate a mock response."""
|
|
76
|
+
if stream:
|
|
77
|
+
return self._mock_stream_response(messages, tools)
|
|
78
|
+
else:
|
|
79
|
+
return self._mock_single_response(messages, tools)
|
|
80
|
+
|
|
81
|
+
async def _mock_single_response(self, messages: List[Message], tools: Optional[List[Dict[str, Any]]] = None) -> LanguageModelResponse:
|
|
82
|
+
"""Generate a single mock response."""
|
|
83
|
+
# Simulate processing time
|
|
84
|
+
await asyncio.sleep(0.1)
|
|
85
|
+
|
|
86
|
+
last_message = messages[-1] if messages else None
|
|
87
|
+
user_content = last_message.content if last_message else "Hello"
|
|
88
|
+
|
|
89
|
+
# Generate a mock response based on the input
|
|
90
|
+
mock_response = f"This is a mock response to: {user_content[:50]}..." if len(str(user_content)) > 50 else f"This is a mock response to: {user_content}"
|
|
91
|
+
|
|
92
|
+
# If tools are available, sometimes generate a mock tool call
|
|
93
|
+
tool_calls = None
|
|
94
|
+
if tools and len(tools) > 0 and "search" in str(user_content).lower():
|
|
95
|
+
tool_calls = [ToolCall(
|
|
96
|
+
id="mock_tool_call_123",
|
|
97
|
+
name=tools[0]["name"],
|
|
98
|
+
arguments={"query": str(user_content)[:100]}
|
|
99
|
+
)]
|
|
100
|
+
mock_response = "I'll search for that information."
|
|
101
|
+
|
|
102
|
+
return LanguageModelResponse(
|
|
103
|
+
message=mock_response,
|
|
104
|
+
usage=TokenUsage(prompt_tokens=50, completion_tokens=20, total_tokens=70),
|
|
105
|
+
tool_calls=tool_calls,
|
|
106
|
+
model=self.mock_model,
|
|
107
|
+
finish_reason="stop"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
async def _mock_stream_response(self, messages: List[Message], tools: Optional[List[Dict[str, Any]]] = None) -> AsyncIterator[LanguageModelResponse]:
|
|
111
|
+
"""Generate a streaming mock response."""
|
|
112
|
+
response_text = await self._mock_single_response(messages, tools)
|
|
113
|
+
words = response_text.message.split()
|
|
114
|
+
|
|
115
|
+
for i, word in enumerate(words):
|
|
116
|
+
await asyncio.sleep(0.05) # Simulate streaming delay
|
|
117
|
+
yield LanguageModelResponse(
|
|
118
|
+
message=word + " " if i < len(words) - 1 else word,
|
|
119
|
+
usage=TokenUsage(),
|
|
120
|
+
model=self.mock_model
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def convert_messages_to_provider_format(self, messages: List[Message]) -> List[Dict[str, Any]]:
|
|
124
|
+
"""Convert messages to mock format."""
|
|
125
|
+
return [{"role": msg.role.value, "content": msg.content} for msg in messages]
|
|
126
|
+
|
|
127
|
+
def convert_tools_to_provider_format(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
128
|
+
"""Convert tools to mock format."""
|
|
129
|
+
return tools
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class Agent:
|
|
133
|
+
"""
|
|
134
|
+
High-level agent that orchestrates conversations and tool usage.
|
|
135
|
+
|
|
136
|
+
Example:
|
|
137
|
+
```python
|
|
138
|
+
from agnt5 import Agent, Tool
|
|
139
|
+
|
|
140
|
+
# Define a tool
|
|
141
|
+
@tool
|
|
142
|
+
def search_web(query: str) -> str:
|
|
143
|
+
return f"Results for: {query}"
|
|
144
|
+
|
|
145
|
+
# Create an agent
|
|
146
|
+
agent = Agent(
|
|
147
|
+
name="research-assistant",
|
|
148
|
+
tools=[search_web],
|
|
149
|
+
system_prompt="You are a helpful research assistant."
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Run the agent
|
|
153
|
+
response = await agent.run("Find information about AGNT5")
|
|
154
|
+
```
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def __init__(
|
|
158
|
+
self,
|
|
159
|
+
name: str,
|
|
160
|
+
*,
|
|
161
|
+
description: Optional[str] = None,
|
|
162
|
+
model: str = "gpt-4o",
|
|
163
|
+
temperature: float = 0.7,
|
|
164
|
+
max_tokens: Optional[int] = None,
|
|
165
|
+
tools: Optional[List[Union[Tool, Callable]]] = None,
|
|
166
|
+
system_prompt: Optional[str] = None,
|
|
167
|
+
memory: Optional[Memory] = None,
|
|
168
|
+
config: Optional[AgentConfig] = None,
|
|
169
|
+
llm_provider: Optional[str] = None,
|
|
170
|
+
api_key: Optional[str] = None,
|
|
171
|
+
**llm_kwargs,
|
|
172
|
+
):
|
|
173
|
+
"""Initialize an Agent."""
|
|
174
|
+
if config:
|
|
175
|
+
self.config = config
|
|
176
|
+
else:
|
|
177
|
+
self.config = AgentConfig(
|
|
178
|
+
name=name,
|
|
179
|
+
description=description,
|
|
180
|
+
model=model,
|
|
181
|
+
temperature=temperature,
|
|
182
|
+
max_tokens=max_tokens,
|
|
183
|
+
tools=tools or [],
|
|
184
|
+
system_prompt=system_prompt,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
self.memory = memory or Memory()
|
|
188
|
+
self._tools: Dict[str, Tool] = {}
|
|
189
|
+
self._conversation: List[Message] = []
|
|
190
|
+
self._execution_context: Optional[ExecutionContext] = None
|
|
191
|
+
|
|
192
|
+
# Initialize LLM provider
|
|
193
|
+
self._llm = self._initialize_llm(
|
|
194
|
+
model=model,
|
|
195
|
+
provider=llm_provider,
|
|
196
|
+
api_key=api_key,
|
|
197
|
+
system_prompt=system_prompt,
|
|
198
|
+
**llm_kwargs
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Register tools
|
|
202
|
+
for tool in self.config.tools:
|
|
203
|
+
if isinstance(tool, Tool):
|
|
204
|
+
self._tools[tool.name] = tool
|
|
205
|
+
elif callable(tool):
|
|
206
|
+
# Convert function to Tool
|
|
207
|
+
from .tool import tool as tool_decorator
|
|
208
|
+
wrapped_tool = tool_decorator(tool)
|
|
209
|
+
self._tools[wrapped_tool.name] = wrapped_tool
|
|
210
|
+
|
|
211
|
+
def _initialize_llm(
|
|
212
|
+
self,
|
|
213
|
+
model: str,
|
|
214
|
+
provider: Optional[str] = None,
|
|
215
|
+
api_key: Optional[str] = None,
|
|
216
|
+
system_prompt: Optional[str] = None,
|
|
217
|
+
**kwargs
|
|
218
|
+
) -> LanguageModel:
|
|
219
|
+
"""Initialize the LLM provider using provider/model format."""
|
|
220
|
+
try:
|
|
221
|
+
# Parse the model string using LanguageModelType
|
|
222
|
+
model_type = LanguageModelType.from_string(model)
|
|
223
|
+
|
|
224
|
+
# Use explicit provider if given, otherwise use detected provider
|
|
225
|
+
resolved_provider = provider or model_type.get_provider()
|
|
226
|
+
resolved_model = model_type.value
|
|
227
|
+
|
|
228
|
+
logger.info(f"Using model '{resolved_model}' with provider '{resolved_provider}'")
|
|
229
|
+
|
|
230
|
+
# Create LLM instance
|
|
231
|
+
return create_llm(
|
|
232
|
+
provider=resolved_provider,
|
|
233
|
+
model=resolved_model,
|
|
234
|
+
api_key=api_key,
|
|
235
|
+
system_prompt=system_prompt,
|
|
236
|
+
**kwargs
|
|
237
|
+
)
|
|
238
|
+
except Exception as e:
|
|
239
|
+
logger.warning(f"Failed to initialize LLM provider {resolved_provider}: {e}")
|
|
240
|
+
logger.info("Falling back to mock LLM for testing")
|
|
241
|
+
return MockLanguageModel(model=model, system_prompt=system_prompt)
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def name(self) -> str:
|
|
245
|
+
return self.config.name
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def tools(self) -> Dict[str, Tool]:
|
|
249
|
+
return self._tools
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def llm(self) -> LanguageModel:
|
|
253
|
+
"""Get the LLM instance."""
|
|
254
|
+
return self._llm
|
|
255
|
+
|
|
256
|
+
def add_tool(self, tool: Union[Tool, Callable]) -> None:
|
|
257
|
+
"""Add a tool to the agent."""
|
|
258
|
+
if isinstance(tool, Tool):
|
|
259
|
+
self._tools[tool.name] = tool
|
|
260
|
+
elif callable(tool):
|
|
261
|
+
from .tool import tool as tool_decorator
|
|
262
|
+
wrapped_tool = tool_decorator(tool)
|
|
263
|
+
self._tools[wrapped_tool.name] = wrapped_tool
|
|
264
|
+
|
|
265
|
+
async def run(
|
|
266
|
+
self,
|
|
267
|
+
message: Union[str, Message],
|
|
268
|
+
*,
|
|
269
|
+
context: Optional[Context] = None,
|
|
270
|
+
stream: bool = False,
|
|
271
|
+
) -> Union[Message, AsyncIterator[Message]]:
|
|
272
|
+
"""
|
|
273
|
+
Run the agent with a message with comprehensive OpenAI-style tracing.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
message: Input message (string or Message object)
|
|
277
|
+
context: Optional execution context
|
|
278
|
+
stream: Whether to stream responses
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Response message or async iterator of messages if streaming
|
|
282
|
+
"""
|
|
283
|
+
with trace_agent_run(self.name) as trace_obj:
|
|
284
|
+
# Set trace metadata
|
|
285
|
+
trace_obj.metadata.update({
|
|
286
|
+
"agent.system_prompt": self.config.system_prompt[:100] + "..." if self.config.system_prompt and len(self.config.system_prompt) > 100 else self.config.system_prompt,
|
|
287
|
+
"agent.tools_count": len(self._tools),
|
|
288
|
+
"agent.memory_enabled": self.memory is not None,
|
|
289
|
+
"agent.model": self.config.model,
|
|
290
|
+
"agent.temperature": self.config.temperature,
|
|
291
|
+
"agent.durability_enabled": getattr(self.config, 'enable_durability', False),
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
with span("agent.run") as agent_span:
|
|
295
|
+
# Set span attributes
|
|
296
|
+
agent_span.set_attribute("agent.name", self.name)
|
|
297
|
+
agent_span.set_attribute("agent.model", self.config.model)
|
|
298
|
+
agent_span.set_attribute("agent.temperature", self.config.temperature)
|
|
299
|
+
agent_span.set_attribute("stream.enabled", stream)
|
|
300
|
+
|
|
301
|
+
# Convert string to Message if needed
|
|
302
|
+
if isinstance(message, str):
|
|
303
|
+
message = Message(
|
|
304
|
+
role=MessageRole.USER,
|
|
305
|
+
content=message,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
agent_span.set_attribute("input.length", len(message.content))
|
|
309
|
+
agent_span.set_attribute("input.role", message.role.value)
|
|
310
|
+
agent_span.set_attribute("tools.available", list(self._tools.keys()))
|
|
311
|
+
|
|
312
|
+
# Log start event
|
|
313
|
+
agent_span.add_event("agent.execution.started", {
|
|
314
|
+
"query_preview": message.content[:50] + "..." if len(message.content) > 50 else message.content,
|
|
315
|
+
"tools_available": list(self._tools.keys()),
|
|
316
|
+
"conversation_length": len(self._conversation)
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
# Get or create context
|
|
321
|
+
ctx = context or get_context()
|
|
322
|
+
|
|
323
|
+
# Add to conversation history
|
|
324
|
+
self._conversation.append(message)
|
|
325
|
+
|
|
326
|
+
# Store in memory if enabled
|
|
327
|
+
if self.memory:
|
|
328
|
+
with trace_memory_operation("add", "conversation"):
|
|
329
|
+
await self.memory.add(message)
|
|
330
|
+
|
|
331
|
+
# Create durable execution
|
|
332
|
+
if getattr(self.config, 'enable_durability', False):
|
|
333
|
+
result = await self._run_durable(message, ctx, stream)
|
|
334
|
+
else:
|
|
335
|
+
result = await self._run_direct(message, ctx, stream)
|
|
336
|
+
|
|
337
|
+
# Log success
|
|
338
|
+
agent_span.set_attribute("execution.status", "success")
|
|
339
|
+
if isinstance(result, Message):
|
|
340
|
+
agent_span.set_attribute("response.length", len(result.content))
|
|
341
|
+
agent_span.add_event("agent.execution.completed", {
|
|
342
|
+
"response_preview": result.content[:50] + "..." if len(result.content) > 50 else result.content,
|
|
343
|
+
"response_role": result.role.value
|
|
344
|
+
})
|
|
345
|
+
else:
|
|
346
|
+
agent_span.set_attribute("response.type", "stream")
|
|
347
|
+
agent_span.add_event("agent.execution.streaming", {
|
|
348
|
+
"stream_enabled": True
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
return result
|
|
352
|
+
|
|
353
|
+
except Exception as e:
|
|
354
|
+
# Handle error
|
|
355
|
+
agent_span.set_error(e)
|
|
356
|
+
agent_span.add_event("agent.execution.failed", {
|
|
357
|
+
"error_type": type(e).__name__,
|
|
358
|
+
"error_message": str(e)
|
|
359
|
+
})
|
|
360
|
+
log(TraceLevel.ERROR, f"Agent execution failed: {e}",
|
|
361
|
+
agent_name=self.name, error_type=type(e).__name__)
|
|
362
|
+
raise
|
|
363
|
+
|
|
364
|
+
@durable.function
|
|
365
|
+
async def _run_durable(
|
|
366
|
+
self,
|
|
367
|
+
message: Message,
|
|
368
|
+
context: Context,
|
|
369
|
+
stream: bool,
|
|
370
|
+
) -> Union[Message, AsyncIterator[Message]]:
|
|
371
|
+
"""Run with durability guarantees."""
|
|
372
|
+
# Implementation would integrate with the durable runtime
|
|
373
|
+
# For now, delegate to direct execution
|
|
374
|
+
return await self._run_direct(message, context, stream)
|
|
375
|
+
|
|
376
|
+
async def _run_direct(
|
|
377
|
+
self,
|
|
378
|
+
message: Message,
|
|
379
|
+
context: Context,
|
|
380
|
+
stream: bool,
|
|
381
|
+
) -> Union[Message, AsyncIterator[Message]]:
|
|
382
|
+
"""Direct execution without durability with detailed tracing."""
|
|
383
|
+
|
|
384
|
+
with span("agent.execution.direct") as execution_span:
|
|
385
|
+
execution_span.set_attribute("execution.type", "direct")
|
|
386
|
+
execution_span.set_attribute("execution.stream", stream)
|
|
387
|
+
|
|
388
|
+
# Prepare messages for LLM
|
|
389
|
+
with span("agent.prepare_messages") as prep_span:
|
|
390
|
+
messages = self._prepare_messages()
|
|
391
|
+
prep_span.set_attribute("messages.count", len(messages))
|
|
392
|
+
prep_span.set_attribute("conversation.length", len(self._conversation))
|
|
393
|
+
|
|
394
|
+
# Call LLM with tracing
|
|
395
|
+
response = await self._call_llm_with_tracing(messages, stream)
|
|
396
|
+
|
|
397
|
+
if stream:
|
|
398
|
+
execution_span.set_attribute("response.type", "stream")
|
|
399
|
+
return self._stream_response(response)
|
|
400
|
+
else:
|
|
401
|
+
# Process tool calls if any
|
|
402
|
+
if response.tool_calls:
|
|
403
|
+
execution_span.set_attribute("tools.called", True)
|
|
404
|
+
execution_span.set_attribute("tools.count", len(response.tool_calls))
|
|
405
|
+
|
|
406
|
+
tool_results = await self._execute_tools_with_tracing(response.tool_calls)
|
|
407
|
+
|
|
408
|
+
# Add tool results to conversation
|
|
409
|
+
for result in tool_results:
|
|
410
|
+
tool_message = Message(
|
|
411
|
+
role=MessageRole.TOOL,
|
|
412
|
+
content=json.dumps(result.output),
|
|
413
|
+
tool_call_id=result.tool_call_id,
|
|
414
|
+
)
|
|
415
|
+
self._conversation.append(tool_message)
|
|
416
|
+
|
|
417
|
+
# Get final response after tool execution
|
|
418
|
+
with span("agent.final_llm_call") as final_span:
|
|
419
|
+
messages = self._prepare_messages()
|
|
420
|
+
final_span.set_attribute("messages.count", len(messages))
|
|
421
|
+
final_span.set_attribute("after_tools", True)
|
|
422
|
+
response = await self._call_llm_with_tracing(messages, False)
|
|
423
|
+
else:
|
|
424
|
+
execution_span.set_attribute("tools.called", False)
|
|
425
|
+
|
|
426
|
+
# Add response to conversation
|
|
427
|
+
self._conversation.append(response)
|
|
428
|
+
|
|
429
|
+
# Store in memory
|
|
430
|
+
if self.memory:
|
|
431
|
+
with trace_memory_operation("add", "response"):
|
|
432
|
+
await self.memory.add(response)
|
|
433
|
+
|
|
434
|
+
execution_span.set_attribute("response.length", len(response.content))
|
|
435
|
+
execution_span.set_attribute("response.role", response.role.value)
|
|
436
|
+
|
|
437
|
+
return response
|
|
438
|
+
|
|
439
|
+
def _prepare_messages(self) -> List[Dict[str, Any]]:
|
|
440
|
+
"""Prepare messages for LLM API."""
|
|
441
|
+
messages = []
|
|
442
|
+
|
|
443
|
+
# Add system prompt
|
|
444
|
+
if self.config.system_prompt:
|
|
445
|
+
messages.append({
|
|
446
|
+
"role": "system",
|
|
447
|
+
"content": self.config.system_prompt,
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
# Add conversation history
|
|
451
|
+
for msg in self._conversation:
|
|
452
|
+
msg_dict = {
|
|
453
|
+
"role": msg.role.value,
|
|
454
|
+
"content": msg.content,
|
|
455
|
+
}
|
|
456
|
+
if msg.name:
|
|
457
|
+
msg_dict["name"] = msg.name
|
|
458
|
+
if msg.tool_calls:
|
|
459
|
+
msg_dict["tool_calls"] = [
|
|
460
|
+
{
|
|
461
|
+
"id": tc.id,
|
|
462
|
+
"type": "function",
|
|
463
|
+
"function": {
|
|
464
|
+
"name": tc.name,
|
|
465
|
+
"arguments": json.dumps(tc.arguments),
|
|
466
|
+
},
|
|
467
|
+
}
|
|
468
|
+
for tc in msg.tool_calls
|
|
469
|
+
]
|
|
470
|
+
if msg.tool_call_id:
|
|
471
|
+
msg_dict["tool_call_id"] = msg.tool_call_id
|
|
472
|
+
|
|
473
|
+
messages.append(msg_dict)
|
|
474
|
+
|
|
475
|
+
return messages
|
|
476
|
+
|
|
477
|
+
async def _call_llm_with_tracing(
|
|
478
|
+
self,
|
|
479
|
+
messages: List[Dict[str, Any]],
|
|
480
|
+
stream: bool,
|
|
481
|
+
) -> Message:
|
|
482
|
+
"""Call the LLM API with comprehensive tracing."""
|
|
483
|
+
# Calculate prompt length for tracing
|
|
484
|
+
prompt_length = sum(len(str(msg.get("content", ""))) for msg in messages)
|
|
485
|
+
|
|
486
|
+
with trace_llm_call(self.config.model, prompt_length) as llm_span:
|
|
487
|
+
llm_span.set_attribute("llm.temperature", self.config.temperature)
|
|
488
|
+
llm_span.set_attribute("llm.max_tokens", self.config.max_tokens or 0)
|
|
489
|
+
llm_span.set_attribute("llm.stream", stream)
|
|
490
|
+
llm_span.set_attribute("llm.messages_count", len(messages))
|
|
491
|
+
llm_span.set_attribute("llm.provider", self._llm.provider_name)
|
|
492
|
+
|
|
493
|
+
llm_span.add_event("llm.request.started", {
|
|
494
|
+
"model": self.config.model,
|
|
495
|
+
"prompt_length": prompt_length,
|
|
496
|
+
"temperature": self.config.temperature,
|
|
497
|
+
"provider": self._llm.provider_name
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
start_time = time.time()
|
|
501
|
+
|
|
502
|
+
try:
|
|
503
|
+
# Convert messages to LLM format
|
|
504
|
+
llm_messages = self._convert_to_llm_messages(messages)
|
|
505
|
+
|
|
506
|
+
# Prepare tools for LLM
|
|
507
|
+
tools = self._prepare_tools_for_llm() if self._tools else None
|
|
508
|
+
|
|
509
|
+
# Call the actual LLM
|
|
510
|
+
response = await self._llm.generate(
|
|
511
|
+
messages=llm_messages,
|
|
512
|
+
tools=tools,
|
|
513
|
+
max_tokens=self.config.max_tokens or 1024,
|
|
514
|
+
temperature=self.config.temperature,
|
|
515
|
+
stream=stream,
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
519
|
+
|
|
520
|
+
# Convert LLM response back to Message
|
|
521
|
+
message = Message(
|
|
522
|
+
role=MessageRole.ASSISTANT,
|
|
523
|
+
content=response.message,
|
|
524
|
+
tool_calls=response.tool_calls,
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
llm_span.set_attribute("llm.response.length", len(response.message))
|
|
528
|
+
llm_span.set_attribute("llm.duration_ms", duration_ms)
|
|
529
|
+
llm_span.set_attribute("llm.status", "success")
|
|
530
|
+
llm_span.set_attribute("llm.tokens.prompt", response.usage.prompt_tokens)
|
|
531
|
+
llm_span.set_attribute("llm.tokens.completion", response.usage.completion_tokens)
|
|
532
|
+
llm_span.set_attribute("llm.tokens.total", response.usage.total_tokens)
|
|
533
|
+
|
|
534
|
+
if response.tool_calls:
|
|
535
|
+
llm_span.set_attribute("llm.tool_calls.count", len(response.tool_calls))
|
|
536
|
+
|
|
537
|
+
llm_span.add_event("llm.request.completed", {
|
|
538
|
+
"response_length": len(response.message),
|
|
539
|
+
"duration_ms": duration_ms,
|
|
540
|
+
"tokens_prompt": response.usage.prompt_tokens,
|
|
541
|
+
"tokens_completion": response.usage.completion_tokens,
|
|
542
|
+
"tokens_total": response.usage.total_tokens,
|
|
543
|
+
"tool_calls": len(response.tool_calls) if response.tool_calls else 0,
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
return message
|
|
547
|
+
|
|
548
|
+
except LLMError as e:
|
|
549
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
550
|
+
|
|
551
|
+
llm_span.set_error(e)
|
|
552
|
+
llm_span.set_attribute("llm.duration_ms", duration_ms)
|
|
553
|
+
llm_span.set_attribute("llm.status", "error")
|
|
554
|
+
|
|
555
|
+
llm_span.add_event("llm.request.failed", {
|
|
556
|
+
"error_type": type(e).__name__,
|
|
557
|
+
"error_message": str(e),
|
|
558
|
+
"duration_ms": duration_ms,
|
|
559
|
+
"provider": e.provider,
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
log(TraceLevel.ERROR, f"LLM call failed: {e}",
|
|
563
|
+
model=self.config.model, provider=e.provider, error_type=type(e).__name__)
|
|
564
|
+
|
|
565
|
+
raise
|
|
566
|
+
except Exception as e:
|
|
567
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
568
|
+
|
|
569
|
+
llm_span.set_error(e)
|
|
570
|
+
llm_span.set_attribute("llm.duration_ms", duration_ms)
|
|
571
|
+
llm_span.set_attribute("llm.status", "error")
|
|
572
|
+
|
|
573
|
+
llm_span.add_event("llm.request.failed", {
|
|
574
|
+
"error_type": type(e).__name__,
|
|
575
|
+
"error_message": str(e),
|
|
576
|
+
"duration_ms": duration_ms
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
log(TraceLevel.ERROR, f"LLM call failed: {e}",
|
|
580
|
+
model=self.config.model, error_type=type(e).__name__)
|
|
581
|
+
|
|
582
|
+
raise
|
|
583
|
+
|
|
584
|
+
def _convert_to_llm_messages(self, messages: List[Dict[str, Any]]) -> List[Message]:
|
|
585
|
+
"""Convert agent messages to LLM Message format."""
|
|
586
|
+
llm_messages = []
|
|
587
|
+
|
|
588
|
+
for msg in messages:
|
|
589
|
+
role_str = msg.get("role", "user")
|
|
590
|
+
if role_str == "system":
|
|
591
|
+
role = MessageRole.SYSTEM
|
|
592
|
+
elif role_str == "user":
|
|
593
|
+
role = MessageRole.USER
|
|
594
|
+
elif role_str == "assistant":
|
|
595
|
+
role = MessageRole.ASSISTANT
|
|
596
|
+
elif role_str == "tool":
|
|
597
|
+
role = MessageRole.TOOL
|
|
598
|
+
else:
|
|
599
|
+
role = MessageRole.USER # Default fallback
|
|
600
|
+
|
|
601
|
+
# Convert tool calls if present
|
|
602
|
+
tool_calls = None
|
|
603
|
+
if msg.get("tool_calls"):
|
|
604
|
+
tool_calls = [
|
|
605
|
+
ToolCall(
|
|
606
|
+
id=tc["id"],
|
|
607
|
+
name=tc["function"]["name"],
|
|
608
|
+
arguments=json.loads(tc["function"]["arguments"]) if isinstance(tc["function"]["arguments"], str) else tc["function"]["arguments"]
|
|
609
|
+
)
|
|
610
|
+
for tc in msg["tool_calls"]
|
|
611
|
+
]
|
|
612
|
+
|
|
613
|
+
message = Message(
|
|
614
|
+
role=role,
|
|
615
|
+
content=msg.get("content", ""),
|
|
616
|
+
name=msg.get("name"),
|
|
617
|
+
tool_calls=tool_calls,
|
|
618
|
+
tool_call_id=msg.get("tool_call_id"),
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
llm_messages.append(message)
|
|
622
|
+
|
|
623
|
+
return llm_messages
|
|
624
|
+
|
|
625
|
+
def _prepare_tools_for_llm(self) -> List[Dict[str, Any]]:
|
|
626
|
+
"""Prepare tools in the format expected by LLM providers."""
|
|
627
|
+
tools = []
|
|
628
|
+
|
|
629
|
+
for tool in self._tools.values():
|
|
630
|
+
# Create tool schema
|
|
631
|
+
tool_schema = {
|
|
632
|
+
"name": tool.name,
|
|
633
|
+
"description": tool.description or f"Tool: {tool.name}",
|
|
634
|
+
"parameters": {
|
|
635
|
+
"type": "object",
|
|
636
|
+
"properties": {},
|
|
637
|
+
"required": []
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
# Add tool parameters if available
|
|
642
|
+
if hasattr(tool, 'parameters') and tool.parameters:
|
|
643
|
+
tool_schema["parameters"] = tool.parameters
|
|
644
|
+
elif hasattr(tool, 'config') and hasattr(tool.config, 'parameters'):
|
|
645
|
+
tool_schema["parameters"] = tool.config.parameters
|
|
646
|
+
|
|
647
|
+
tools.append(tool_schema)
|
|
648
|
+
|
|
649
|
+
return tools
|
|
650
|
+
|
|
651
|
+
async def _call_llm(
|
|
652
|
+
self,
|
|
653
|
+
messages: List[Dict[str, Any]],
|
|
654
|
+
stream: bool,
|
|
655
|
+
) -> Message:
|
|
656
|
+
"""Call the LLM API (legacy method for backward compatibility)."""
|
|
657
|
+
return await self._call_llm_with_tracing(messages, stream)
|
|
658
|
+
|
|
659
|
+
async def _stream_response(
|
|
660
|
+
self,
|
|
661
|
+
response: Any,
|
|
662
|
+
) -> AsyncIterator[Message]:
|
|
663
|
+
"""Stream response messages."""
|
|
664
|
+
# Placeholder implementation
|
|
665
|
+
yield Message(
|
|
666
|
+
role=MessageRole.ASSISTANT,
|
|
667
|
+
content="Streaming not yet implemented.",
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
async def _execute_tools_with_tracing(self, tool_calls: List[ToolCall]) -> List[ToolResult]:
|
|
671
|
+
"""Execute tool calls with comprehensive tracing."""
|
|
672
|
+
results = []
|
|
673
|
+
|
|
674
|
+
with span("agent.tools.execute") as tools_span:
|
|
675
|
+
tools_span.set_attribute("tools.count", len(tool_calls))
|
|
676
|
+
tools_span.add_event("tools.execution.started", {
|
|
677
|
+
"tool_calls": [call.name for call in tool_calls],
|
|
678
|
+
"total_count": len(tool_calls)
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
for i, call in enumerate(tool_calls):
|
|
682
|
+
with trace_tool_call(call.name) as tool_span:
|
|
683
|
+
tool_span.set_attribute("tool.name", call.name)
|
|
684
|
+
tool_span.set_attribute("tool.index", i)
|
|
685
|
+
tool_span.set_attribute("tool.call_id", call.id)
|
|
686
|
+
tool_span.set_attribute("tool.arguments_count", len(call.arguments))
|
|
687
|
+
|
|
688
|
+
tool = self._tools.get(call.name)
|
|
689
|
+
if not tool:
|
|
690
|
+
error_msg = f"Tool '{call.name}' not found"
|
|
691
|
+
tool_span.set_attribute("tool.status", "not_found")
|
|
692
|
+
tool_span.add_event("tool.execution.failed", {
|
|
693
|
+
"error_type": "ToolNotFound",
|
|
694
|
+
"error_message": error_msg,
|
|
695
|
+
"available_tools": list(self._tools.keys())
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
results.append(ToolResult(
|
|
699
|
+
tool_call_id=call.id,
|
|
700
|
+
output=None,
|
|
701
|
+
error=error_msg,
|
|
702
|
+
))
|
|
703
|
+
continue
|
|
704
|
+
|
|
705
|
+
tool_span.add_event("tool.execution.started", {
|
|
706
|
+
"tool_name": call.name,
|
|
707
|
+
"arguments": str(call.arguments)[:200] + "..." if len(str(call.arguments)) > 200 else str(call.arguments)
|
|
708
|
+
})
|
|
709
|
+
|
|
710
|
+
start_time = time.time()
|
|
711
|
+
|
|
712
|
+
try:
|
|
713
|
+
# Execute tool
|
|
714
|
+
output = await tool.invoke(**call.arguments)
|
|
715
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
716
|
+
|
|
717
|
+
tool_span.set_attribute("tool.status", "success")
|
|
718
|
+
tool_span.set_attribute("tool.duration_ms", duration_ms)
|
|
719
|
+
tool_span.set_attribute("tool.output_length", len(str(output)) if output else 0)
|
|
720
|
+
|
|
721
|
+
tool_span.add_event("tool.execution.completed", {
|
|
722
|
+
"result_type": type(output).__name__,
|
|
723
|
+
"result_preview": str(output)[:100] + "..." if len(str(output)) > 100 else str(output),
|
|
724
|
+
"duration_ms": duration_ms
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
results.append(ToolResult(
|
|
728
|
+
tool_call_id=call.id,
|
|
729
|
+
output=output,
|
|
730
|
+
))
|
|
731
|
+
|
|
732
|
+
except Exception as e:
|
|
733
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
734
|
+
|
|
735
|
+
tool_span.set_error(e)
|
|
736
|
+
tool_span.set_attribute("tool.status", "error")
|
|
737
|
+
tool_span.set_attribute("tool.duration_ms", duration_ms)
|
|
738
|
+
|
|
739
|
+
tool_span.add_event("tool.execution.failed", {
|
|
740
|
+
"error_type": type(e).__name__,
|
|
741
|
+
"error_message": str(e),
|
|
742
|
+
"duration_ms": duration_ms
|
|
743
|
+
})
|
|
744
|
+
|
|
745
|
+
logger.error(f"Tool execution failed: {e}")
|
|
746
|
+
log(TraceLevel.ERROR, f"Tool {call.name} failed: {e}",
|
|
747
|
+
tool_name=call.name, error_type=type(e).__name__)
|
|
748
|
+
|
|
749
|
+
results.append(ToolResult(
|
|
750
|
+
tool_call_id=call.id,
|
|
751
|
+
output=None,
|
|
752
|
+
error=str(e),
|
|
753
|
+
))
|
|
754
|
+
|
|
755
|
+
# Record overall tool execution results
|
|
756
|
+
successful_tools = sum(1 for result in results if result.error is None)
|
|
757
|
+
failed_tools = len(results) - successful_tools
|
|
758
|
+
|
|
759
|
+
tools_span.set_attribute("tools.successful", successful_tools)
|
|
760
|
+
tools_span.set_attribute("tools.failed", failed_tools)
|
|
761
|
+
tools_span.add_event("tools.execution.completed", {
|
|
762
|
+
"total_executed": len(results),
|
|
763
|
+
"successful": successful_tools,
|
|
764
|
+
"failed": failed_tools
|
|
765
|
+
})
|
|
766
|
+
|
|
767
|
+
return results
|
|
768
|
+
|
|
769
|
+
async def _execute_tools(self, tool_calls: List[ToolCall]) -> List[ToolResult]:
|
|
770
|
+
"""Execute tool calls (legacy method for backward compatibility)."""
|
|
771
|
+
return await self._execute_tools_with_tracing(tool_calls)
|
|
772
|
+
|
|
773
|
+
@traced("agent.clear_conversation")
|
|
774
|
+
async def clear_conversation(self) -> None:
|
|
775
|
+
"""Clear the conversation history and persist state."""
|
|
776
|
+
with span("agent.conversation.clear") as clear_span:
|
|
777
|
+
conversation_length = len(self._conversation)
|
|
778
|
+
clear_span.set_attribute("conversation.length_before", conversation_length)
|
|
779
|
+
|
|
780
|
+
self._conversation.clear()
|
|
781
|
+
|
|
782
|
+
clear_span.set_attribute("conversation.length_after", 0)
|
|
783
|
+
clear_span.add_event("conversation.cleared", {
|
|
784
|
+
"messages_removed": conversation_length
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
# Save state with tracing
|
|
788
|
+
await self._save_with_tracing()
|
|
789
|
+
|
|
790
|
+
log(TraceLevel.INFO, f"Cleared conversation with {conversation_length} messages",
|
|
791
|
+
agent_name=self.name, messages_removed=conversation_length)
|
|
792
|
+
|
|
793
|
+
def get_conversation(self) -> List[Message]:
|
|
794
|
+
"""Get the conversation history."""
|
|
795
|
+
return self._conversation.copy()
|
|
796
|
+
|
|
797
|
+
@traced("agent.save_state")
|
|
798
|
+
async def save_state(self) -> Dict[str, Any]:
|
|
799
|
+
"""Save agent state for persistence with tracing."""
|
|
800
|
+
with span("agent.state.save") as save_span:
|
|
801
|
+
save_span.set_attribute("agent.name", self.name)
|
|
802
|
+
save_span.set_attribute("conversation.length", len(self._conversation))
|
|
803
|
+
save_span.set_attribute("memory.enabled", self.memory is not None)
|
|
804
|
+
|
|
805
|
+
save_span.add_event("state.serialization.started")
|
|
806
|
+
|
|
807
|
+
# Serialize conversation
|
|
808
|
+
conversation_data = []
|
|
809
|
+
for msg in self._conversation:
|
|
810
|
+
msg_data = {
|
|
811
|
+
"role": msg.role.value,
|
|
812
|
+
"content": msg.content,
|
|
813
|
+
"name": msg.name,
|
|
814
|
+
"tool_calls": [tc.__dict__ for tc in msg.tool_calls] if msg.tool_calls else None,
|
|
815
|
+
"tool_call_id": msg.tool_call_id,
|
|
816
|
+
"metadata": msg.metadata,
|
|
817
|
+
"timestamp": msg.timestamp.isoformat(),
|
|
818
|
+
}
|
|
819
|
+
conversation_data.append(msg_data)
|
|
820
|
+
|
|
821
|
+
# Export memory if available
|
|
822
|
+
memory_data = None
|
|
823
|
+
if self.memory:
|
|
824
|
+
with trace_memory_operation("export", "full_state"):
|
|
825
|
+
memory_data = await self.memory.export()
|
|
826
|
+
|
|
827
|
+
state = {
|
|
828
|
+
"config": self.config.__dict__,
|
|
829
|
+
"conversation": conversation_data,
|
|
830
|
+
"memory": memory_data,
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
# Calculate state size for monitoring
|
|
834
|
+
state_size = len(str(state))
|
|
835
|
+
save_span.set_attribute("state.size_bytes", state_size)
|
|
836
|
+
save_span.add_event("state.serialization.completed", {
|
|
837
|
+
"state_size_bytes": state_size,
|
|
838
|
+
"conversation_messages": len(conversation_data),
|
|
839
|
+
"memory_included": memory_data is not None
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
log(TraceLevel.INFO, f"Saved agent state: {state_size} bytes",
|
|
843
|
+
agent_name=self.name, state_size=state_size)
|
|
844
|
+
|
|
845
|
+
return state
|
|
846
|
+
|
|
847
|
+
@traced("agent.load_state")
|
|
848
|
+
async def load_state(self, state: Dict[str, Any]) -> None:
|
|
849
|
+
"""Load agent state from persistence with tracing."""
|
|
850
|
+
with span("agent.state.load") as load_span:
|
|
851
|
+
state_size = len(str(state))
|
|
852
|
+
load_span.set_attribute("agent.name", self.name)
|
|
853
|
+
load_span.set_attribute("state.size_bytes", state_size)
|
|
854
|
+
|
|
855
|
+
conversation_data = state.get("conversation", [])
|
|
856
|
+
load_span.set_attribute("conversation.messages_to_load", len(conversation_data))
|
|
857
|
+
load_span.set_attribute("memory.data_present", "memory" in state and state["memory"] is not None)
|
|
858
|
+
|
|
859
|
+
load_span.add_event("state.deserialization.started", {
|
|
860
|
+
"state_size_bytes": state_size,
|
|
861
|
+
"conversation_messages": len(conversation_data)
|
|
862
|
+
})
|
|
863
|
+
|
|
864
|
+
# Restore conversation
|
|
865
|
+
with span("agent.conversation.restore") as conv_span:
|
|
866
|
+
self._conversation.clear()
|
|
867
|
+
for i, msg_data in enumerate(conversation_data):
|
|
868
|
+
try:
|
|
869
|
+
msg = Message(
|
|
870
|
+
role=MessageRole(msg_data["role"]),
|
|
871
|
+
content=msg_data["content"],
|
|
872
|
+
name=msg_data.get("name"),
|
|
873
|
+
tool_call_id=msg_data.get("tool_call_id"),
|
|
874
|
+
metadata=msg_data.get("metadata", {}),
|
|
875
|
+
)
|
|
876
|
+
if msg_data.get("tool_calls"):
|
|
877
|
+
msg.tool_calls = [
|
|
878
|
+
ToolCall(**tc) for tc in msg_data["tool_calls"]
|
|
879
|
+
]
|
|
880
|
+
self._conversation.append(msg)
|
|
881
|
+
except Exception as e:
|
|
882
|
+
conv_span.add_event("message.restore.failed", {
|
|
883
|
+
"message_index": i,
|
|
884
|
+
"error": str(e)
|
|
885
|
+
})
|
|
886
|
+
log(TraceLevel.WARN, f"Failed to restore message {i}: {e}",
|
|
887
|
+
agent_name=self.name, message_index=i)
|
|
888
|
+
|
|
889
|
+
conv_span.set_attribute("messages.restored", len(self._conversation))
|
|
890
|
+
|
|
891
|
+
# Restore memory
|
|
892
|
+
if self.memory and state.get("memory"):
|
|
893
|
+
with trace_memory_operation("import", "full_state"):
|
|
894
|
+
await self.memory.import_data(state["memory"])
|
|
895
|
+
|
|
896
|
+
load_span.add_event("state.deserialization.completed", {
|
|
897
|
+
"conversation_messages_restored": len(self._conversation),
|
|
898
|
+
"memory_restored": self.memory is not None and state.get("memory") is not None
|
|
899
|
+
})
|
|
900
|
+
|
|
901
|
+
log(TraceLevel.INFO, f"Loaded agent state: {len(self._conversation)} messages",
|
|
902
|
+
agent_name=self.name, messages_restored=len(self._conversation))
|
|
903
|
+
|
|
904
|
+
async def _save_with_tracing(self) -> None:
|
|
905
|
+
"""Save agent state with tracing (placeholder for actual persistence)."""
|
|
906
|
+
with span("agent.save") as save_span:
|
|
907
|
+
# This would call actual persistence layer
|
|
908
|
+
save_span.add_event("save.placeholder", {
|
|
909
|
+
"note": "Actual persistence implementation pending"
|
|
910
|
+
})
|
|
911
|
+
# For now, just simulate save operation
|
|
912
|
+
await asyncio.sleep(0.01)
|
|
913
|
+
|
|
914
|
+
# Reflection capabilities
|
|
915
|
+
async def reflect_on_response(
|
|
916
|
+
self,
|
|
917
|
+
user_query: str,
|
|
918
|
+
agent_response: str,
|
|
919
|
+
level: str = "analytical"
|
|
920
|
+
) -> Dict[str, Any]:
|
|
921
|
+
"""
|
|
922
|
+
Reflect on the quality of a response.
|
|
923
|
+
|
|
924
|
+
Args:
|
|
925
|
+
user_query: The original user query
|
|
926
|
+
agent_response: The agent's response
|
|
927
|
+
level: Reflection level ('surface', 'analytical', 'metacognitive')
|
|
928
|
+
|
|
929
|
+
Returns:
|
|
930
|
+
Reflection results with scores and insights
|
|
931
|
+
"""
|
|
932
|
+
from .reflection import reflect_on_response
|
|
933
|
+
|
|
934
|
+
with trace_agent_run(f"{self.name}_reflection") as trace_obj:
|
|
935
|
+
trace_obj.metadata.update({
|
|
936
|
+
"reflection.type": "response_quality",
|
|
937
|
+
"reflection.level": level,
|
|
938
|
+
"agent.name": self.name,
|
|
939
|
+
})
|
|
940
|
+
|
|
941
|
+
return await reflect_on_response(
|
|
942
|
+
user_query=user_query,
|
|
943
|
+
agent_response=agent_response,
|
|
944
|
+
level=level,
|
|
945
|
+
agent=self
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
async def reflect_on_conversation(self, level: str = "analytical") -> Dict[str, Any]:
|
|
949
|
+
"""
|
|
950
|
+
Reflect on the entire conversation so far.
|
|
951
|
+
|
|
952
|
+
Args:
|
|
953
|
+
level: Reflection level
|
|
954
|
+
|
|
955
|
+
Returns:
|
|
956
|
+
Reflection results for the conversation
|
|
957
|
+
"""
|
|
958
|
+
if not self._conversation:
|
|
959
|
+
return {
|
|
960
|
+
"insights": ["No conversation to reflect on"],
|
|
961
|
+
"improvements": ["Start a conversation first"],
|
|
962
|
+
"overall_score": 0.0
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
# Get the last user message and agent response
|
|
966
|
+
user_messages = [msg for msg in self._conversation if msg.role == MessageRole.USER]
|
|
967
|
+
agent_messages = [msg for msg in self._conversation if msg.role == MessageRole.ASSISTANT]
|
|
968
|
+
|
|
969
|
+
if not user_messages or not agent_messages:
|
|
970
|
+
return {
|
|
971
|
+
"insights": ["Incomplete conversation"],
|
|
972
|
+
"improvements": ["Complete the conversation cycle"],
|
|
973
|
+
"overall_score": 2.0
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
# Reflect on the last exchange
|
|
977
|
+
last_user_msg = user_messages[-1].content
|
|
978
|
+
last_agent_msg = agent_messages[-1].content
|
|
979
|
+
|
|
980
|
+
return await self.reflect_on_response(
|
|
981
|
+
user_query=str(last_user_msg),
|
|
982
|
+
agent_response=str(last_agent_msg),
|
|
983
|
+
level=level
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
async def self_evaluate(self, criteria: Optional[List[str]] = None) -> Dict[str, Any]:
|
|
987
|
+
"""
|
|
988
|
+
Perform self-evaluation based on recent performance.
|
|
989
|
+
|
|
990
|
+
Args:
|
|
991
|
+
criteria: Optional list of criteria to evaluate
|
|
992
|
+
|
|
993
|
+
Returns:
|
|
994
|
+
Self-evaluation results
|
|
995
|
+
"""
|
|
996
|
+
from .reflection import ReflectionEngine, ReflectionType, ReflectionLevel
|
|
997
|
+
|
|
998
|
+
engine = ReflectionEngine()
|
|
999
|
+
|
|
1000
|
+
# Gather context from recent conversation
|
|
1001
|
+
context = {
|
|
1002
|
+
"conversation_length": len(self._conversation),
|
|
1003
|
+
"tools_available": list(self._tools.keys()),
|
|
1004
|
+
"model": self.config.model,
|
|
1005
|
+
"recent_messages": [
|
|
1006
|
+
{"role": msg.role.value, "content": str(msg.content)[:200]}
|
|
1007
|
+
for msg in self._conversation[-5:] # Last 5 messages
|
|
1008
|
+
]
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
# Perform performance reflection
|
|
1012
|
+
result = await engine.reflect(
|
|
1013
|
+
ReflectionType.PERFORMANCE_REVIEW,
|
|
1014
|
+
ReflectionLevel.METACOGNITIVE,
|
|
1015
|
+
context,
|
|
1016
|
+
agent=self
|
|
1017
|
+
)
|
|
1018
|
+
|
|
1019
|
+
return result.to_dict()
|
|
1020
|
+
|
|
1021
|
+
async def improve_response(
|
|
1022
|
+
self,
|
|
1023
|
+
original_query: str,
|
|
1024
|
+
original_response: str,
|
|
1025
|
+
feedback: Optional[str] = None
|
|
1026
|
+
) -> str:
|
|
1027
|
+
"""
|
|
1028
|
+
Generate an improved response based on reflection.
|
|
1029
|
+
|
|
1030
|
+
Args:
|
|
1031
|
+
original_query: The original user query
|
|
1032
|
+
original_response: The original response
|
|
1033
|
+
feedback: Optional user feedback
|
|
1034
|
+
|
|
1035
|
+
Returns:
|
|
1036
|
+
Improved response
|
|
1037
|
+
"""
|
|
1038
|
+
# First, reflect on the original response
|
|
1039
|
+
reflection = await self.reflect_on_response(
|
|
1040
|
+
user_query=original_query,
|
|
1041
|
+
agent_response=original_response,
|
|
1042
|
+
level="analytical"
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
# Build improvement prompt
|
|
1046
|
+
improvements = reflection.get("improvements", [])
|
|
1047
|
+
insights = reflection.get("insights", [])
|
|
1048
|
+
|
|
1049
|
+
improvement_prompt = f"""Based on reflection, please provide an improved response to the original query.
|
|
1050
|
+
|
|
1051
|
+
Original Query: {original_query}
|
|
1052
|
+
Original Response: {original_response}
|
|
1053
|
+
|
|
1054
|
+
Reflection Insights:
|
|
1055
|
+
{chr(10).join(f"- {insight}" for insight in insights)}
|
|
1056
|
+
|
|
1057
|
+
Suggested Improvements:
|
|
1058
|
+
{chr(10).join(f"- {improvement}" for improvement in improvements)}
|
|
1059
|
+
|
|
1060
|
+
{f"User Feedback: {feedback}" if feedback else ""}
|
|
1061
|
+
|
|
1062
|
+
Please provide an improved response that addresses the identified issues:"""
|
|
1063
|
+
|
|
1064
|
+
# Generate improved response
|
|
1065
|
+
response = await self.run(improvement_prompt)
|
|
1066
|
+
return response.content if hasattr(response, 'content') else str(response)
|
|
1067
|
+
|
|
1068
|
+
async def learn_from_error(self, error_description: str, context: Optional[str] = None) -> Dict[str, Any]:
|
|
1069
|
+
"""
|
|
1070
|
+
Learn from an error by analyzing what went wrong.
|
|
1071
|
+
|
|
1072
|
+
Args:
|
|
1073
|
+
error_description: Description of the error
|
|
1074
|
+
context: Optional context where the error occurred
|
|
1075
|
+
|
|
1076
|
+
Returns:
|
|
1077
|
+
Learning insights and prevention strategies
|
|
1078
|
+
"""
|
|
1079
|
+
from .reflection import analyze_errors
|
|
1080
|
+
|
|
1081
|
+
return await analyze_errors(
|
|
1082
|
+
errors=[error_description],
|
|
1083
|
+
attempted_solutions=[context] if context else [],
|
|
1084
|
+
level="metacognitive",
|
|
1085
|
+
agent=self
|
|
1086
|
+
)
|