ambivo-agents 1.0.1__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,617 @@
1
+ # ambivo_agents/agents/moderator.py
2
+ """
3
+ ModeratorAgent: Complete intelligent orchestrator that routes queries to specialized agents
4
+ FIXED VERSION - All methods implemented correctly
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import uuid
10
+ import time
11
+ import logging
12
+ from typing import Dict, List, Any, Optional, Union
13
+ from datetime import datetime
14
+ from dataclasses import dataclass
15
+
16
+ from ..core.base import BaseAgent, AgentRole, AgentMessage, MessageType, ExecutionContext
17
+ from ..config.loader import load_config, get_config_section
18
+ from ..core.history import BaseAgentHistoryMixin, ContextType
19
+
20
+
21
+ @dataclass
22
+ class AgentResponse:
23
+ """Response from an individual agent"""
24
+ agent_type: str
25
+ content: str
26
+ success: bool
27
+ execution_time: float
28
+ metadata: Dict[str, Any]
29
+ error: Optional[str] = None
30
+
31
+
32
+ class ModeratorAgent(BaseAgent, BaseAgentHistoryMixin):
33
+ """
34
+ Complete moderator agent that intelligently routes queries to specialized agents
35
+ Users only interact with this agent, which handles everything behind the scenes
36
+ """
37
+
38
+ def __init__(self, agent_id: str = None, memory_manager=None, llm_service=None,
39
+ enabled_agents: List[str] = None, **kwargs):
40
+ if agent_id is None:
41
+ agent_id = f"moderator_{str(uuid.uuid4())[:8]}"
42
+
43
+ super().__init__(
44
+ agent_id=agent_id,
45
+ role=AgentRole.COORDINATOR,
46
+ memory_manager=memory_manager,
47
+ llm_service=llm_service,
48
+ name="Moderator Agent",
49
+ description="Intelligent orchestrator that routes queries to specialized agents",
50
+ **kwargs
51
+ )
52
+
53
+ # Initialize history mixin
54
+ self.setup_history_mixin()
55
+
56
+ # Load configuration
57
+ self.config = load_config()
58
+ self.capabilities = self.config.get('agent_capabilities', {})
59
+ self.moderator_config = self.config.get('moderator', {})
60
+
61
+ # Initialize available agents based on config and enabled list
62
+ self.enabled_agents = enabled_agents or self._get_default_enabled_agents()
63
+ self.specialized_agents = {}
64
+ self.agent_routing_patterns = {}
65
+
66
+ # Initialize specialized agents
67
+ self._initialize_specialized_agents()
68
+
69
+ # Setup routing intelligence
70
+ self._setup_routing_patterns()
71
+
72
+ logging.info(f"ModeratorAgent initialized with agents: {list(self.specialized_agents.keys())}")
73
+
74
+ def _get_default_enabled_agents(self) -> List[str]:
75
+ """Get default enabled agents from configuration"""
76
+ # Check moderator config first
77
+ if 'default_enabled_agents' in self.moderator_config:
78
+ return self.moderator_config['default_enabled_agents']
79
+
80
+ # Otherwise check capabilities config
81
+ enabled = []
82
+
83
+ if self.capabilities.get('enable_knowledge_base', False):
84
+ enabled.append('knowledge_base')
85
+ if self.capabilities.get('enable_web_search', False):
86
+ enabled.append('web_search')
87
+ if self.capabilities.get('enable_code_execution', False):
88
+ enabled.append('code_executor')
89
+ if self.capabilities.get('enable_media_editor', False):
90
+ enabled.append('media_editor')
91
+ if self.capabilities.get('enable_youtube_download', False):
92
+ enabled.append('youtube_download')
93
+ if self.capabilities.get('enable_web_scraping', False):
94
+ enabled.append('web_scraper')
95
+
96
+ # Always include assistant for general queries
97
+ enabled.append('assistant')
98
+
99
+ return enabled
100
+
101
+ def _is_agent_enabled(self, agent_type: str) -> bool:
102
+ """Check if an agent type is enabled"""
103
+ if agent_type in self.enabled_agents:
104
+ return True
105
+
106
+ # Double-check against capabilities config
107
+ capability_map = {
108
+ 'knowledge_base': 'enable_knowledge_base',
109
+ 'web_search': 'enable_web_search',
110
+ 'code_executor': 'enable_code_execution',
111
+ 'media_editor': 'enable_media_editor',
112
+ 'youtube_download': 'enable_youtube_download',
113
+ 'web_scraper': 'enable_web_scraping',
114
+ 'assistant': True # Always enabled
115
+ }
116
+
117
+ if agent_type == 'assistant':
118
+ return True
119
+
120
+ capability_key = capability_map.get(agent_type)
121
+ if capability_key and isinstance(capability_key, str):
122
+ return self.capabilities.get(capability_key, False)
123
+
124
+ return False
125
+
126
+ def _get_agent_config(self, agent_type: str) -> Dict[str, Any]:
127
+ """Get configuration for specific agent type"""
128
+ agent_config = {}
129
+
130
+ if agent_type == 'knowledge_base':
131
+ agent_config = self.config.get('knowledge_base', {})
132
+ elif agent_type == 'web_search':
133
+ agent_config = self.config.get('web_search', {})
134
+ elif agent_type == 'media_editor':
135
+ agent_config = self.config.get('media_editor', {})
136
+ elif agent_type == 'youtube_download':
137
+ agent_config = self.config.get('youtube_download', {})
138
+ elif agent_type == 'web_scraper':
139
+ agent_config = self.config.get('web_scraping', {})
140
+ elif agent_type == 'code_executor':
141
+ agent_config = self.config.get('docker', {})
142
+
143
+ return agent_config
144
+
145
+ def _initialize_specialized_agents(self):
146
+ """Initialize all enabled specialized agents"""
147
+ # Import agents dynamically to avoid circular imports
148
+ try:
149
+ from . import (
150
+ KnowledgeBaseAgent, WebSearchAgent, CodeExecutorAgent,
151
+ MediaEditorAgent, YouTubeDownloadAgent, WebScraperAgent, AssistantAgent
152
+ )
153
+ except ImportError:
154
+ # Fallback individual imports
155
+ try:
156
+ from .knowledge_base import KnowledgeBaseAgent
157
+ except ImportError:
158
+ KnowledgeBaseAgent = None
159
+ try:
160
+ from .web_search import WebSearchAgent
161
+ except ImportError:
162
+ WebSearchAgent = None
163
+ try:
164
+ from .code_executor import CodeExecutorAgent
165
+ except ImportError:
166
+ CodeExecutorAgent = None
167
+ try:
168
+ from .media_editor import MediaEditorAgent
169
+ except ImportError:
170
+ MediaEditorAgent = None
171
+ try:
172
+ from .youtube_download import YouTubeDownloadAgent
173
+ except ImportError:
174
+ YouTubeDownloadAgent = None
175
+ try:
176
+ from .web_scraper import WebScraperAgent
177
+ except ImportError:
178
+ WebScraperAgent = None
179
+ try:
180
+ from .assistant import AssistantAgent
181
+ except ImportError:
182
+ AssistantAgent = None
183
+
184
+ agent_classes = {
185
+ 'knowledge_base': KnowledgeBaseAgent,
186
+ 'web_search': WebSearchAgent,
187
+ 'code_executor': CodeExecutorAgent,
188
+ 'media_editor': MediaEditorAgent,
189
+ 'youtube_download': YouTubeDownloadAgent,
190
+ 'web_scraper': WebScraperAgent,
191
+ 'assistant': AssistantAgent
192
+ }
193
+
194
+ for agent_type in self.enabled_agents:
195
+ if not self._is_agent_enabled(agent_type):
196
+ continue
197
+
198
+ agent_class = agent_classes.get(agent_type)
199
+ if agent_class is None:
200
+ logging.warning(f"Agent class for {agent_type} not available")
201
+ continue
202
+
203
+ try:
204
+ # Create agent with shared context
205
+ agent_instance = agent_class.create_simple(
206
+ user_id=self.context.user_id,
207
+ tenant_id=self.context.tenant_id,
208
+ session_metadata={
209
+ 'parent_moderator': self.agent_id,
210
+ 'agent_type': agent_type
211
+ }
212
+ )
213
+ self.specialized_agents[agent_type] = agent_instance
214
+ logging.info(f"Initialized {agent_type} agent: {agent_instance.agent_id}")
215
+
216
+ except Exception as e:
217
+ logging.error(f"Failed to initialize {agent_type} agent: {e}")
218
+
219
+ def _setup_routing_patterns(self):
220
+ """Setup intelligent routing patterns for different query types"""
221
+ self.agent_routing_patterns = {
222
+ 'knowledge_base': {
223
+ 'keywords': ['search knowledge', 'query kb', 'knowledge base', 'find in documents',
224
+ 'search documents', 'what do you know about', 'from my files'],
225
+ 'patterns': [r'search\s+(?:in\s+)?(?:kb|knowledge|documents?)',
226
+ r'query\s+(?:the\s+)?(?:kb|knowledge|database)',
227
+ r'find\s+(?:in\s+)?(?:my\s+)?(?:files|documents?)'],
228
+ 'indicators': ['kb_name', 'collection_table', 'document', 'file'],
229
+ 'priority': 1
230
+ },
231
+
232
+ 'web_search': {
233
+ 'keywords': ['search web', 'google', 'find online', 'search for', 'look up',
234
+ 'search internet', 'web search', 'find information'],
235
+ 'patterns': [r'search\s+(?:the\s+)?(?:web|internet|online)',
236
+ r'(?:google|look\s+up|find)\s+(?:information\s+)?(?:about|on)',
237
+ r'what\'s\s+happening\s+with', r'latest\s+news'],
238
+ 'indicators': ['search', 'web', 'online', 'internet', 'news'],
239
+ 'priority': 2
240
+ },
241
+
242
+ 'youtube_download': {
243
+ 'keywords': ['download youtube', 'youtube video', 'download video', 'get from youtube'],
244
+ 'patterns': [r'download\s+(?:from\s+)?youtube', r'youtube\.com/watch', r'youtu\.be/',
245
+ r'get\s+(?:video|audio)\s+from\s+youtube'],
246
+ 'indicators': ['youtube.com', 'youtu.be', 'download video', 'download audio'],
247
+ 'priority': 1
248
+ },
249
+
250
+ 'media_editor': {
251
+ 'keywords': ['convert video', 'edit media', 'extract audio', 'resize video',
252
+ 'media processing', 'ffmpeg'],
253
+ 'patterns': [r'convert\s+(?:video|audio)', r'extract\s+audio', r'resize\s+video',
254
+ r'trim\s+(?:video|audio)', r'media\s+(?:processing|editing)'],
255
+ 'indicators': ['.mp4', '.avi', '.mp3', '.wav', 'video', 'audio'],
256
+ 'priority': 1
257
+ },
258
+
259
+ 'web_scraper': {
260
+ 'keywords': ['scrape website', 'extract from site', 'crawl web', 'scrape data'],
261
+ 'patterns': [r'scrape\s+(?:website|site|web)', r'extract\s+(?:data\s+)?from\s+(?:website|site)',
262
+ r'crawl\s+(?:website|web)'],
263
+ 'indicators': ['scrape', 'crawl', 'extract data', 'website'],
264
+ 'priority': 1
265
+ },
266
+
267
+ 'code_executor': {
268
+ 'keywords': ['run code', 'execute python', 'run script', 'code execution'],
269
+ 'patterns': [r'run\s+(?:this\s+)?(?:code|script|python)', r'execute\s+(?:code|script)',
270
+ r'```(?:python|bash)'],
271
+ 'indicators': ['```', 'def ', 'import ', 'python', 'bash'],
272
+ 'priority': 1
273
+ },
274
+
275
+ 'assistant': {
276
+ 'keywords': ['help', 'explain', 'how to', 'what is', 'tell me'],
277
+ 'patterns': [r'(?:help|explain|tell)\s+me', r'what\s+is', r'how\s+(?:do\s+)?(?:I|to)',
278
+ r'can\s+you\s+(?:help|explain)'],
279
+ 'indicators': ['help', 'explain', 'question', 'general'],
280
+ 'priority': 3 # Lowest priority - fallback
281
+ }
282
+ }
283
+
284
+ async def _analyze_query_intent(self, user_message: str) -> Dict[str, Any]:
285
+ """Analyze user query to determine which agent(s) should handle it"""
286
+ message_lower = user_message.lower()
287
+
288
+ # Extract context from conversation history
289
+ conversation_context = self._get_conversation_context_summary()
290
+
291
+ # Score each agent type
292
+ agent_scores = {}
293
+
294
+ for agent_type, patterns in self.agent_routing_patterns.items():
295
+ if agent_type not in self.specialized_agents:
296
+ continue
297
+
298
+ score = 0
299
+
300
+ # Keyword matching
301
+ keyword_matches = sum(1 for keyword in patterns['keywords']
302
+ if keyword in message_lower)
303
+ score += keyword_matches * 2
304
+
305
+ # Pattern matching
306
+ import re
307
+ pattern_matches = sum(1 for pattern in patterns['patterns']
308
+ if re.search(pattern, message_lower))
309
+ score += pattern_matches * 3
310
+
311
+ # Indicator matching
312
+ indicator_matches = sum(1 for indicator in patterns['indicators']
313
+ if indicator in message_lower)
314
+ score += indicator_matches * 1
315
+
316
+ # Context matching from conversation history
317
+ if conversation_context and agent_type in conversation_context.lower():
318
+ score += 2
319
+
320
+ # Apply priority weighting (lower priority number = higher weight)
321
+ priority_weight = 4 - patterns.get('priority', 3)
322
+ score *= priority_weight
323
+
324
+ agent_scores[agent_type] = score
325
+
326
+ # Determine primary agent (highest score)
327
+ if agent_scores:
328
+ primary_agent = max(agent_scores.items(), key=lambda x: x[1])[0]
329
+ confidence = agent_scores[primary_agent] / sum(agent_scores.values()) if sum(
330
+ agent_scores.values()) > 0 else 0
331
+ else:
332
+ primary_agent = 'assistant' # fallback
333
+ confidence = 0.5
334
+
335
+ # Determine if multiple agents needed
336
+ high_scoring_agents = [agent for agent, score in agent_scores.items() if score > 3]
337
+ requires_multiple = len(high_scoring_agents) > 1
338
+
339
+ return {
340
+ 'primary_agent': primary_agent,
341
+ 'confidence': confidence,
342
+ 'agent_scores': agent_scores,
343
+ 'requires_multiple_agents': requires_multiple,
344
+ 'high_scoring_agents': high_scoring_agents,
345
+ 'context_detected': bool(conversation_context)
346
+ }
347
+
348
+ async def _route_to_agent(self, agent_type: str, user_message: str,
349
+ context: ExecutionContext = None) -> AgentResponse:
350
+ """Route query to specific agent and get response"""
351
+ if agent_type not in self.specialized_agents:
352
+ return AgentResponse(
353
+ agent_type=agent_type,
354
+ content=f"Agent {agent_type} not available",
355
+ success=False,
356
+ execution_time=0.0,
357
+ metadata={},
358
+ error=f"Agent {agent_type} not initialized"
359
+ )
360
+
361
+ start_time = time.time()
362
+
363
+ try:
364
+ agent = self.specialized_agents[agent_type]
365
+
366
+ # Use the agent's chat interface
367
+ response_content = await agent.chat(user_message)
368
+
369
+ execution_time = time.time() - start_time
370
+
371
+ return AgentResponse(
372
+ agent_type=agent_type,
373
+ content=response_content,
374
+ success=True,
375
+ execution_time=execution_time,
376
+ metadata={
377
+ 'agent_id': agent.agent_id,
378
+ 'session_id': agent.context.session_id
379
+ }
380
+ )
381
+
382
+ except Exception as e:
383
+ execution_time = time.time() - start_time
384
+ logging.error(f"Error routing to {agent_type} agent: {e}")
385
+
386
+ return AgentResponse(
387
+ agent_type=agent_type,
388
+ content=f"Error processing request with {agent_type} agent",
389
+ success=False,
390
+ execution_time=execution_time,
391
+ metadata={},
392
+ error=str(e)
393
+ )
394
+
395
+ async def _coordinate_multiple_agents(self, agents: List[str], user_message: str,
396
+ context: ExecutionContext = None) -> str:
397
+ """Coordinate multiple agents for complex queries"""
398
+ responses = []
399
+
400
+ # Execute agents concurrently
401
+ tasks = [self._route_to_agent(agent_type, user_message, context)
402
+ for agent_type in agents]
403
+
404
+ agent_responses = await asyncio.gather(*tasks, return_exceptions=True)
405
+
406
+ successful_responses = []
407
+ for response in agent_responses:
408
+ if isinstance(response, AgentResponse) and response.success:
409
+ successful_responses.append(response)
410
+
411
+ if not successful_responses:
412
+ return "I wasn't able to process your request with any of the available agents."
413
+
414
+ # Combine multiple responses intelligently
415
+ if len(successful_responses) == 1:
416
+ return successful_responses[0].content
417
+
418
+ combined_response = "Here's what I found from multiple sources:\n\n"
419
+
420
+ for i, response in enumerate(successful_responses, 1):
421
+ combined_response += f"**From {response.agent_type.replace('_', ' ').title()}:**\n"
422
+ combined_response += f"{response.content}\n\n"
423
+
424
+ return combined_response.strip()
425
+
426
+ def _get_conversation_context_summary(self) -> str:
427
+ """Get conversation context summary (simplified for now)"""
428
+ try:
429
+ recent_history = self.get_conversation_history_with_context(limit=3)
430
+ context_summary = []
431
+
432
+ for msg in recent_history:
433
+ if msg.get('message_type') == 'user_input':
434
+ content = msg.get('content', '')
435
+ context_summary.append(content[:50])
436
+
437
+ return " ".join(context_summary) if context_summary else ""
438
+ except:
439
+ return ""
440
+
441
+ async def process_message(self, message: AgentMessage, context: ExecutionContext = None) -> AgentMessage:
442
+ """Main processing method - routes to appropriate agents"""
443
+ self.memory.store_message(message)
444
+
445
+ try:
446
+ user_message = message.content
447
+
448
+ # Update conversation state
449
+ self.update_conversation_state(user_message)
450
+
451
+ # Analyze intent to determine routing
452
+ intent_analysis = await self._analyze_query_intent(user_message)
453
+
454
+ logging.info(f"Intent analysis: Primary={intent_analysis['primary_agent']}, "
455
+ f"Confidence={intent_analysis['confidence']:.2f}")
456
+
457
+ # Route based on analysis
458
+ if intent_analysis['requires_multiple_agents'] and len(intent_analysis['high_scoring_agents']) > 1:
459
+ # Use multiple agents
460
+ response_content = await self._coordinate_multiple_agents(
461
+ intent_analysis['high_scoring_agents'],
462
+ user_message,
463
+ context
464
+ )
465
+ else:
466
+ # Use single primary agent
467
+ primary_response = await self._route_to_agent(
468
+ intent_analysis['primary_agent'],
469
+ user_message,
470
+ context
471
+ )
472
+
473
+ if primary_response.success:
474
+ response_content = primary_response.content
475
+ else:
476
+ # Fallback to assistant if primary agent fails
477
+ if intent_analysis['primary_agent'] != 'assistant' and 'assistant' in self.specialized_agents:
478
+ fallback_response = await self._route_to_agent('assistant', user_message, context)
479
+ response_content = fallback_response.content
480
+ else:
481
+ response_content = f"I encountered an error processing your request: {primary_response.error}"
482
+
483
+ # Add routing metadata to response
484
+ agent_name = intent_analysis['primary_agent'].replace('_', ' ').title()
485
+ response_content += f"\n\n*Processed by: {agent_name} (confidence: {intent_analysis['confidence']:.2f})*"
486
+
487
+ response = self.create_response(
488
+ content=response_content,
489
+ recipient_id=message.sender_id,
490
+ session_id=message.session_id,
491
+ conversation_id=message.conversation_id
492
+ )
493
+
494
+ self.memory.store_message(response)
495
+ return response
496
+
497
+ except Exception as e:
498
+ logging.error(f"ModeratorAgent error: {e}")
499
+ error_response = self.create_response(
500
+ content=f"I encountered an error processing your request: {str(e)}",
501
+ recipient_id=message.sender_id,
502
+ message_type=MessageType.ERROR,
503
+ session_id=message.session_id,
504
+ conversation_id=message.conversation_id
505
+ )
506
+ return error_response
507
+
508
+ async def get_agent_status(self) -> Dict[str, Any]:
509
+ """Get status of all managed agents - FIXED METHOD"""
510
+ status = {
511
+ 'moderator_id': self.agent_id,
512
+ 'enabled_agents': self.enabled_agents,
513
+ 'active_agents': {},
514
+ 'total_agents': len(self.specialized_agents),
515
+ 'routing_patterns': len(self.agent_routing_patterns)
516
+ }
517
+
518
+ for agent_type, agent in self.specialized_agents.items():
519
+ try:
520
+ # Simple status check
521
+ status['active_agents'][agent_type] = {
522
+ 'agent_id': agent.agent_id,
523
+ 'status': 'active',
524
+ 'session_id': agent.context.session_id if hasattr(agent, 'context') else 'unknown'
525
+ }
526
+ except Exception as e:
527
+ status['active_agents'][agent_type] = {
528
+ 'agent_id': getattr(agent, 'agent_id', 'unknown'),
529
+ 'status': 'error',
530
+ 'error': str(e)
531
+ }
532
+
533
+ return status
534
+
535
+ async def cleanup_session(self) -> bool:
536
+ """Cleanup all managed agents"""
537
+ success = True
538
+
539
+ # Cleanup all specialized agents
540
+ for agent_type, agent in self.specialized_agents.items():
541
+ try:
542
+ await agent.cleanup_session()
543
+ logging.info(f"Cleaned up {agent_type} agent")
544
+ except Exception as e:
545
+ logging.error(f"Error cleaning up {agent_type} agent: {e}")
546
+ success = False
547
+
548
+ # Cleanup moderator itself
549
+ moderator_cleanup = await super().cleanup_session()
550
+
551
+ return success and moderator_cleanup
552
+
553
+ @classmethod
554
+ def create(cls,
555
+ agent_id: str = None,
556
+ user_id: str = None,
557
+ tenant_id: str = "default",
558
+ enabled_agents: List[str] = None,
559
+ session_metadata: Dict[str, Any] = None,
560
+ **kwargs):
561
+ """
562
+ Create ModeratorAgent with specified enabled agents
563
+
564
+ Args:
565
+ agent_id: Optional agent ID
566
+ user_id: User ID for context
567
+ tenant_id: Tenant ID for context
568
+ enabled_agents: List of agent types to enable. If None, uses config defaults
569
+ session_metadata: Additional session metadata
570
+ **kwargs: Additional arguments
571
+
572
+ Returns:
573
+ Tuple of (ModeratorAgent, AgentContext)
574
+ """
575
+ if agent_id is None:
576
+ agent_id = f"moderator_{str(uuid.uuid4())[:8]}"
577
+
578
+ agent = cls(
579
+ agent_id=agent_id,
580
+ user_id=user_id,
581
+ tenant_id=tenant_id,
582
+ enabled_agents=enabled_agents,
583
+ session_metadata=session_metadata,
584
+ auto_configure=True,
585
+ **kwargs
586
+ )
587
+
588
+ return agent, agent.context
589
+
590
+ @classmethod
591
+ def create_simple(cls,
592
+ user_id: str = None,
593
+ tenant_id: str = "default",
594
+ enabled_agents: List[str] = None,
595
+ **kwargs):
596
+ """
597
+ Simple factory method for ModeratorAgent
598
+
599
+ Args:
600
+ user_id: User ID for context
601
+ tenant_id: Tenant ID for context
602
+ enabled_agents: List of agent types to enable
603
+ **kwargs: Additional arguments
604
+
605
+ Returns:
606
+ ModeratorAgent instance
607
+ """
608
+ agent_id = f"moderator_{str(uuid.uuid4())[:8]}"
609
+
610
+ return cls(
611
+ agent_id=agent_id,
612
+ user_id=user_id,
613
+ tenant_id=tenant_id,
614
+ enabled_agents=enabled_agents,
615
+ auto_configure=True,
616
+ **kwargs
617
+ )