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.
Files changed (49) hide show
  1. agnt5/__init__.py +307 -0
  2. agnt5/__pycache__/__init__.cpython-311.pyc +0 -0
  3. agnt5/__pycache__/agent.cpython-311.pyc +0 -0
  4. agnt5/__pycache__/context.cpython-311.pyc +0 -0
  5. agnt5/__pycache__/durable.cpython-311.pyc +0 -0
  6. agnt5/__pycache__/extraction.cpython-311.pyc +0 -0
  7. agnt5/__pycache__/memory.cpython-311.pyc +0 -0
  8. agnt5/__pycache__/reflection.cpython-311.pyc +0 -0
  9. agnt5/__pycache__/runtime.cpython-311.pyc +0 -0
  10. agnt5/__pycache__/task.cpython-311.pyc +0 -0
  11. agnt5/__pycache__/tool.cpython-311.pyc +0 -0
  12. agnt5/__pycache__/tracing.cpython-311.pyc +0 -0
  13. agnt5/__pycache__/types.cpython-311.pyc +0 -0
  14. agnt5/__pycache__/workflow.cpython-311.pyc +0 -0
  15. agnt5/_core.abi3.so +0 -0
  16. agnt5/agent.py +1086 -0
  17. agnt5/context.py +406 -0
  18. agnt5/durable.py +1050 -0
  19. agnt5/extraction.py +410 -0
  20. agnt5/llm/__init__.py +179 -0
  21. agnt5/llm/__pycache__/__init__.cpython-311.pyc +0 -0
  22. agnt5/llm/__pycache__/anthropic.cpython-311.pyc +0 -0
  23. agnt5/llm/__pycache__/azure.cpython-311.pyc +0 -0
  24. agnt5/llm/__pycache__/base.cpython-311.pyc +0 -0
  25. agnt5/llm/__pycache__/google.cpython-311.pyc +0 -0
  26. agnt5/llm/__pycache__/mistral.cpython-311.pyc +0 -0
  27. agnt5/llm/__pycache__/openai.cpython-311.pyc +0 -0
  28. agnt5/llm/__pycache__/together.cpython-311.pyc +0 -0
  29. agnt5/llm/anthropic.py +319 -0
  30. agnt5/llm/azure.py +348 -0
  31. agnt5/llm/base.py +315 -0
  32. agnt5/llm/google.py +373 -0
  33. agnt5/llm/mistral.py +330 -0
  34. agnt5/llm/model_registry.py +467 -0
  35. agnt5/llm/models.json +227 -0
  36. agnt5/llm/openai.py +334 -0
  37. agnt5/llm/together.py +377 -0
  38. agnt5/memory.py +746 -0
  39. agnt5/reflection.py +514 -0
  40. agnt5/runtime.py +699 -0
  41. agnt5/task.py +476 -0
  42. agnt5/testing.py +451 -0
  43. agnt5/tool.py +516 -0
  44. agnt5/tracing.py +624 -0
  45. agnt5/types.py +210 -0
  46. agnt5/workflow.py +897 -0
  47. agnt5-0.1.0.dist-info/METADATA +93 -0
  48. agnt5-0.1.0.dist-info/RECORD +49 -0
  49. 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
+ )