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.
- ambivo_agents/__init__.py +91 -0
- ambivo_agents/agents/__init__.py +21 -0
- ambivo_agents/agents/assistant.py +203 -0
- ambivo_agents/agents/code_executor.py +133 -0
- ambivo_agents/agents/code_executor2.py +222 -0
- ambivo_agents/agents/knowledge_base.py +935 -0
- ambivo_agents/agents/media_editor.py +992 -0
- ambivo_agents/agents/moderator.py +617 -0
- ambivo_agents/agents/simple_web_search.py +404 -0
- ambivo_agents/agents/web_scraper.py +1027 -0
- ambivo_agents/agents/web_search.py +933 -0
- ambivo_agents/agents/youtube_download.py +784 -0
- ambivo_agents/cli.py +699 -0
- ambivo_agents/config/__init__.py +4 -0
- ambivo_agents/config/loader.py +301 -0
- ambivo_agents/core/__init__.py +33 -0
- ambivo_agents/core/base.py +1024 -0
- ambivo_agents/core/history.py +606 -0
- ambivo_agents/core/llm.py +333 -0
- ambivo_agents/core/memory.py +640 -0
- ambivo_agents/executors/__init__.py +8 -0
- ambivo_agents/executors/docker_executor.py +108 -0
- ambivo_agents/executors/media_executor.py +237 -0
- ambivo_agents/executors/youtube_executor.py +404 -0
- ambivo_agents/services/__init__.py +6 -0
- ambivo_agents/services/agent_service.py +605 -0
- ambivo_agents/services/factory.py +370 -0
- ambivo_agents-1.0.1.dist-info/METADATA +1090 -0
- ambivo_agents-1.0.1.dist-info/RECORD +33 -0
- ambivo_agents-1.0.1.dist-info/WHEEL +5 -0
- ambivo_agents-1.0.1.dist-info/entry_points.txt +3 -0
- ambivo_agents-1.0.1.dist-info/licenses/LICENSE +21 -0
- ambivo_agents-1.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,404 @@
|
|
1
|
+
# ambivo_agents/agents/simple_web_search.py
|
2
|
+
"""
|
3
|
+
Simple Web Search Agent - Focused and Direct
|
4
|
+
Reports search provider information clearly
|
5
|
+
"""
|
6
|
+
|
7
|
+
import asyncio
|
8
|
+
import json
|
9
|
+
import time
|
10
|
+
import uuid
|
11
|
+
|
12
|
+
import requests
|
13
|
+
import logging
|
14
|
+
from typing import Dict, List, Any, Optional
|
15
|
+
from datetime import datetime
|
16
|
+
from dataclasses import dataclass
|
17
|
+
|
18
|
+
from ..core.base import BaseAgent, AgentRole, AgentMessage, MessageType, ExecutionContext, AgentTool
|
19
|
+
from ..config.loader import load_config, get_config_section
|
20
|
+
|
21
|
+
|
22
|
+
@dataclass
|
23
|
+
class SearchResult:
|
24
|
+
"""Simple search result structure"""
|
25
|
+
title: str
|
26
|
+
url: str
|
27
|
+
snippet: str
|
28
|
+
rank: int
|
29
|
+
provider: str
|
30
|
+
search_time: float
|
31
|
+
|
32
|
+
|
33
|
+
class SimpleWebSearchAgent(BaseAgent):
|
34
|
+
"""Simple, targeted web search agent with clear provider reporting"""
|
35
|
+
|
36
|
+
def __init__(self, agent_id: str=None, memory_manager=None, llm_service=None, **kwargs):
|
37
|
+
if agent_id is None:
|
38
|
+
agent_id = f"search_{str(uuid.uuid4())[:8]}"
|
39
|
+
|
40
|
+
super().__init__(
|
41
|
+
agent_id=agent_id,
|
42
|
+
role=AgentRole.RESEARCHER,
|
43
|
+
memory_manager=memory_manager,
|
44
|
+
llm_service=llm_service,
|
45
|
+
name="Simple Web Search Agent",
|
46
|
+
description="Direct web search with clear provider reporting",
|
47
|
+
**kwargs
|
48
|
+
)
|
49
|
+
|
50
|
+
# Load search configuration
|
51
|
+
try:
|
52
|
+
config = load_config()
|
53
|
+
self.search_config = get_config_section('web_search', config)
|
54
|
+
except Exception as e:
|
55
|
+
raise ValueError(f"web_search configuration not found: {e}")
|
56
|
+
|
57
|
+
self.logger = logging.getLogger(f"SimpleWebSearch-{agent_id}")
|
58
|
+
# Initialize providers
|
59
|
+
self.providers = {}
|
60
|
+
self.current_provider = None
|
61
|
+
self._initialize_providers()
|
62
|
+
|
63
|
+
|
64
|
+
|
65
|
+
def _initialize_providers(self):
|
66
|
+
"""Initialize available search providers"""
|
67
|
+
|
68
|
+
# Brave Search
|
69
|
+
if self.search_config.get('brave_api_key'):
|
70
|
+
self.providers['brave'] = {
|
71
|
+
'name': 'Brave Search',
|
72
|
+
'api_key': self.search_config['brave_api_key'],
|
73
|
+
'url': 'https://api.search.brave.com/res/v1/web/search',
|
74
|
+
'priority': 1,
|
75
|
+
'available': True,
|
76
|
+
'rate_limit': 2.0
|
77
|
+
}
|
78
|
+
|
79
|
+
# AVES API
|
80
|
+
if self.search_config.get('avesapi_api_key'):
|
81
|
+
self.providers['aves'] = {
|
82
|
+
'name': 'AVES Search',
|
83
|
+
'api_key': self.search_config['avesapi_api_key'],
|
84
|
+
'url': 'https://api.avesapi.com/search',
|
85
|
+
'priority': 2,
|
86
|
+
'available': True,
|
87
|
+
'rate_limit': 1.5
|
88
|
+
}
|
89
|
+
|
90
|
+
if not self.providers:
|
91
|
+
raise ValueError("No search providers configured")
|
92
|
+
|
93
|
+
# Set current provider (highest priority available)
|
94
|
+
available_providers = [(name, config) for name, config in self.providers.items()
|
95
|
+
if config['available']]
|
96
|
+
if available_providers:
|
97
|
+
available_providers.sort(key=lambda x: x[1]['priority'])
|
98
|
+
self.current_provider = available_providers[0][0]
|
99
|
+
|
100
|
+
self.logger.info(f"Initialized with providers: {list(self.providers.keys())}")
|
101
|
+
self.logger.info(f"Current provider: {self.current_provider}")
|
102
|
+
|
103
|
+
async def search_web(self, query: str, max_results: int = 10) -> Dict[str, Any]:
|
104
|
+
"""Perform web search and return results with provider info"""
|
105
|
+
|
106
|
+
if not self.current_provider:
|
107
|
+
return {
|
108
|
+
'success': False,
|
109
|
+
'error': 'No search provider available',
|
110
|
+
'provider': None,
|
111
|
+
'query': query
|
112
|
+
}
|
113
|
+
|
114
|
+
provider_info = self.providers[self.current_provider]
|
115
|
+
self.logger.info(f"Searching with {provider_info['name']} for: {query}")
|
116
|
+
|
117
|
+
start_time = time.time()
|
118
|
+
|
119
|
+
try:
|
120
|
+
# Rate limiting
|
121
|
+
await asyncio.sleep(provider_info['rate_limit'])
|
122
|
+
|
123
|
+
if self.current_provider == 'brave':
|
124
|
+
results = await self._search_brave(query, max_results)
|
125
|
+
elif self.current_provider == 'aves':
|
126
|
+
results = await self._search_aves(query, max_results)
|
127
|
+
else:
|
128
|
+
raise ValueError(f"Unknown provider: {self.current_provider}")
|
129
|
+
|
130
|
+
search_time = time.time() - start_time
|
131
|
+
|
132
|
+
return {
|
133
|
+
'success': True,
|
134
|
+
'query': query,
|
135
|
+
'results': results,
|
136
|
+
'total_results': len(results),
|
137
|
+
'provider': {
|
138
|
+
'name': provider_info['name'],
|
139
|
+
'code': self.current_provider,
|
140
|
+
'api_endpoint': provider_info['url']
|
141
|
+
},
|
142
|
+
'search_time': search_time,
|
143
|
+
'timestamp': datetime.now().isoformat()
|
144
|
+
}
|
145
|
+
|
146
|
+
except Exception as e:
|
147
|
+
search_time = time.time() - start_time
|
148
|
+
self.logger.error(f"Search failed with {provider_info['name']}: {e}")
|
149
|
+
|
150
|
+
# Try fallback provider
|
151
|
+
fallback_result = await self._try_fallback_provider(query, max_results)
|
152
|
+
if fallback_result:
|
153
|
+
return fallback_result
|
154
|
+
|
155
|
+
return {
|
156
|
+
'success': False,
|
157
|
+
'error': str(e),
|
158
|
+
'provider': {
|
159
|
+
'name': provider_info['name'],
|
160
|
+
'code': self.current_provider,
|
161
|
+
'api_endpoint': provider_info['url']
|
162
|
+
},
|
163
|
+
'query': query,
|
164
|
+
'search_time': search_time
|
165
|
+
}
|
166
|
+
|
167
|
+
async def _search_brave(self, query: str, max_results: int) -> List[SearchResult]:
|
168
|
+
"""Search using Brave API"""
|
169
|
+
|
170
|
+
provider = self.providers['brave']
|
171
|
+
|
172
|
+
headers = {
|
173
|
+
'Accept': 'application/json',
|
174
|
+
'Accept-Encoding': 'gzip',
|
175
|
+
'X-Subscription-Token': provider['api_key']
|
176
|
+
}
|
177
|
+
|
178
|
+
params = {
|
179
|
+
'q': query,
|
180
|
+
'count': min(max_results, 20),
|
181
|
+
'country': 'US',
|
182
|
+
'search_lang': 'en'
|
183
|
+
}
|
184
|
+
|
185
|
+
response = requests.get(provider['url'], headers=headers, params=params, timeout=15)
|
186
|
+
|
187
|
+
if response.status_code == 429:
|
188
|
+
raise Exception("Brave API rate limit exceeded")
|
189
|
+
elif response.status_code == 401:
|
190
|
+
raise Exception("Brave API authentication failed")
|
191
|
+
|
192
|
+
response.raise_for_status()
|
193
|
+
data = response.json()
|
194
|
+
|
195
|
+
results = []
|
196
|
+
web_results = data.get('web', {}).get('results', [])
|
197
|
+
|
198
|
+
for i, result in enumerate(web_results[:max_results]):
|
199
|
+
results.append(SearchResult(
|
200
|
+
title=result.get('title', ''),
|
201
|
+
url=result.get('url', ''),
|
202
|
+
snippet=result.get('description', ''),
|
203
|
+
rank=i + 1,
|
204
|
+
provider='brave',
|
205
|
+
search_time=0.0 # Will be set by caller
|
206
|
+
))
|
207
|
+
|
208
|
+
return results
|
209
|
+
|
210
|
+
async def _search_aves(self, query: str, max_results: int) -> List[SearchResult]:
|
211
|
+
"""Search using AVES API"""
|
212
|
+
|
213
|
+
provider = self.providers['aves']
|
214
|
+
|
215
|
+
params = {
|
216
|
+
'apikey': provider['api_key'],
|
217
|
+
'type': 'web',
|
218
|
+
'query': query,
|
219
|
+
'device': 'desktop',
|
220
|
+
'output': 'json',
|
221
|
+
'num': min(max_results, 10)
|
222
|
+
}
|
223
|
+
|
224
|
+
response = requests.get(provider['url'], params=params, timeout=15)
|
225
|
+
|
226
|
+
if response.status_code == 429:
|
227
|
+
raise Exception("AVES API rate limit exceeded")
|
228
|
+
elif response.status_code == 401:
|
229
|
+
raise Exception("AVES API authentication failed")
|
230
|
+
|
231
|
+
response.raise_for_status()
|
232
|
+
data = response.json()
|
233
|
+
|
234
|
+
results = []
|
235
|
+
|
236
|
+
# AVES has different response structures, handle both
|
237
|
+
search_results = data.get('result', {}).get('organic_results', [])
|
238
|
+
if not search_results:
|
239
|
+
search_results = data.get('organic_results', [])
|
240
|
+
|
241
|
+
for i, result in enumerate(search_results[:max_results]):
|
242
|
+
results.append(SearchResult(
|
243
|
+
title=result.get('title', ''),
|
244
|
+
url=result.get('url', result.get('link', '')),
|
245
|
+
snippet=result.get('description', result.get('snippet', '')),
|
246
|
+
rank=i + 1,
|
247
|
+
provider='aves',
|
248
|
+
search_time=0.0 # Will be set by caller
|
249
|
+
))
|
250
|
+
|
251
|
+
return results
|
252
|
+
|
253
|
+
async def _try_fallback_provider(self, query: str, max_results: int) -> Optional[Dict[str, Any]]:
|
254
|
+
"""Try fallback provider if current one fails"""
|
255
|
+
|
256
|
+
# Mark current provider as temporarily unavailable
|
257
|
+
self.providers[self.current_provider]['available'] = False
|
258
|
+
|
259
|
+
# Find next available provider
|
260
|
+
available_providers = [(name, config) for name, config in self.providers.items()
|
261
|
+
if config['available']]
|
262
|
+
|
263
|
+
if not available_providers:
|
264
|
+
return None
|
265
|
+
|
266
|
+
available_providers.sort(key=lambda x: x[1]['priority'])
|
267
|
+
fallback_provider = available_providers[0][0]
|
268
|
+
|
269
|
+
self.logger.info(f"Falling back from {self.current_provider} to {fallback_provider}")
|
270
|
+
self.current_provider = fallback_provider
|
271
|
+
|
272
|
+
# Try search with fallback provider
|
273
|
+
try:
|
274
|
+
return await self.search_web(query, max_results)
|
275
|
+
except Exception as e:
|
276
|
+
self.logger.error(f"Fallback provider {fallback_provider} also failed: {e}")
|
277
|
+
return None
|
278
|
+
|
279
|
+
def format_search_response(self, search_data: Dict[str, Any]) -> str:
|
280
|
+
"""Format search results into a readable response"""
|
281
|
+
|
282
|
+
if not search_data['success']:
|
283
|
+
provider_name = search_data.get('provider', {}).get('name', 'Unknown')
|
284
|
+
return f"""❌ **Search Failed**
|
285
|
+
|
286
|
+
**Provider**: {provider_name}
|
287
|
+
**Error**: {search_data['error']}
|
288
|
+
**Query**: {search_data['query']}
|
289
|
+
|
290
|
+
Please try again in a few moments."""
|
291
|
+
|
292
|
+
provider = search_data['provider']
|
293
|
+
results = search_data['results']
|
294
|
+
|
295
|
+
response = f"""✅ **Search Results**
|
296
|
+
|
297
|
+
**🔍 Query**: {search_data['query']}
|
298
|
+
**📡 Provider**: {provider['name']} ({provider['code'].upper()})
|
299
|
+
**⏱️ Search Time**: {search_data['search_time']:.2f}s
|
300
|
+
**📊 Results**: {len(results)} found
|
301
|
+
|
302
|
+
"""
|
303
|
+
|
304
|
+
if results:
|
305
|
+
response += "**🔗 Top Results**:\n\n"
|
306
|
+
|
307
|
+
for result in results[:5]: # Show top 5 results
|
308
|
+
response += f"**{result.rank}. {result.title}**\n"
|
309
|
+
response += f"🔗 {result.url}\n"
|
310
|
+
response += f"📝 {result.snippet[:150]}...\n\n"
|
311
|
+
else:
|
312
|
+
response += "**No results found for this query.**\n"
|
313
|
+
|
314
|
+
response += f"*Powered by {provider['name']} Search API*"
|
315
|
+
|
316
|
+
return response
|
317
|
+
|
318
|
+
async def process_message(self, message: AgentMessage, context: ExecutionContext=None) -> AgentMessage:
|
319
|
+
"""Process web search requests"""
|
320
|
+
|
321
|
+
self.memory.store_message(message)
|
322
|
+
|
323
|
+
try:
|
324
|
+
content = message.content
|
325
|
+
|
326
|
+
# Extract search query from message
|
327
|
+
query = self._extract_query_from_message(content)
|
328
|
+
|
329
|
+
if not query:
|
330
|
+
response_content = """I'm a web search agent. Please provide a search query like:
|
331
|
+
|
332
|
+
• "search for what is ambivo"
|
333
|
+
• "find information about AI trends"
|
334
|
+
• "search web for Python tutorials"
|
335
|
+
|
336
|
+
I'll search the web and show you which provider (Brave or AVES) was used."""
|
337
|
+
else:
|
338
|
+
# Perform the search
|
339
|
+
search_data = await self.search_web(query, max_results=5)
|
340
|
+
|
341
|
+
# Format the response
|
342
|
+
response_content = self.format_search_response(search_data)
|
343
|
+
|
344
|
+
# Store search data in memory for debugging
|
345
|
+
self.memory.store_context('last_search', search_data, message.conversation_id)
|
346
|
+
|
347
|
+
response = self.create_response(
|
348
|
+
content=response_content,
|
349
|
+
recipient_id=message.sender_id,
|
350
|
+
session_id=message.session_id,
|
351
|
+
conversation_id=message.conversation_id
|
352
|
+
)
|
353
|
+
|
354
|
+
self.memory.store_message(response)
|
355
|
+
return response
|
356
|
+
|
357
|
+
except Exception as e:
|
358
|
+
self.logger.error(f"Search processing error: {e}")
|
359
|
+
error_response = self.create_response(
|
360
|
+
content=f"🔧 **Search Error**: {str(e)}\n\nPlease check your search configuration and try again.",
|
361
|
+
recipient_id=message.sender_id,
|
362
|
+
message_type=MessageType.ERROR,
|
363
|
+
session_id=message.session_id,
|
364
|
+
conversation_id=message.conversation_id
|
365
|
+
)
|
366
|
+
return error_response
|
367
|
+
|
368
|
+
def _extract_query_from_message(self, content: str) -> Optional[str]:
|
369
|
+
"""Extract search query from user message"""
|
370
|
+
|
371
|
+
content_lower = content.lower()
|
372
|
+
|
373
|
+
# Remove common search prefixes
|
374
|
+
prefixes_to_remove = [
|
375
|
+
'search for ', 'search the web for ', 'find ', 'look up ',
|
376
|
+
'search web for ', 'web search for ', 'google ', 'find information about '
|
377
|
+
]
|
378
|
+
|
379
|
+
query = content
|
380
|
+
for prefix in prefixes_to_remove:
|
381
|
+
if content_lower.startswith(prefix):
|
382
|
+
query = content[len(prefix):].strip()
|
383
|
+
break
|
384
|
+
|
385
|
+
# If no prefix found, check if it's a search-like message
|
386
|
+
search_indicators = ['search', 'find', 'what is', 'who is', 'how to', 'where is']
|
387
|
+
if not any(indicator in content_lower for indicator in search_indicators):
|
388
|
+
return None
|
389
|
+
|
390
|
+
return query.strip() if query.strip() else None
|
391
|
+
|
392
|
+
def get_provider_status(self) -> Dict[str, Any]:
|
393
|
+
"""Get current provider status"""
|
394
|
+
return {
|
395
|
+
'current_provider': self.current_provider,
|
396
|
+
'providers': {
|
397
|
+
name: {
|
398
|
+
'name': config['name'],
|
399
|
+
'available': config['available'],
|
400
|
+
'priority': config['priority']
|
401
|
+
}
|
402
|
+
for name, config in self.providers.items()
|
403
|
+
}
|
404
|
+
}
|