Chris4K commited on
Commit
a409351
·
verified ·
1 Parent(s): 96127a3

Upload web_search_oracle_addon.py

Browse files

https://huggingface.co/spaces/Agents-MCP-Hackathon/web-search-mcp

Files changed (1) hide show
  1. src/addons/web_search_oracle_addon.py +455 -0
src/addons/web_search_oracle_addon.py ADDED
@@ -0,0 +1,455 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Web Search Oracle Add-on - MCP-powered web search information system.
3
+ """
4
+
5
+ import time
6
+ import json
7
+ import random
8
+ import string
9
+ import asyncio
10
+ from typing import Dict, List, Optional
11
+ import gradio as gr
12
+ from abc import ABC, abstractmethod
13
+ from mcp import ClientSession
14
+ from mcp.client.sse import sse_client
15
+ from contextlib import AsyncExitStack
16
+
17
+ from ..interfaces.npc_addon import NPCAddon
18
+
19
+
20
+ class IWebSearchService(ABC):
21
+ """Interface for Web Search service operations."""
22
+
23
+ @abstractmethod
24
+ def search_web(self, query: str, engine: str = "brave") -> str:
25
+ """Search the web for information."""
26
+ pass
27
+
28
+ @abstractmethod
29
+ def connect_to_mcp(self) -> str:
30
+ """Connect to MCP web search server."""
31
+ pass
32
+
33
+
34
+ class WebSearchOracleService(IWebSearchService, NPCAddon):
35
+ """Service for managing Web Search Oracle MCP integration."""
36
+
37
+ def __init__(self):
38
+ super().__init__()
39
+ self.connected = False
40
+ self.last_connection_attempt = 0
41
+ self.connection_cooldown = 30 # 30 seconds between connection attempts
42
+ self.server_url = "https://agents-mcp-hackathon-web-search-mcp.hf.space/gradio_api/mcp/sse"
43
+ self.session = None
44
+ self.tools = []
45
+ self.exit_stack = None
46
+ self.search_engines = [
47
+ "brave", "duckduckgo", "searxng", "serpapi", "serper", "tavily"
48
+ ]
49
+ # Set up event loop for async operations
50
+ try:
51
+ self.loop = asyncio.get_event_loop()
52
+ except RuntimeError:
53
+ self.loop = asyncio.new_event_loop()
54
+
55
+ @property
56
+ def addon_id(self) -> str:
57
+ """Unique identifier for this add-on"""
58
+ return "web_search_oracle"
59
+
60
+ @property
61
+ def addon_name(self) -> str:
62
+ """Display name for this add-on"""
63
+ return "Web Search Oracle (MCP)"
64
+
65
+ @property
66
+ def npc_config(self) -> Dict:
67
+ """NPC configuration for auto-placement in world"""
68
+ return {
69
+ 'id': 'web_search_oracle',
70
+ 'name': 'Web Search Oracle (MCP)',
71
+ 'x': 450, 'y': 300,
72
+ 'char': '🔍',
73
+ 'type': 'mcp',
74
+ 'description': 'MCP-powered web search information service'
75
+ }
76
+
77
+ @property
78
+ def ui_tab_name(self) -> str:
79
+ """UI tab name for this addon"""
80
+ return "Web Search Oracle"
81
+
82
+ def get_interface(self) -> gr.Component:
83
+ """Return Gradio interface for this add-on"""
84
+ with gr.Column() as interface:
85
+ gr.Markdown("""
86
+ ## 🔍 Web Search Oracle (MCP)
87
+
88
+ *I commune with the digital spirits through the mystical MCP protocol to bring you knowledge from across the web!*
89
+
90
+ **Ask me to search for anything:**
91
+ - Current news and information
92
+ - Technical documentation
93
+ - Research topics
94
+ - General knowledge queries
95
+ - Powered by Model Context Protocol (MCP)
96
+
97
+ *Available search engines: Brave, DuckDuckGo, SearxNG, SerpAPI, Serper, Tavily*
98
+ """)
99
+
100
+ # Connection status
101
+ connection_status = gr.HTML(
102
+ value=f"<div style='color: {'green' if self.connected else 'red'};'>{'🟢 Connected to search spirits' if self.connected else '🔴 Disconnected from search realm'}</div>"
103
+ )
104
+
105
+ with gr.Row():
106
+ query_input = gr.Textbox(
107
+ label="Search Query",
108
+ placeholder="e.g., latest news about AI, Python programming tutorials",
109
+ scale=3
110
+ )
111
+ search_btn = gr.Button("🔍 Consult Search Spirits", variant="primary", scale=1)
112
+
113
+ with gr.Row():
114
+ engine_dropdown = gr.Dropdown(
115
+ choices=self.search_engines,
116
+ value="brave",
117
+ label="Search Engine",
118
+ scale=2
119
+ )
120
+ max_results = gr.Slider(
121
+ minimum=1,
122
+ maximum=10,
123
+ value=5,
124
+ step=1,
125
+ label="Max Results",
126
+ scale=1
127
+ )
128
+
129
+ search_output = gr.Textbox(
130
+ label="🔍 Search Wisdom",
131
+ lines=12,
132
+ interactive=False,
133
+ placeholder="Enter a search query and I will consult the search spirits..."
134
+ )
135
+
136
+ # Example queries
137
+ with gr.Row():
138
+ gr.Examples(
139
+ examples=[
140
+ ["latest AI developments"],
141
+ ["Python programming best practices"],
142
+ ["climate change news 2024"],
143
+ ["machine learning tutorials"],
144
+ ["cryptocurrency market trends"],
145
+ ["space exploration news"],
146
+ ["renewable energy technologies"]
147
+ ],
148
+ inputs=[query_input],
149
+ label="🌐 Try These Searches"
150
+ )
151
+
152
+ # Connection controls
153
+ with gr.Row():
154
+ connect_btn = gr.Button("🔗 Connect to MCP", variant="secondary")
155
+ status_btn = gr.Button("📊 Check Status", variant="secondary")
156
+ tools_btn = gr.Button("🛠️ List Tools", variant="secondary")
157
+
158
+ def handle_search_request(query: str, engine: str, max_results: int):
159
+ if not query.strip():
160
+ return "❓ Please enter a search query to find information."
161
+ return self.search_web(query, engine, max_results)
162
+
163
+ def handle_connect():
164
+ result = self.connect_to_mcp()
165
+ # Update connection status
166
+ new_status = f"<div style='color: {'green' if self.connected else 'red'};'>{'🟢 Connected to search spirits' if self.connected else '🔴 Disconnected from search realm'}</div>"
167
+ return result, new_status
168
+
169
+ def handle_status():
170
+ status = "🟢 Connected" if self.connected else "🔴 Disconnected"
171
+ return f"🔍 **Web Search Oracle Status**\n\nConnection: {status}\nLast update: {time.strftime('%H:%M')}\nAvailable engines: {', '.join(self.search_engines)}"
172
+
173
+ def handle_list_tools():
174
+ if not self.connected:
175
+ return "❌ Not connected to MCP server. Please connect first."
176
+ if not self.tools:
177
+ return "❌ No tools available."
178
+
179
+ tool_info = "🛠️ **Available Search Tools:**\n\n"
180
+ for tool in self.tools:
181
+ tool_info += f"• **{tool.name}**: {tool.description}\n"
182
+ return tool_info
183
+
184
+ # Wire up events
185
+ search_btn.click(
186
+ handle_search_request,
187
+ inputs=[query_input, engine_dropdown, max_results],
188
+ outputs=[search_output]
189
+ )
190
+
191
+ query_input.submit(
192
+ handle_search_request,
193
+ inputs=[query_input, engine_dropdown, max_results],
194
+ outputs=[search_output]
195
+ )
196
+
197
+ connect_btn.click(
198
+ handle_connect,
199
+ outputs=[search_output, connection_status]
200
+ )
201
+
202
+ status_btn.click(
203
+ handle_status,
204
+ outputs=[search_output]
205
+ )
206
+
207
+ tools_btn.click(
208
+ handle_list_tools,
209
+ outputs=[search_output]
210
+ )
211
+
212
+ return interface
213
+
214
+ def connect_to_mcp(self) -> str:
215
+ """Connect to MCP web search server."""
216
+ current_time = time.time()
217
+ if current_time - self.last_connection_attempt < self.connection_cooldown:
218
+ return "⏳ Please wait before retrying connection..."
219
+
220
+ self.last_connection_attempt = current_time
221
+
222
+ try:
223
+ return self.loop.run_until_complete(self._connect())
224
+ except Exception as e:
225
+ self.connected = False
226
+ return f"❌ Connection failed: {str(e)}"
227
+
228
+ async def _connect(self) -> str:
229
+ """Async connect to MCP server using SSE."""
230
+ try:
231
+ # Clean up previous connection
232
+ if self.exit_stack:
233
+ await self.exit_stack.aclose()
234
+
235
+ self.exit_stack = AsyncExitStack()
236
+
237
+ # Connect to SSE MCP server
238
+ sse_transport = await self.exit_stack.enter_async_context(
239
+ sse_client(self.server_url)
240
+ )
241
+ read_stream, write_callable = sse_transport
242
+
243
+ self.session = await self.exit_stack.enter_async_context(
244
+ ClientSession(read_stream, write_callable)
245
+ )
246
+ await self.session.initialize()
247
+
248
+ # Get available tools
249
+ response = await self.session.list_tools()
250
+ self.tools = response.tools
251
+
252
+ self.connected = True
253
+ tool_names = [tool.name for tool in self.tools]
254
+ return f"✅ Connected to web search MCP server!\nAvailable tools: {', '.join(tool_names)}"
255
+
256
+ except Exception as e:
257
+ self.connected = False
258
+ return f"❌ Connection failed: {str(e)}"
259
+
260
+ def search_web(self, query: str, engine: str = "brave", max_results: int = 5) -> str:
261
+ """Search the web using actual MCP server"""
262
+ if not self.connected:
263
+ # Try to auto-connect
264
+ connect_result = self.connect_to_mcp()
265
+ if not self.connected:
266
+ return f"❌ Failed to connect to search server. {connect_result}"
267
+
268
+ if not query.strip():
269
+ return "❌ Please enter a search query"
270
+
271
+ try:
272
+ return self.loop.run_until_complete(self._search_web(query, engine, max_results))
273
+ except Exception as e:
274
+ return f"❌ Error performing search: {str(e)}"
275
+
276
+ async def _search_web(self, query: str, engine: str, max_results: int) -> str:
277
+ """Async search web using MCP."""
278
+ try:
279
+ # Find the appropriate search tool based on engine
280
+ tool_name = f"web_search_mcp_{engine}_search"
281
+ search_tool = next((tool for tool in self.tools if tool.name == tool_name), None)
282
+
283
+ if not search_tool:
284
+ # Try to find any search tool if specific engine not found
285
+ search_tool = next((tool for tool in self.tools if 'search' in tool.name.lower()), None)
286
+ if not search_tool:
287
+ return "❌ No search tools found on server"
288
+
289
+ # Call the tool
290
+ params = {"query": query}
291
+ result = await self.session.call_tool(search_tool.name, params)
292
+
293
+ # Extract content properly
294
+ content_text = ""
295
+ if hasattr(result, 'content') and result.content:
296
+ if isinstance(result.content, list):
297
+ for content_item in result.content:
298
+ if hasattr(content_item, 'text'):
299
+ content_text += content_item.text
300
+ elif hasattr(content_item, 'content'):
301
+ content_text += str(content_item.content)
302
+ else:
303
+ content_text += str(content_item)
304
+ elif hasattr(result.content, 'text'):
305
+ content_text = result.content.text
306
+ else:
307
+ content_text = str(result.content)
308
+
309
+ if not content_text:
310
+ return "❌ No content received from server"
311
+
312
+ try:
313
+ # Try to parse as JSON
314
+ parsed = json.loads(content_text)
315
+ if isinstance(parsed, dict):
316
+ if 'error' in parsed:
317
+ return f"❌ Error: {parsed['error']}"
318
+
319
+ # Format search results nicely
320
+ formatted = f"🔍 **Search Results for: '{query}'**\n"
321
+ formatted += f"🌐 **Engine: {engine.title()}**\n\n"
322
+
323
+ # Handle different response formats
324
+ if 'data' in parsed and isinstance(parsed['data'], list):
325
+ results = parsed['data'][:max_results]
326
+ for i, result_item in enumerate(results, 1):
327
+ if isinstance(result_item, list) and len(result_item) >= 3:
328
+ title, link, body = result_item[0], result_item[1], result_item[2]
329
+ formatted += f"**{i}. {title}**\n"
330
+ formatted += f"🔗 {link}\n"
331
+ formatted += f"📝 {body[:200]}{'...' if len(body) > 200 else ''}\n\n"
332
+ elif isinstance(result_item, dict):
333
+ title = result_item.get('title', 'No title')
334
+ link = result_item.get('link', 'No link')
335
+ body = result_item.get('body', 'No description')
336
+ formatted += f"**{i}. {title}**\n"
337
+ formatted += f"🔗 {link}\n"
338
+ formatted += f"📝 {body[:200]}{'...' if len(body) > 200 else ''}\n\n"
339
+ elif 'results' in parsed:
340
+ results = parsed['results'][:max_results]
341
+ for i, result_item in enumerate(results, 1):
342
+ title = result_item.get('title', 'No title')
343
+ link = result_item.get('url', result_item.get('link', 'No link'))
344
+ body = result_item.get('snippet', result_item.get('body', 'No description'))
345
+ formatted += f"**{i}. {title}**\n"
346
+ formatted += f"🔗 {link}\n"
347
+ formatted += f"📝 {body[:200]}{'...' if len(body) > 200 else ''}\n\n"
348
+ else:
349
+ # Fallback to raw JSON display
350
+ formatted += f"✅ Raw search data:\n```json\n{json.dumps(parsed, indent=2)}\n```"
351
+
352
+ formatted += f"\n⏰ Search completed: {time.strftime('%H:%M')}\n⚡ **Powered by MCP**"
353
+ return formatted
354
+
355
+ except json.JSONDecodeError:
356
+ # If not JSON, try to format as text
357
+ formatted = f"🔍 **Search Results for: '{query}'**\n"
358
+ formatted += f"🌐 **Engine: {engine.title()}**\n\n"
359
+ formatted += f"✅ Search data:\n```\n{content_text[:1000]}{'...' if len(content_text) > 1000 else ''}\n```\n\n"
360
+ formatted += f"⏰ Search completed: {time.strftime('%H:%M')}\n⚡ **Powered by MCP**"
361
+ return formatted
362
+
363
+ return f"🔍 **Search Results for: '{query}'**\n\n✅ Raw result:\n{content_text[:1000]}{'...' if len(content_text) > 1000 else ''}\n\n⚡ **Powered by MCP**"
364
+
365
+ except Exception as e:
366
+ return f"❌ Failed to search: {str(e)}"
367
+
368
+ def handle_command(self, player_id: str, command: str) -> str:
369
+ """Handle Web Search Oracle commands."""
370
+ parts = command.strip().split(' ', 2)
371
+ cmd = parts[0].lower()
372
+
373
+ if cmd == "search" and len(parts) > 1:
374
+ query = parts[1]
375
+ engine = "brave" # default
376
+ if len(parts) > 2 and parts[2] in self.search_engines:
377
+ engine = parts[2]
378
+ return self.search_web(query, engine)
379
+ elif cmd == "engines":
380
+ return f"🔍 **Available Search Engines:**\n\n{', '.join(self.search_engines)}\n\n**Usage:** `search <query> [engine]`"
381
+ elif cmd == "connect":
382
+ return self.connect_to_mcp()
383
+ elif cmd == "status":
384
+ status = "🟢 Connected" if self.connected else "🔴 Disconnected"
385
+ return f"🔍 **Web Search Oracle Status**\n\nConnection: {status}\nLast update: {time.strftime('%H:%M')}\nAvailable engines: {', '.join(self.search_engines)}"
386
+ elif cmd == "tools":
387
+ if not self.connected:
388
+ return "❌ Not connected to MCP server. Use `connect` first."
389
+ if not self.tools:
390
+ return "❌ No tools available."
391
+
392
+ tool_info = "🛠️ **Available Search Tools:**\n\n"
393
+ for tool in self.tools:
394
+ tool_info += f"• **{tool.name}**: {tool.description}\n"
395
+ return tool_info
396
+ elif cmd == "help":
397
+ return """🔍 **Web Search Oracle Commands:**
398
+
399
+ **search** `<query>` `[engine]` - Search the web (e.g., 'search AI news brave')
400
+ **engines** - List available search engines
401
+ **connect** - Connect to MCP search server
402
+ **status** - Check connection status
403
+ **tools** - List available MCP tools
404
+ **help** - Show this help
405
+
406
+ 🌐 **Available Engines:**
407
+ {engines}
408
+
409
+ 🔍 **Example Commands:**
410
+ • search latest AI developments
411
+ • search Python tutorials duckduckgo
412
+ • search climate change news searxng
413
+
414
+ ⚡ **Powered by MCP (Model Context Protocol)**""".format(engines=', '.join(self.search_engines))
415
+ else:
416
+ return "❓ Invalid command. Try: `search <query>`, `engines`, `connect`, `status`, `tools`, or `help`"
417
+
418
+
419
+ # Global Web Search Oracle service instance
420
+ web_search_oracle_service = WebSearchOracleService()
421
+
422
+
423
+ def auto_register(game_engine):
424
+ """Auto-register the Web Search Oracle addon with the game engine.
425
+
426
+ This function makes the addon self-contained by handling its own registration.
427
+ """
428
+ try:
429
+ # Create the web search oracle NPC definition
430
+ web_search_oracle_npc = {
431
+ 'id': 'web_search_oracle_auto',
432
+ 'name': '🔍 Web Search Oracle (Auto)',
433
+ 'x': 450, 'y': 150,
434
+ 'char': '🔍',
435
+ 'type': 'mcp',
436
+ 'personality': 'web_search_oracle',
437
+ 'description': 'Self-contained MCP-powered web search information service'
438
+ }
439
+
440
+ # Register the NPC with the NPC service
441
+ npc_service = game_engine.get_npc_service()
442
+ npc_service.register_npc('web_search_oracle_auto', web_search_oracle_npc)
443
+
444
+ # Register the addon for handling private message commands
445
+ world = game_engine.get_world()
446
+ if not hasattr(world, 'addon_npcs'):
447
+ world.addon_npcs = {}
448
+ world.addon_npcs['web_search_oracle_auto'] = web_search_oracle_service
449
+
450
+ print("[WebSearchOracleAddon] Auto-registered successfully as self-contained addon")
451
+ return True
452
+
453
+ except Exception as e:
454
+ print(f"[WebSearchOracleAddon] Error during auto-registration: {e}")
455
+ return False