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,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
+ }