ambivo-agents 1.3.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,880 @@
1
+ # ambivo_agents/core/base.py
2
+ """
3
+ Enhanced BaseAgent with built-in auto-context session management
4
+ No need for separate AutoContextAgent - context is built right into BaseAgent
5
+ """
6
+
7
+ import asyncio
8
+ import uuid
9
+ import time
10
+ import tempfile
11
+ import os
12
+ from pathlib import Path
13
+ from abc import ABC, abstractmethod
14
+ from dataclasses import dataclass, field
15
+ from datetime import datetime, timedelta
16
+ from enum import Enum
17
+ from typing import Dict, List, Any, Optional, Callable, Tuple
18
+ from concurrent.futures import ThreadPoolExecutor
19
+ import logging
20
+
21
+ # Docker imports
22
+ try:
23
+ import docker
24
+
25
+ DOCKER_AVAILABLE = True
26
+ except ImportError:
27
+ DOCKER_AVAILABLE = False
28
+
29
+
30
+ class AgentRole(Enum):
31
+ ASSISTANT = "assistant"
32
+ PROXY = "proxy"
33
+ ANALYST = "analyst"
34
+ RESEARCHER = "researcher"
35
+ COORDINATOR = "coordinator"
36
+ VALIDATOR = "validator"
37
+ CODE_EXECUTOR = "code_executor"
38
+
39
+
40
+ class MessageType(Enum):
41
+ USER_INPUT = "user_input"
42
+ AGENT_RESPONSE = "agent_response"
43
+ SYSTEM_MESSAGE = "system_message"
44
+ TOOL_CALL = "tool_call"
45
+ TOOL_RESPONSE = "tool_response"
46
+ ERROR = "error"
47
+ STATUS_UPDATE = "status_update"
48
+
49
+
50
+ @dataclass
51
+ class AgentMessage:
52
+ id: str
53
+ sender_id: str
54
+ recipient_id: Optional[str]
55
+ content: str
56
+ message_type: MessageType
57
+ metadata: Dict[str, Any] = field(default_factory=dict)
58
+ timestamp: datetime = field(default_factory=datetime.now)
59
+ session_id: Optional[str] = None
60
+ conversation_id: Optional[str] = None
61
+
62
+ def to_dict(self) -> Dict[str, Any]:
63
+ """Convert to dictionary for serialization"""
64
+ return {
65
+ 'id': self.id,
66
+ 'sender_id': self.sender_id,
67
+ 'recipient_id': self.recipient_id,
68
+ 'content': self.content,
69
+ 'message_type': self.message_type.value,
70
+ 'metadata': self.metadata,
71
+ 'timestamp': self.timestamp.isoformat(),
72
+ 'session_id': self.session_id,
73
+ 'conversation_id': self.conversation_id
74
+ }
75
+
76
+ @classmethod
77
+ def from_dict(cls, data: Dict[str, Any]) -> 'AgentMessage':
78
+ """Create from dictionary"""
79
+ return cls(
80
+ id=data['id'],
81
+ sender_id=data['sender_id'],
82
+ recipient_id=data.get('recipient_id'),
83
+ content=data['content'],
84
+ message_type=MessageType(data['message_type']),
85
+ metadata=data.get('metadata', {}),
86
+ timestamp=datetime.fromisoformat(data['timestamp']),
87
+ session_id=data.get('session_id'),
88
+ conversation_id=data.get('conversation_id')
89
+ )
90
+
91
+
92
+ @dataclass
93
+ class AgentTool:
94
+ name: str
95
+ description: str
96
+ function: Callable
97
+ parameters_schema: Dict[str, Any]
98
+ requires_approval: bool = False
99
+ timeout: int = 30
100
+
101
+
102
+ @dataclass
103
+ class ExecutionContext:
104
+ session_id: str
105
+ conversation_id: str
106
+ user_id: str
107
+ tenant_id: str
108
+ metadata: Dict[str, Any] = field(default_factory=dict)
109
+
110
+
111
+ @dataclass
112
+ class AgentContext:
113
+ """
114
+ Built-in context for every BaseAgent instance
115
+ Automatically created when agent is instantiated
116
+ """
117
+ session_id: str
118
+ conversation_id: str
119
+ user_id: str
120
+ tenant_id: str
121
+ agent_id: str
122
+ created_at: datetime = field(default_factory=datetime.now)
123
+ metadata: Dict[str, Any] = field(default_factory=dict)
124
+
125
+ def to_execution_context(self) -> ExecutionContext:
126
+ """Convert to ExecutionContext for operations"""
127
+ return ExecutionContext(
128
+ session_id=self.session_id,
129
+ conversation_id=self.conversation_id,
130
+ user_id=self.user_id,
131
+ tenant_id=self.tenant_id,
132
+ metadata=self.metadata
133
+ )
134
+
135
+ def update_metadata(self, **kwargs):
136
+ """Update context metadata"""
137
+ self.metadata.update(kwargs)
138
+
139
+ def __str__(self):
140
+ return f"AgentContext(session={self.session_id}, user={self.user_id})"
141
+
142
+
143
+ @dataclass
144
+ class ProviderConfig:
145
+ """Configuration for LLM providers"""
146
+ name: str
147
+ model_name: str
148
+ priority: int
149
+ max_requests_per_minute: int = 60
150
+ max_requests_per_hour: int = 3600
151
+ cooldown_minutes: int = 5
152
+ request_count: int = 0
153
+ error_count: int = 0
154
+ last_request_time: Optional[datetime] = None
155
+ last_error_time: Optional[datetime] = None
156
+ is_available: bool = True
157
+
158
+
159
+ class ProviderTracker:
160
+ """Tracks provider usage and availability"""
161
+
162
+ def __init__(self):
163
+ self.providers: Dict[str, ProviderConfig] = {}
164
+ self.current_provider: Optional[str] = None
165
+ self.last_rotation_time: Optional[datetime] = None
166
+ self.rotation_interval_minutes: int = 30
167
+
168
+ def record_request(self, provider_name: str):
169
+ """Record a request to a provider"""
170
+ if provider_name in self.providers:
171
+ provider = self.providers[provider_name]
172
+ provider.request_count += 1
173
+ provider.last_request_time = datetime.now()
174
+
175
+ def record_error(self, provider_name: str, error_message: str):
176
+ """Record an error for a provider"""
177
+ if provider_name in self.providers:
178
+ provider = self.providers[provider_name]
179
+ provider.error_count += 1
180
+ provider.last_error_time = datetime.now()
181
+
182
+ if provider.error_count >= 3:
183
+ provider.is_available = False
184
+
185
+ def is_provider_available(self, provider_name: str) -> bool:
186
+ """Check if a provider is available"""
187
+ if provider_name not in self.providers:
188
+ return False
189
+
190
+ provider = self.providers[provider_name]
191
+
192
+ if not provider.is_available:
193
+ if (provider.last_error_time and
194
+ datetime.now() - provider.last_error_time > timedelta(minutes=provider.cooldown_minutes)):
195
+ provider.is_available = True
196
+ provider.error_count = 0
197
+ else:
198
+ return False
199
+
200
+ now = datetime.now()
201
+ if provider.last_request_time:
202
+ time_since_last = (now - provider.last_request_time).total_seconds()
203
+
204
+ if time_since_last > 3600:
205
+ provider.request_count = 0
206
+
207
+ if provider.request_count >= provider.max_requests_per_hour:
208
+ return False
209
+
210
+ return True
211
+
212
+ def get_best_available_provider(self) -> Optional[str]:
213
+ """Get the best available provider"""
214
+ available_providers = [
215
+ (name, config) for name, config in self.providers.items()
216
+ if self.is_provider_available(name)
217
+ ]
218
+
219
+ if not available_providers:
220
+ return None
221
+
222
+ available_providers.sort(key=lambda x: (x[1].priority, x[1].error_count))
223
+ return available_providers[0][0]
224
+
225
+
226
+ class DockerCodeExecutor:
227
+ """Secure code execution using Docker containers"""
228
+
229
+ def __init__(self, config: Dict[str, Any]):
230
+ self.config = config
231
+ self.work_dir = config.get("work_dir", '/opt/ambivo/work_dir')
232
+ self.docker_images = config.get("docker_images", ["sgosain/amb-ubuntu-python-public-pod"])
233
+ self.timeout = config.get("timeout", 60)
234
+ self.default_image = self.docker_images[0] if self.docker_images else "sgosain/amb-ubuntu-python-public-pod"
235
+
236
+ if DOCKER_AVAILABLE:
237
+ try:
238
+ self.docker_client = docker.from_env()
239
+ self.docker_client.ping()
240
+ self.available = True
241
+ except Exception as e:
242
+ self.available = False
243
+ else:
244
+ self.available = False
245
+
246
+ def execute_code(self, code: str, language: str = "python", files: Dict[str, str] = None) -> Dict[str, Any]:
247
+ """Execute code in Docker container"""
248
+ if not self.available:
249
+ return {
250
+ 'success': False,
251
+ 'error': 'Docker not available',
252
+ 'language': language
253
+ }
254
+
255
+ try:
256
+ with tempfile.TemporaryDirectory() as temp_dir:
257
+ temp_path = Path(temp_dir)
258
+
259
+ if language == "python":
260
+ code_file = temp_path / "code.py"
261
+ code_file.write_text(code)
262
+ cmd = ["python", "/workspace/code.py"]
263
+ elif language == "bash":
264
+ code_file = temp_path / "script.sh"
265
+ code_file.write_text(code)
266
+ cmd = ["bash", "/workspace/script.sh"]
267
+ else:
268
+ raise ValueError(f"Unsupported language: {language}")
269
+
270
+ if files:
271
+ for filename, content in files.items():
272
+ file_path = temp_path / filename
273
+ file_path.write_text(content)
274
+
275
+ container_config = {
276
+ 'image': self.default_image,
277
+ 'command': cmd,
278
+ 'volumes': {str(temp_path): {'bind': '/workspace', 'mode': 'rw'}},
279
+ 'working_dir': '/workspace',
280
+ 'mem_limit': '512m',
281
+ 'network_disabled': True,
282
+ 'remove': True,
283
+ 'stdout': True,
284
+ 'stderr': True
285
+ }
286
+
287
+ start_time = time.time()
288
+ container = self.docker_client.containers.run(**container_config)
289
+ execution_time = time.time() - start_time
290
+
291
+ output = container.decode('utf-8') if isinstance(container, bytes) else str(container)
292
+
293
+ return {
294
+ 'success': True,
295
+ 'output': output,
296
+ 'execution_time': execution_time,
297
+ 'language': language
298
+ }
299
+
300
+ except docker.errors.ContainerError as e:
301
+ return {
302
+ 'success': False,
303
+ 'error': f"Container error: {e.stderr.decode('utf-8') if e.stderr else 'Unknown error'}",
304
+ 'exit_code': e.exit_status,
305
+ 'language': language
306
+ }
307
+ except Exception as e:
308
+ return {
309
+ 'success': False,
310
+ 'error': str(e),
311
+ 'language': language
312
+ }
313
+
314
+
315
+ class BaseAgent(ABC):
316
+ """
317
+ Enhanced BaseAgent with built-in auto-context session management
318
+ Every agent automatically gets a context with session_id, user_id, etc.
319
+ """
320
+
321
+ def __init__(self,
322
+ agent_id: str = None,
323
+ role: AgentRole = AgentRole.ASSISTANT,
324
+ user_id: str = None,
325
+ tenant_id: str = "default",
326
+ session_metadata: Dict[str, Any] = None,
327
+ memory_manager=None,
328
+ llm_service=None,
329
+ config: Dict[str, Any] = None,
330
+ name: str = None,
331
+ description: str = None,
332
+ auto_configure: bool = True,
333
+ session_id: str = None,
334
+ conversation_id: str = None,
335
+ **kwargs):
336
+
337
+ # Auto-generate agent_id if not provided
338
+ if agent_id is None:
339
+ agent_id = f"agent_{str(uuid.uuid4())[:8]}"
340
+
341
+ self.agent_id = agent_id
342
+ self.role = role
343
+ self.name = name or f"{role.value}_{agent_id[:8]}"
344
+ self.description = description or f"Agent with role: {role.value}"
345
+
346
+ # Load config if not provided and auto-configure is enabled
347
+ if config is None and auto_configure:
348
+ try:
349
+ from ..config.loader import load_config
350
+ config = load_config()
351
+ except Exception as e:
352
+ logging.warning(f"Could not load config for auto-configuration: {e}")
353
+ config = {}
354
+
355
+ self.config = config or {}
356
+
357
+ self.context = self._create_agent_context(user_id, tenant_id,
358
+ session_metadata,
359
+ session_id,
360
+ conversation_id)
361
+
362
+ # Auto-configure memory if not provided and auto-configure is enabled
363
+ if memory_manager is None and auto_configure:
364
+ try:
365
+ from ..core.memory import create_redis_memory_manager
366
+ self.memory = create_redis_memory_manager(
367
+ agent_id=agent_id,
368
+ redis_config=None # Will load from config automatically
369
+ )
370
+ logging.info(f"Auto-configured memory for agent {agent_id}")
371
+ except Exception as e:
372
+ logging.error(f"Failed to auto-configure memory for {agent_id}: {e}")
373
+ self.memory = None
374
+ else:
375
+ self.memory = memory_manager
376
+
377
+ # Auto-configure LLM service if not provided and auto-configure is enabled
378
+ if llm_service is None and auto_configure:
379
+ try:
380
+ from ..core.llm import create_multi_provider_llm_service
381
+ self.llm_service = create_multi_provider_llm_service()
382
+ logging.info(f"Auto-configured LLM service for agent {agent_id}")
383
+ except Exception as e:
384
+ logging.warning(f"Could not auto-configure LLM for {agent_id}: {e}")
385
+ self.llm_service = None
386
+ else:
387
+ self.llm_service = llm_service
388
+
389
+ self.tools = kwargs.get('tools', [])
390
+ self.active = True
391
+
392
+ # Initialize executor
393
+ self.executor = ThreadPoolExecutor(max_workers=4)
394
+
395
+ logging.info(f"๐Ÿš€ BaseAgent created with auto-context:")
396
+ logging.info(f" ๐Ÿค– Agent: {self.agent_id}")
397
+ logging.info(f" ๐Ÿ“‹ Session: {self.context.session_id}")
398
+ logging.info(f" ๐Ÿ‘ค User: {self.context.user_id}")
399
+
400
+ def _create_agent_context(self,
401
+ user_id: str = None,
402
+ tenant_id: str = "default",
403
+ session_metadata: Dict[str, Any] = None,
404
+ session_id: str = None,
405
+ conversation_id: str = None
406
+ ) -> AgentContext:
407
+ """Create auto-context for this agent instance"""
408
+
409
+ # Auto-generate user_id if not provided
410
+ if user_id is None:
411
+ user_id = f"user_{str(uuid.uuid4())[:8]}"
412
+
413
+ if session_id and conversation_id:
414
+ final_session_id = session_id
415
+ final_conversation_id = conversation_id
416
+ else:
417
+ final_session_id = f"session_{str(uuid.uuid4())[:8]}"
418
+ final_conversation_id = f"conv_{str(uuid.uuid4())[:8]}"
419
+
420
+ return AgentContext(
421
+ session_id=final_session_id,
422
+ conversation_id=final_conversation_id,
423
+ user_id=user_id,
424
+ tenant_id=tenant_id,
425
+ agent_id=self.agent_id,
426
+ metadata=session_metadata or {}
427
+ )
428
+
429
+
430
+
431
+ @classmethod
432
+ def create(cls,
433
+ agent_id: str = None,
434
+ user_id: str = None,
435
+ tenant_id: str = "default",
436
+ session_metadata: Dict[str, Any] = None,
437
+ session_id: str = None,
438
+ conversation_id: str = None,
439
+ **kwargs) -> Tuple['BaseAgent', AgentContext]:
440
+ """
441
+ ๐ŸŒŸ DEFAULT: Create agent and return both agent and context
442
+ This is the RECOMMENDED way to create agents with auto-context
443
+
444
+ Usage:
445
+ agent, context = KnowledgeBaseAgent.create(user_id="john")
446
+ print(f"Session: {context.session_id}")
447
+ print(f"User: {context.user_id}")
448
+ """
449
+ if agent_id is None:
450
+ agent_id = f"{cls.__name__.lower()}_{str(uuid.uuid4())[:8]}"
451
+
452
+ agent = cls(
453
+ agent_id=agent_id,
454
+ user_id=user_id,
455
+ tenant_id=tenant_id,
456
+ session_metadata=session_metadata,
457
+ session_id=session_id,
458
+ conversation_id=conversation_id,
459
+ auto_configure=True,
460
+ **kwargs
461
+ )
462
+
463
+ return agent, agent.context
464
+
465
+ @classmethod
466
+ def create_simple(cls,
467
+ agent_id: str = None,
468
+ user_id: str = None,
469
+ tenant_id: str = "default",
470
+ session_metadata: Dict[str, Any] = None,
471
+ **kwargs) -> 'BaseAgent':
472
+ """
473
+ Create agent with auto-context (returns agent only)
474
+
475
+ โš ๏ธ LEGACY: Use create() instead for explicit context handling
476
+
477
+ Usage:
478
+ agent = KnowledgeBaseAgent.create_simple(user_id="john")
479
+ print(f"Session: {agent.context.session_id}") # Context still available
480
+ """
481
+ if agent_id is None:
482
+ agent_id = f"{cls.__name__.lower()}_{str(uuid.uuid4())[:8]}"
483
+
484
+ return cls(
485
+ agent_id=agent_id,
486
+ user_id=user_id,
487
+ tenant_id=tenant_id,
488
+ session_metadata=session_metadata,
489
+ auto_configure=True,
490
+ **kwargs
491
+ )
492
+
493
+ @classmethod
494
+ def create_advanced(cls,
495
+ agent_id: str,
496
+ memory_manager,
497
+ llm_service=None,
498
+ config: Dict[str, Any] = None,
499
+ user_id: str = None,
500
+ tenant_id: str = "default",
501
+ **kwargs):
502
+ """
503
+ Advanced factory method for explicit dependency injection
504
+
505
+ Usage:
506
+ memory = create_redis_memory_manager("custom_agent")
507
+ llm = create_multi_provider_llm_service()
508
+ agent = YouTubeDownloadAgent.create_advanced("my_id", memory, llm)
509
+ """
510
+ return cls(
511
+ agent_id=agent_id,
512
+ memory_manager=memory_manager,
513
+ llm_service=llm_service,
514
+ config=config,
515
+ user_id=user_id,
516
+ tenant_id=tenant_id,
517
+ auto_configure=False, # Disable auto-config when using advanced mode
518
+ **kwargs
519
+ )
520
+
521
+ # ๐Ÿ”ง CONTEXT MANAGEMENT METHODS
522
+
523
+ def get_context(self) -> AgentContext:
524
+ """Get the agent's auto-generated context"""
525
+ return self.context
526
+
527
+ def get_execution_context(self) -> ExecutionContext:
528
+ """Get ExecutionContext for operations that need it"""
529
+ return self.context.to_execution_context()
530
+
531
+ def update_context_metadata(self, **kwargs):
532
+ """Update context metadata"""
533
+ self.context.update_metadata(**kwargs)
534
+
535
+ # ๐Ÿง  CONVERSATION HISTORY METHODS (Built into BaseAgent)
536
+
537
+ async def get_conversation_history(self,
538
+ limit: int = None,
539
+ include_metadata: bool = True) -> List[Dict[str, Any]]:
540
+ """
541
+ Get conversation history for this agent's session
542
+
543
+ Args:
544
+ limit: Maximum number of messages to return (None = all)
545
+ include_metadata: Whether to include message metadata
546
+
547
+ Returns:
548
+ List of conversation messages with context
549
+ """
550
+ try:
551
+ if not self.memory:
552
+ logging.warning(f"No memory available for agent {self.agent_id}")
553
+ return []
554
+
555
+ # Get history using session_id from auto-context
556
+ history = self.memory.get_recent_messages(
557
+ limit=limit or 10,
558
+ conversation_id=self.context.conversation_id
559
+ )
560
+
561
+ # Add context information to each message
562
+ enriched_history = []
563
+ for msg in history:
564
+ if include_metadata:
565
+ msg_with_context = {
566
+ **msg,
567
+ 'session_id': self.context.session_id,
568
+ 'user_id': self.context.user_id,
569
+ 'agent_id': self.agent_id,
570
+ 'conversation_id': self.context.conversation_id
571
+ }
572
+ else:
573
+ msg_with_context = msg
574
+
575
+ enriched_history.append(msg_with_context)
576
+
577
+ return enriched_history
578
+
579
+ except Exception as e:
580
+ logging.error(f"Failed to get conversation history for {self.agent_id}: {e}")
581
+ return []
582
+
583
+ async def add_to_conversation_history(self,
584
+ message: str,
585
+ message_type: str = "user",
586
+ metadata: Dict[str, Any] = None) -> bool:
587
+ """
588
+ Add a message to conversation history
589
+
590
+ Args:
591
+ message: The message content
592
+ message_type: Type of message ("user", "agent", "system")
593
+ metadata: Additional metadata for the message
594
+
595
+ Returns:
596
+ True if successfully added, False otherwise
597
+ """
598
+ try:
599
+ if not self.memory:
600
+ logging.warning(f"No memory available for agent {self.agent_id}")
601
+ return False
602
+
603
+ # Create AgentMessage for storage
604
+ agent_message = AgentMessage(
605
+ id=str(uuid.uuid4()),
606
+ sender_id=self.agent_id if message_type == "agent" else f"{message_type}_sender",
607
+ recipient_id=None,
608
+ content=message,
609
+ message_type=MessageType.AGENT_RESPONSE if message_type == "agent" else MessageType.USER_INPUT,
610
+ session_id=self.context.session_id,
611
+ conversation_id=self.context.conversation_id,
612
+ metadata={
613
+ 'type': message_type,
614
+ 'user_id': self.context.user_id,
615
+ 'agent_id': self.agent_id,
616
+ **(metadata or {})
617
+ }
618
+ )
619
+
620
+ # Store in memory
621
+ self.memory.store_message(agent_message)
622
+ return True
623
+
624
+ except Exception as e:
625
+ logging.error(f"Failed to add to conversation history for {self.agent_id}: {e}")
626
+ return False
627
+
628
+ async def clear_conversation_history(self) -> bool:
629
+ """
630
+ Clear conversation history for this agent's session
631
+
632
+ Returns:
633
+ True if successfully cleared, False otherwise
634
+ """
635
+ try:
636
+ if not self.memory:
637
+ logging.warning(f"No memory available for agent {self.agent_id}")
638
+ return False
639
+
640
+ self.memory.clear_memory(self.context.conversation_id)
641
+ logging.info(f"Cleared conversation history for session {self.context.session_id}")
642
+ return True
643
+
644
+ except Exception as e:
645
+ logging.error(f"Failed to clear conversation history for {self.agent_id}: {e}")
646
+ return False
647
+
648
+ async def get_conversation_summary(self) -> Dict[str, Any]:
649
+ """
650
+ Get a summary of the current conversation
651
+
652
+ Returns:
653
+ Dictionary with conversation statistics and summary
654
+ """
655
+ try:
656
+ history = await self.get_conversation_history(include_metadata=True)
657
+
658
+ if not history:
659
+ return {
660
+ 'total_messages': 0,
661
+ 'user_messages': 0,
662
+ 'agent_messages': 0,
663
+ 'session_duration': '0 minutes',
664
+ 'first_message': None,
665
+ 'last_message': None,
666
+ 'session_id': self.context.session_id
667
+ }
668
+
669
+ # Analyze conversation
670
+ total_messages = len(history)
671
+ user_messages = len([msg for msg in history if msg.get('message_type') == 'user_input'])
672
+ agent_messages = len([msg for msg in history if msg.get('message_type') == 'agent_response'])
673
+
674
+ # Calculate session duration
675
+ first_msg_time = self.context.created_at
676
+ last_msg_time = datetime.now()
677
+ duration = last_msg_time - first_msg_time
678
+ duration_minutes = int(duration.total_seconds() / 60)
679
+
680
+ return {
681
+ 'total_messages': total_messages,
682
+ 'user_messages': user_messages,
683
+ 'agent_messages': agent_messages,
684
+ 'session_duration': f"{duration_minutes} minutes",
685
+ 'first_message': history[0].get('content', '')[:100] + "..." if len(
686
+ history[0].get('content', '')) > 100 else history[0].get('content', '') if history else None,
687
+ 'last_message': history[-1].get('content', '')[:100] + "..." if len(
688
+ history[-1].get('content', '')) > 100 else history[-1].get('content', '') if history else None,
689
+ 'session_id': self.context.session_id,
690
+ 'conversation_id': self.context.conversation_id,
691
+ 'user_id': self.context.user_id
692
+ }
693
+
694
+ except Exception as e:
695
+ logging.error(f"Failed to get conversation summary for {self.agent_id}: {e}")
696
+ return {
697
+ 'error': str(e),
698
+ 'session_id': self.context.session_id
699
+ }
700
+
701
+ async def _with_auto_context(self, operation_name: str, **kwargs) -> Dict[str, Any]:
702
+ """
703
+ Internal method that automatically applies context to operations
704
+ All agent operations should use this to ensure context is applied
705
+ """
706
+ execution_context = self.get_execution_context()
707
+
708
+ # Add context info to operation metadata
709
+ operation_metadata = {
710
+ 'session_id': self.context.session_id,
711
+ 'user_id': self.context.user_id,
712
+ 'tenant_id': self.context.tenant_id,
713
+ 'operation': operation_name,
714
+ 'timestamp': datetime.now().isoformat(),
715
+ **kwargs
716
+ }
717
+
718
+ # Update context metadata
719
+ self.context.update_metadata(**operation_metadata)
720
+
721
+ return {
722
+ 'execution_context': execution_context,
723
+ 'operation_metadata': operation_metadata
724
+ }
725
+
726
+ # ๐Ÿงน SESSION CLEANUP
727
+
728
+ async def cleanup_session(self) -> bool:
729
+ """Cleanup the agent's session and resources"""
730
+ try:
731
+ session_id = self.context.session_id
732
+
733
+ # Clear memory for this session
734
+
735
+ if hasattr(self, 'memory') and self.memory:
736
+ try:
737
+ # Hack temporraily commented memmort
738
+ #self.memory.clear_memory(self.context.conversation_id)
739
+ logging.info(f"๐Ÿงน Cleared memory for session {session_id}")
740
+ except Exception as e:
741
+ logging.warning(f"โš ๏ธ Could not clear memory: {e}")
742
+
743
+ # Shutdown executor
744
+ if hasattr(self, 'executor') and self.executor:
745
+ try:
746
+ self.executor.shutdown(wait=True)
747
+ logging.info(f"๐Ÿ›‘ Shutdown executor for session {session_id}")
748
+ except Exception as e:
749
+ logging.warning(f"โš ๏ธ Could not shutdown executor: {e}")
750
+
751
+ logging.info(f"โœ… Session {session_id} cleaned up successfully")
752
+ return True
753
+
754
+ except Exception as e:
755
+ logging.error(f"โŒ Error cleaning up session: {e}")
756
+ return False
757
+
758
+ # ๐Ÿ› ๏ธ TOOL MANAGEMENT
759
+
760
+ def add_tool(self, tool: AgentTool):
761
+ """Add a tool to the agent"""
762
+ self.tools.append(tool)
763
+
764
+ def get_tool(self, tool_name: str) -> Optional[AgentTool]:
765
+ """Get a tool by name"""
766
+ return next((tool for tool in self.tools if tool.name == tool_name), None)
767
+
768
+ async def execute_tool(self, tool_name: str, parameters: Dict[str, Any]) -> Dict[str, Any]:
769
+ """Execute a tool with auto-context"""
770
+ tool = self.get_tool(tool_name)
771
+ if not tool:
772
+ raise ValueError(f"Tool {tool_name} not found")
773
+
774
+ # Apply auto-context to tool execution
775
+ context_data = await self._with_auto_context("tool_execution",
776
+ tool_name=tool_name,
777
+ parameters=parameters)
778
+
779
+ try:
780
+ if asyncio.iscoroutinefunction(tool.function):
781
+ result = await tool.function(**parameters)
782
+ else:
783
+ result = await asyncio.get_event_loop().run_in_executor(
784
+ self.executor, tool.function, **parameters
785
+ )
786
+
787
+ return {
788
+ 'success': True,
789
+ 'result': result,
790
+ 'session_id': self.context.session_id,
791
+ 'context': context_data
792
+ }
793
+ except Exception as e:
794
+ return {
795
+ 'success': False,
796
+ 'error': str(e),
797
+ 'session_id': self.context.session_id
798
+ }
799
+
800
+ def create_response(self,
801
+ content: str,
802
+ recipient_id: str,
803
+ message_type: MessageType = MessageType.AGENT_RESPONSE,
804
+ metadata: Dict[str, Any] = None,
805
+ session_id: str = None,
806
+ conversation_id: str = None) -> AgentMessage:
807
+ """
808
+ Create a response message with auto-context
809
+ Uses agent's context if session_id/conversation_id not provided
810
+ """
811
+ return AgentMessage(
812
+ id=str(uuid.uuid4()),
813
+ sender_id=self.agent_id,
814
+ recipient_id=recipient_id,
815
+ content=content,
816
+ message_type=message_type,
817
+ metadata=metadata or {},
818
+ session_id=session_id or self.context.session_id, # ๐ŸŽฏ Auto-context!
819
+ conversation_id=conversation_id or self.context.conversation_id # ๐ŸŽฏ Auto-context!
820
+ )
821
+
822
+ # ๐Ÿ“จ ABSTRACT METHOD (must be implemented by subclasses)
823
+
824
+ @abstractmethod
825
+ async def process_message(self, message: AgentMessage, context: ExecutionContext = None) -> AgentMessage:
826
+ """
827
+ Process incoming message and return response
828
+ Uses agent's auto-context if context not provided
829
+ """
830
+ if context is None:
831
+ context = self.get_execution_context()
832
+
833
+ # Subclasses must implement this
834
+ pass
835
+
836
+ def register_agent(self, agent: 'BaseAgent'):
837
+ """Default implementation - only ProxyAgent should override this"""
838
+ return False
839
+
840
+
841
+ # ๐ŸŽฏ CONTEXT MANAGER FOR AUTO-CONTEXT AGENTS
842
+
843
+ class AgentSession:
844
+ """
845
+ Context manager for BaseAgent instances with automatic cleanup
846
+
847
+ Usage:
848
+ async with AgentSession(KnowledgeBaseAgent, user_id="john") as agent:
849
+ result = await agent.get_answer("kb", "query")
850
+ print(f"Session: {agent.context.session_id}")
851
+ # Agent automatically cleaned up
852
+ """
853
+
854
+ def __init__(self,
855
+ agent_class,
856
+ user_id: str = None,
857
+ tenant_id: str = "default",
858
+ session_metadata: Dict[str, Any] = None,
859
+ **agent_kwargs):
860
+ self.agent_class = agent_class
861
+ self.user_id = user_id
862
+ self.tenant_id = tenant_id
863
+ self.session_metadata = session_metadata
864
+ self.agent_kwargs = agent_kwargs
865
+ self.agent = None
866
+
867
+ async def __aenter__(self):
868
+ """Create agent when entering context"""
869
+ self.agent = self.agent_class.create_simple(
870
+ user_id=self.user_id,
871
+ tenant_id=self.tenant_id,
872
+ session_metadata=self.session_metadata,
873
+ **self.agent_kwargs
874
+ )
875
+ return self.agent
876
+
877
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
878
+ """Cleanup agent when exiting context"""
879
+ if self.agent:
880
+ await self.agent.cleanup_session()