workingshem commited on
Commit
2f67bbe
·
1 Parent(s): ff4747e

Update newest technology

Browse files
Files changed (4) hide show
  1. README.md +1 -0
  2. app.py +413 -4
  3. gradio_mcp_server.py +512 -0
  4. recom.py +25 -0
README.md CHANGED
@@ -9,6 +9,7 @@ app_file: app.py
9
  pinned: false
10
  license: mit
11
  short_description: 'Connection Search for Real World Location to a Map '
 
12
  ---
13
 
14
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
9
  pinned: false
10
  license: mit
11
  short_description: 'Connection Search for Real World Location to a Map '
12
+ long_description: 'This allows you to prompt to LLM searching for a Point of Interest like restaurants, parks, or any things that has location and the MCP Server will convert those location into visualizeable coordinates and display in the map"
13
  ---
14
 
15
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py CHANGED
@@ -1,7 +1,416 @@
 
 
 
 
 
 
 
1
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- def greet(name):
4
- return "Hello " + name + "!!"
5
 
6
- demo = gr.Interface(fn=greet, inputs="text", outputs="text")
7
- demo.launch()
 
 
 
 
 
1
+ import asyncio
2
+ import os
3
+ import json
4
+ from typing import List, Dict, Any, Union, Optional
5
+ from contextlib import AsyncExitStack
6
+ from pydantic import BaseModel, Field
7
+
8
  import gradio as gr
9
+ from gradio.components.chatbot import ChatMessage
10
+ from mcp import ClientSession, StdioServerParameters
11
+ from mcp.client.stdio import stdio_client
12
+ from anthropic import Anthropic
13
+ from dotenv import load_dotenv
14
+ from recom import RecommendationUI
15
+
16
+ load_dotenv()
17
+
18
+ # Define Pydantic models for structured responses
19
+ class ToolResponse(BaseModel):
20
+ status_goal: str = Field(..., description="Status of the goal: 'completed', 'in_progress', or 'impossible'")
21
+ next_steps: List[str] = Field(default_factory=list, description="List of next actions needed")
22
+ tool_suggestions: str = Field(..., description="Analysis of whether the goal is achievable with current tools")
23
+ tool_result: Optional[str] = Field(None, description="Result from the tool execution")
24
+
25
+ class MessageResponse(BaseModel):
26
+ role: str = Field(..., description="Role of the message sender")
27
+ content: str = Field(..., description="Content of the message")
28
+ metadata: Optional[Dict[str, Any]] = Field(None, description="Additional metadata for the message")
29
+
30
+
31
+
32
+ # To handle UI/UX outside of gradio_interface function. Especially when updating maps.
33
+ _recom_UI = RecommendationUI()
34
+
35
+ loop = asyncio.new_event_loop()
36
+ asyncio.set_event_loop(loop)
37
+
38
+ class MCPClientWrapper:
39
+ def __init__(self):
40
+ self.session = None
41
+ self.exit_stack = None
42
+ self.anthropic = Anthropic()
43
+ self.tools = []
44
+
45
+ def connect(self, server_path: str) -> str:
46
+ return loop.run_until_complete(self._connect(server_path))
47
+
48
+ async def _connect(self, server_path: str) -> str:
49
+ if self.exit_stack:
50
+ await self.exit_stack.aclose()
51
+
52
+ self.exit_stack = AsyncExitStack()
53
+
54
+ is_python = server_path.endswith('.py')
55
+ command = "python" if is_python else "node"
56
+
57
+ server_params = StdioServerParameters(
58
+ command=command,
59
+ args=[server_path],
60
+ env={"PYTHONIOENCODING": "utf-8", "PYTHONUNBUFFERED": "1"}
61
+ )
62
+
63
+ stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
64
+ self.stdio, self.write = stdio_transport
65
+
66
+ self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
67
+ await self.session.initialize()
68
+
69
+ response = await self.session.list_tools()
70
+ self.tools = [{
71
+ "name": tool.name,
72
+ "description": tool.description,
73
+ "input_schema": tool.inputSchema
74
+ } for tool in response.tools]
75
+
76
+ tool_names = [tool["name"] for tool in self.tools]
77
+ return f"Connected to MCP server. Available tools: {', '.join(tool_names)}"
78
+
79
+ def process_message(self, message: str, history: List[Union[Dict[str, Any], ChatMessage]]) -> tuple:
80
+ global current_lat,current_lon
81
+ current_status = "in_progress"
82
+ new_message = (f"Current Status: {current_status}\n" +
83
+ f"Query: {message}\n"
84
+ + f" ,Current Coordinates: long: {current_lon}, lat: {current_lat}"
85
+ )
86
+
87
+ if not self.session:
88
+ return history + [
89
+ {"role": "user", "content": new_message},
90
+ {"role": "assistant", "content": "Please connect to an MCP server first."}
91
+ ], gr.Textbox(value="")
92
+
93
+ after_process_msg = loop.run_until_complete(self._process_query(new_message, history))
94
+ return history + [{"role": "user", "content": new_message}] + after_process_msg, gr.Textbox(value="")
95
+
96
+ async def _process_query(self, message: str, history: List[Union[Dict[str, Any], ChatMessage]]):
97
+ claude_messages = []
98
+ for msg in history:
99
+ if isinstance(msg, ChatMessage):
100
+ role, content = msg.role, msg.content
101
+ else:
102
+ role, content = msg.get("role"), msg.get("content")
103
+
104
+ if role in ["user", "assistant", "system"]:
105
+ claude_messages.append({"role": role, "content": content})
106
+
107
+ claude_messages.append({"role": "user", "content": message})
108
+ # First Run
109
+ response = self.anthropic.messages.create(
110
+ model="claude-3-5-sonnet-20241022",
111
+ max_tokens=1000,
112
+ messages=claude_messages,
113
+ tools=self.tools
114
+ )
115
+
116
+ result_messages = []
117
+
118
+ for content in response.content:
119
+ if content.type == 'text':
120
+ result_messages.append(MessageResponse(
121
+ role="assistant",
122
+ content=content.text
123
+ ).dict())
124
+ # If need to use tools
125
+ elif content.type == 'tool_use':
126
+ tool_name = content.name
127
+ tool_args = content.input
128
+ # The step by step tool uses
129
+ result_messages.append(MessageResponse(
130
+ role="assistant",
131
+ content=f"I'll use the {tool_name} tool to help answer your question.",
132
+ metadata={
133
+ "title": f"Using tool: {tool_name}",
134
+ "log": f"Parameters: {json.dumps(tool_args, ensure_ascii=True)}",
135
+ "status": "pending",
136
+ "id": f"tool_call_{tool_name}"
137
+ }
138
+ ).dict())
139
+ # The parameters passed to process with the tools
140
+ result_messages.append(MessageResponse(
141
+ role="assistant",
142
+ content="```json\n" + json.dumps(tool_args, indent=2, ensure_ascii=True) + "\n```",
143
+ metadata={
144
+ "parent_id": f"tool_call_{tool_name}",
145
+ "id": f"params_{tool_name}",
146
+ "title": "Tool Parameters"
147
+ }
148
+ ).dict())
149
+ # Execute the tools
150
+ result = await self.session.call_tool(tool_name, tool_args)
151
+
152
+ # Status done if the tools managed to be executed
153
+ if result_messages and "metadata" in result_messages[-2]:
154
+ result_messages[-2]["metadata"]["status"] = "done"
155
+
156
+ # The result from the tools into a message response
157
+ result_messages.append(MessageResponse(
158
+ role="assistant",
159
+ content="Here are the results from the tool:",
160
+ metadata={
161
+ "title": f"Tool Result for {tool_name}",
162
+ "status": "done",
163
+ "id": f"result_{tool_name}"
164
+ }
165
+ ).dict())
166
+ # Retrieve the response content
167
+ result_content = result.content
168
+ if isinstance(result_content, list):
169
+ result_content = "\n".join(str(item) for item in result_content)
170
+
171
+ try:
172
+ result_json = json.loads(result_content)
173
+ if isinstance(result_json, dict) and "type" in result_json:
174
+ if result_json["type"] == "image" and "url" in result_json:
175
+ result_messages.append(MessageResponse(
176
+ role="assistant",
177
+ content={"path": result_json["url"], "alt_text": result_json.get("message", "Generated image")},
178
+ metadata={
179
+ "parent_id": f"result_{tool_name}",
180
+ "id": f"image_{tool_name}",
181
+ "title": "Generated Image"
182
+ }
183
+ ).dict())
184
+ else:
185
+ result_messages.append(MessageResponse(
186
+ role="assistant",
187
+ content="```\n" + result_content + "\n```",
188
+ metadata={
189
+ "parent_id": f"result_{tool_name}",
190
+ "id": f"raw_result_{tool_name}",
191
+ "title": "Raw Output"
192
+ }
193
+ ).dict())
194
+ except:
195
+ result_messages.append(MessageResponse(
196
+ role="assistant",
197
+ content="```\n" + result_content + "\n```",
198
+ metadata={
199
+ "parent_id": f"result_{tool_name}",
200
+ "id": f"raw_result_{tool_name}",
201
+ "title": "Raw Output"
202
+ }
203
+ ).dict())
204
+
205
+ if tool_name == 'convert_entities_to_geojson_and_update_map':
206
+ _recom_UI.update_geojson(result.content[0].text)
207
+
208
+ tool_response = ToolResponse(
209
+ status_goal="in_progress",
210
+ next_steps=["Analyze tool results", "Determine next actions"],
211
+ tool_suggestions="Current tools are sufficient for the task",
212
+ tool_result=result_content
213
+ )
214
+ # Reasoning whether the model should complete / stop the step due to impossible task.
215
+ claude_messages.append({
216
+ "role": "user",
217
+ "content": f"""Analyze if the tools have accomplished the user's goal and what needs to be done next.
218
+ Tool result for {tool_name}: {result_content}"""
219
+ """Respond with JSON in this exact format:
220
+ \{
221
+ "status_goal": "Look at the available tools first and decide the status of next step:
222
+ completed (the current result is aligned with user's goal
223
+ |in_progress (the next step could be done with the current set of tools
224
+ |impossible (It's impossible to achieve the user's goal with the current tools)",
225
+ "next_steps": ["action1", "action2"],
226
+ "tool_suggestions": "your analysis here.",
227
+ "tool_result": "look at the existing result"
228
+ \}
229
+ """
230
+ })
231
+
232
+ # Retreive the next response.
233
+ next_response = self.anthropic.messages.create(
234
+ model="claude-3-5-sonnet-20241022",
235
+ max_tokens=1000,
236
+ messages=claude_messages,
237
+ tools=self.tools
238
+ )
239
+
240
+ try:
241
+ import re
242
+
243
+ # Search for JSON pattern in the response text using regex because
244
+ json_match = re.search(r'\{.*\}', next_response.content[0].text, re.DOTALL)
245
+
246
+ # Parse the matched JSON string if found, otherwise use empty dict
247
+ next_response_json = json.loads(json_match.group(0).replace("\n","")) if json_match else {}
248
+
249
+ # Create ToolResponse object from parsed JSON data
250
+ # ** operator unpacks the dictionary into keyword arguments
251
+ tool_response = ToolResponse(**next_response_json)
252
+
253
+ # Check if the tool execution is still in progress
254
+ if tool_response.status_goal == "in_progress":
255
+ # Recursively process the query with updated messages
256
+ recursive_messages = await self._process_query(next_response.content[0].text, claude_messages)
257
+ # Extend the result messages list with new recursive messages
258
+ result_messages.extend(recursive_messages)
259
+
260
+ except json.JSONDecodeError as e:
261
+ print(f"Failed to parse response as JSON {e} : {next_response.content[0]}")
262
+
263
+ if next_response.content and next_response.content[0].type == 'text':
264
+ result_messages.append(MessageResponse(
265
+ role="assistant",
266
+ content=next_response.content[0].text
267
+ ).dict())
268
+
269
+ return result_messages
270
+
271
+ client = MCPClientWrapper()
272
+ # Store selected coordinates
273
+ current_lat = -6.1944
274
+ current_lon = 106.8229
275
+
276
+
277
+ def refreshed_map():
278
+ global _recom_UI
279
+ import plotly.graph_objects as go
280
+ import geopandas as gpd
281
+ import pandas as pd
282
+ import subprocess
283
+ import urllib.parse
284
+
285
+ """
286
+ Function to refresh the map with new GeoJSON data.
287
+ This can be called from anywhere to update the map.
288
+ """
289
+ df_points = pd.DataFrame()
290
+
291
+ fig = go.Figure()
292
+ # Add the current location marker
293
+ fig.add_trace(go.Scattermapbox(
294
+ lat=[current_lat],
295
+ lon=[current_lon],
296
+ mode='markers',
297
+ marker=dict(size=20, color='blue'),
298
+ name='Current Location',
299
+ hovertemplate='Jakarta Map Data<br>Lat: %{lat}<br>Lon: %{lon}<extra></extra>'
300
+ ))
301
+
302
+ # Add GeoJSON data if available
303
+ if _recom_UI.get_current_geojson()['features']:
304
+ existing_feature = _recom_UI.get_current_geojson()['features']
305
+ gdf = gpd.GeoDataFrame.from_features(existing_feature)
306
+ df_points = pd.DataFrame(gdf.drop(columns='geometry'))
307
+ df_points['url'] = df_points.apply(lambda x: f"https://maps.google.com/?q={x['name'].replace(' ','+')}",axis=1)
308
+ df_points['url2'] = df_points.apply(lambda x: f"https://maps.google.com/?ll={x['latitude']},{x['longitude']}",axis=1)
309
+
310
+ # Add the DataFrame points
311
+ fig.add_trace(go.Scattermapbox(
312
+ lat=df_points["latitude"],
313
+ lon=df_points["longitude"],
314
+ mode='markers',
315
+ marker=dict(size=10, color='red'),
316
+ name='Points of Interest',
317
+ text=df_points["name"],
318
+ hovertemplate='<b>%{text}</b><br>' +
319
+ 'Location: %{customdata[0]}<br>' +
320
+ 'Description: %{customdata[1]}<br>' +
321
+ 'URL by Name: <a href="%{customdata[2]}" target="_blank" style="color: #007bff; text-decoration: underline;">%{customdata[2]}</a><br>' +
322
+ 'URL by Coordinates: <a href="%{customdata[3]}" target="_blank" style="color: #007bff; text-decoration: underline;">%{customdata[2]}</a><br>' +
323
+ 'Lat: %{lat}<br>Lon: %{lon}<extra></extra>',
324
+ customdata=df_points[["location", "description", 'url','url2']].values
325
+ ))
326
+
327
+ fig.update_layout(
328
+ mapbox_style="carto-positron",
329
+ mapbox=dict(
330
+ center=dict(lat=current_lat, lon=current_lon),
331
+ zoom=12
332
+ ),
333
+ margin=dict(l=0, r=0, t=0, b=0),
334
+ height=600,
335
+ dragmode='pan',
336
+ showlegend=True
337
+ )
338
+
339
+ # Activate click event (Difficult)
340
+ # if _recom_UI.get_current_geojson()['features']:
341
+ # # Create the plot configuration with click handling
342
+ # config = {
343
+ # 'displayModeBar': True,
344
+ # 'displaylogo': False
345
+ # }
346
+ # # Show plot and add JavaScript callback
347
+ # fig.show(config=config)
348
+
349
+ # # Add the click event handling
350
+ # def open_url_in_windows(point_index):
351
+ # url = df_points.iloc[point_index]['url']
352
+ # subprocess.run(["wslview", url])
353
+
354
+ # fig.data[1].on_click(lambda trace, points, selector:
355
+ # [open_url_in_windows(point.point_index) for point in points.point_indices])
356
+
357
+ return fig
358
+
359
+ def gradio_interface():
360
+ _demo = _recom_UI.get_demo()
361
+
362
+ with _demo:
363
+ gr.Markdown("# MCP Recommendation Assistant")
364
+ gr.Markdown("""Connect to your MCP to extract viral location like food, drinks, restaurants, parks and
365
+ any categories that exist on the internet""")
366
+
367
+ with gr.Row(equal_height=True):
368
+ with gr.Column(scale=4):
369
+ server_path = gr.Textbox(
370
+ label="Server Script Path",
371
+ placeholder="Enter path to server script (e.g., weather.py)",
372
+ value="gradio_mcp_server.py"
373
+ )
374
+ with gr.Column(scale=1):
375
+ connect_btn = gr.Button("Connect")
376
+
377
+ status = gr.Textbox(label="Connection Status", interactive=False)
378
+
379
+ map = gr.Plot()
380
+ _demo.load(refreshed_map,[], map)
381
+ # btn.click(filter_map, map)
382
+
383
+ chatbot = gr.Chatbot(
384
+ value=[],
385
+ height=500,
386
+ type="messages",
387
+ show_copy_button=True,
388
+ avatar_images=("👤", "🤖")
389
+ )
390
+
391
+ with gr.Row(equal_height=True):
392
+ msg = gr.Textbox(
393
+ label="Your Question",
394
+ placeholder="Ask about what you want to display(e.g., Top 5 nearby restaurants, clinics, hospitals or anything.)",
395
+ scale=4
396
+ )
397
+
398
+ clear_btn = gr.Button("Clear Chat", scale=1)
399
+
400
+ connect_btn.click(client.connect, inputs=server_path, outputs=status)
401
+ msg.submit(client.process_message,
402
+ [msg, chatbot],
403
+ [chatbot, msg])
404
+ clear_btn.click(lambda: [], None, chatbot)
405
+
406
+ timer = gr.Timer(10)
407
+ timer.tick(refreshed_map,[],outputs=[map])
408
 
409
+ return _demo
 
410
 
411
+ if __name__ == "__main__":
412
+ if not os.getenv("ANTHROPIC_API_KEY"):
413
+ print("Warning: ANTHROPIC_API_KEY not found in environment. Please set it in your .env file.")
414
+
415
+ interface = gradio_interface()
416
+ interface.launch(debug=True)
gradio_mcp_server.py ADDED
@@ -0,0 +1,512 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from mcp.server.fastmcp import FastMCP
2
+ import json
3
+ import sys
4
+ import io
5
+ import time
6
+ from gradio_client import Client
7
+ import urllib
8
+ from pydantic import BaseModel, Field
9
+ import os
10
+ from recom import RecommendationUI
11
+
12
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
13
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
14
+
15
+
16
+ class ExtractedInfo(BaseModel):
17
+ """Schema for extracted information"""
18
+ name: str = Field(..., description="Name of the business, organization, or entity")
19
+ address: str = Field(..., description="Detail address of the business or geographical information.")
20
+ description: str = Field(..., description="Description or summary of the entity")
21
+
22
+ mcp = FastMCP("huggingface_spaces_image_display")
23
+
24
+
25
+ @mcp.tool()
26
+ async def reverse_geocode(latitude: float, longitude: float) -> str:
27
+ """Convert geographic coordinates into a human-readable address using the Nominatim API.
28
+
29
+ This tool provides a more reliable and structured way to get location information from coordinates
30
+ compared to direct Google searches. It's particularly useful when:
31
+ - Working with GPS coordinates from devices or applications
32
+ - Need precise address information for a specific location
33
+ - Want to avoid the limitations of browser-based coordinate searches
34
+ - Need programmatic access to location data
35
+
36
+ The tool uses OpenStreetMap's Nominatim service, which provides detailed address components
37
+ including street names, neighborhoods, cities, states, and countries. This structured approach
38
+ is more reliable than trying to interpret coordinate-based Google search results.
39
+
40
+ Args:
41
+ latitude: The latitude coordinate (must be between -90 and 90)
42
+ longitude: The longitude coordinate (must be between -180 and 180)
43
+
44
+ Returns:
45
+ A JSON string containing:
46
+ - type: "address" for successful geocoding or "error" for failures
47
+ - content: Structured address data including:
48
+ - display_name: Full formatted address
49
+ - road: Street name
50
+ - suburb: Neighborhood or district
51
+ - city: City or town
52
+ - state: State or region
53
+ - country: Country name
54
+ - postcode: Postal/ZIP code
55
+ - message: Status message about the geocoding result
56
+
57
+ Example:
58
+ >>> result = await reverse_geocode(40.7128, -74.0060)
59
+ >>> print(result)
60
+ {
61
+ "type": "address",
62
+ "content": {
63
+ "display_name": "New York, NY, USA",
64
+ "road": "Broadway",
65
+ "suburb": "Manhattan",
66
+ "city": "New York",
67
+ "state": "New York",
68
+ "country": "United States",
69
+ "postcode": "10007"
70
+ },
71
+ "message": "Found address for coordinates: 40.7128, -74.0060"
72
+ }
73
+
74
+ Usage Notes:
75
+ - More reliable than browser-based coordinate searches
76
+ - Provides structured, machine-readable address data
77
+ - Can be used programmatically in applications
78
+ - Respects rate limits and usage policies
79
+ - Works well for both urban and rural locations
80
+ """
81
+ try:
82
+ import requests
83
+
84
+ # Use Nominatim API for reverse geocoding
85
+ url = f"https://nominatim.openstreetmap.org/reverse?format=json&lat={latitude}&lon={longitude}"
86
+ headers = {
87
+ 'User-Agent': 'MCPGeocodingTool/1.0' # Required by Nominatim's usage policy
88
+ }
89
+
90
+ response = requests.get(url, headers=headers)
91
+ response.raise_for_status()
92
+
93
+ data = response.json()
94
+
95
+ # Extract relevant address components
96
+ address = {
97
+ "display_name": data.get("display_name", ""),
98
+ "road": data.get("address", {}).get("road", ""),
99
+ "suburb": data.get("address", {}).get("suburb", ""),
100
+ "display_name": data.get("display_name", ""),
101
+ "city": data.get("address", {}).get("city", ""),
102
+ "state": data.get("address", {}).get("state", ""),
103
+ "country": data.get("address", {}).get("country", ""),
104
+ "postcode": data.get("address", {}).get("postcode", "")
105
+ }
106
+
107
+ return json.dumps({
108
+ "type": "address",
109
+ "content": address,
110
+ "message": f"Found address for coordinates: {latitude}, {longitude}"
111
+ })
112
+
113
+ except Exception as e:
114
+ return json.dumps({
115
+ "type": "error",
116
+ "message": f"Error performing reverse geocoding: {str(e)}"
117
+ })
118
+
119
+ @mcp.tool()
120
+ async def extract_links(query: str, exclude_external: bool = True, exclude_social: bool = True) -> str:
121
+ """Extract and analyze relevant links from Google search results based on a user's query.
122
+
123
+ This tool performs a Google search for the given query and extracts structured information
124
+ about the search results. It's designed to help users find relevant resources and information
125
+ related to their specific goals or needs. The tool is particularly useful for:
126
+ - Research and information gathering
127
+ - Finding authoritative sources on a topic
128
+ - Discovering related resources and references
129
+ - Building a knowledge base for a specific subject
130
+
131
+ The extracted links are filtered and categorized to provide the most relevant results,
132
+ excluding external and social media links by default to focus on primary content sources.
133
+
134
+ Args:
135
+ query: The search query string that describes the user's information need or goal
136
+ exclude_external: Whether to exclude links to external websites (default: True)
137
+ exclude_social: Whether to exclude social media platform links (default: True)
138
+
139
+ Returns:
140
+ A JSON string containing:
141
+ - type: "link_analysis" for successful extraction or "error" for failures
142
+ - content: Structured data with internal and external links
143
+ - message: Summary of the extraction results
144
+
145
+ Example:
146
+ >>> result = await extract_links("best practices for machine learning")
147
+ >>> print(result)
148
+ {
149
+ "type": "link_analysis",
150
+ "content": {
151
+ "internal": [
152
+ {"href": "https://example.com/ml-guide", "text": "ML Best Practices Guide"},
153
+ {"href": "https://example.com/tutorials", "text": "ML Tutorials"}
154
+ ],
155
+ "external": [
156
+ {"href": "https://external.com/ml-resources", "text": "External Resources"}
157
+ ]
158
+ },
159
+ "message": "Found 2 internal and 1 external links"
160
+ }
161
+
162
+ Usage Notes:
163
+ - Use this tool as the first step in information gathering
164
+ - The extracted links can be used with extract_entity_info() to get detailed information
165
+ - Adjust exclude_external and exclude_social parameters based on your specific needs
166
+ - For academic research, consider setting exclude_external=False to get a broader range of sources
167
+ """
168
+ url = f"https://www.google.com/search?q={urllib.parse.quote(query)}"
169
+
170
+ try:
171
+ from crawl4ai import AsyncWebCrawler, CacheMode, CrawlerRunConfig
172
+
173
+ async with AsyncWebCrawler() as crawler:
174
+ config = CrawlerRunConfig(
175
+ cache_mode=CacheMode.ENABLED,
176
+ exclude_external_links=exclude_external,
177
+ exclude_social_media_links=exclude_social
178
+ )
179
+
180
+ result = await crawler.arun(
181
+ url=url,
182
+ config=config
183
+ )
184
+
185
+ # Format results as structured data
186
+ links_data = {
187
+ "internal": [
188
+ {
189
+ "href": link["href"],
190
+ "text": link["text"]
191
+ } for link in result.links["internal"]
192
+ ],
193
+ "external": [
194
+ {
195
+ "href": link["href"],
196
+ "text": link["text"]
197
+ } for link in result.links["external"]
198
+ ]
199
+ }
200
+
201
+ return json.dumps({
202
+ "type": "link_analysis",
203
+ "content": links_data,
204
+ "message": f"Use this links to pass to extract_entity_info. Total links exists: {len(links_data)}"
205
+ })
206
+
207
+ except Exception as e:
208
+ return json.dumps({
209
+ "type": "error",
210
+ "message": f"Error extracting links: {str(e)}"
211
+ })
212
+
213
+ @mcp.tool()
214
+ async def extract_entity_info(urls_list: list,
215
+ user_query: str,
216
+ anthrophic_api_key:str = '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'
217
+ ) -> str:
218
+ """Extract structured entity information from a website URL using AI-powered web crawling.
219
+
220
+ This tool uses advanced web crawling and LLM-based extraction to identify and extract key information
221
+ about an entity (business, organization, or location) from a given website. It specifically looks for:
222
+ - Name: The official name or title of the entity
223
+ - Location: Physical address or geographical information
224
+ - Description: A concise summary of the entity's purpose or activities
225
+
226
+ The extraction is performed using an AI model that understands webpage context and can identify
227
+ relevant information even when it's not explicitly labeled. The tool excludes navigation elements,
228
+ footers, and other non-content areas to focus on the main information.
229
+
230
+ Args:
231
+ urls_list: List of the url you want to extracts.
232
+ user_query: The goal of the user asking you.
233
+
234
+ Returns:
235
+ A JSON string containing:
236
+ - type: Either "entity_info" for successful extraction or "error" for failures
237
+ - content: The extracted information in a structured format
238
+ - message: A status message describing the result
239
+
240
+ Example:
241
+ >>> result = await extract_entity_info("https://example.com")
242
+ >>> print(result)
243
+ {
244
+ "type": "entity_info",
245
+ "content": {
246
+ "name": "Example Corp",
247
+ "location": "123 Main St, City, Country",
248
+ "description": "Leading provider of example services"
249
+ },
250
+ "message": "Successfully extracted entity information from https://example.com"
251
+ }
252
+ """
253
+ try:
254
+ from crawl4ai import AsyncWebCrawler, CacheMode, CrawlerRunConfig, LLMExtractionStrategy, LLMConfig
255
+ from pydantic import BaseModel
256
+ from typing import Optional
257
+ # Convert results to GeoJSON format
258
+ import geopandas as gpd
259
+ from shapely.geometry import Point
260
+ import pandas as pd
261
+ # # Verify API key is available
262
+ # api_key = os.getenv("ANTHROPIC_API_KEY")
263
+ # if not api_key:
264
+ # raise ValueError("ANTHROPIC_API_KEY environment variable is not set")
265
+
266
+ # print(f"API Key found: {'*' * 4}{api_key[-4:] if api_key else 'None'}") # Only print last 4 chars for security
267
+
268
+ # Initialize crawler with LLM extraction strategy
269
+ extraction_strategy = LLMExtractionStrategy(
270
+ llm_config=LLMConfig(
271
+ provider="anthropic/claude-3-5-sonnet-20241022",
272
+ api_token=anthrophic_api_key
273
+ ),
274
+ schema=ExtractedInfo.schema(),
275
+ extraction_type="schema",
276
+ instruction=f"""
277
+ REMEMBER USER'S QUERIES: {user_query}
278
+ Extract the following information from the website:
279
+ 1. Name: The main name, title, or business name of the entity, according to the user's queries
280
+ 2. Location: Any address, location, or geographical information, according to the user's queries
281
+ 3. Description: A brief description or summary of what this entity does or is about.
282
+
283
+ Be concise and accurate. If information is not available, use "Not found" as the value.
284
+ """
285
+ )
286
+ async with AsyncWebCrawler(verbose=True) as crawler:
287
+ config = CrawlerRunConfig(
288
+ cache_mode=CacheMode.ENABLED,
289
+ extraction_strategy=extraction_strategy,
290
+ excluded_tags=['nav', 'footer', 'script', 'style'],
291
+ remove_overlay_elements=True,
292
+ word_count_threshold=1
293
+ )
294
+
295
+ results = []
296
+ extracted_data_list = []
297
+ for url in urls_list:
298
+ result = await crawler.arun(url=url, config=config)
299
+ if result.success:
300
+ extracted_data = json.loads(result.extracted_content)
301
+ extracted_data_list.extend(extracted_data)
302
+ else:
303
+ print(f"Failed to extract entity information from {url}")
304
+
305
+ results.append({
306
+ "type": "entity_info",
307
+ "content": extracted_data_list,
308
+ "message": f"Successfully extracted entity information from {len(urls_list)} URLs"
309
+ })
310
+
311
+ return json.dumps({
312
+ "type": "batch_results",
313
+ "content": results,
314
+ "message": f"Processed {len(urls_list)} URLs"
315
+ })
316
+
317
+ except Exception as e:
318
+ return json.dumps({
319
+ "type": "error",
320
+ "message": f"Error extracting entity information: {str(e)}"
321
+ })
322
+
323
+ @mcp.tool()
324
+ async def extract_coordinates_from_address(entity_info :list):
325
+ """Extract geographical coordinates for entities using Google Maps automation.
326
+
327
+ This function takes a list of entities and uses automated browser interaction with Google Maps
328
+ to find and extract their precise geographical coordinates. The extracted coordinates are then
329
+ used by plot_geojson_unto_map to create an interactive map visualization.
330
+
331
+ This tool is particularly useful for:
332
+ - Converting business/place names into precise coordinates
333
+ - Preparing data for map visualizations
334
+ - Geocoding multiple locations in batch
335
+ - Supporting location-based services and applications
336
+
337
+ Args:
338
+ entity_info (list): A list of dictionaries containing entity information.
339
+ Each dictionary must contain:
340
+ {
341
+ "name": str, # Name of the business, place, or entity
342
+ "location": str, # Physical address or location details
343
+ "description": str # Brief description of the entity
344
+ }
345
+
346
+ Returns:
347
+ list: The input entity_info list with added coordinates for each entity.
348
+ Each entity dictionary will have an additional "coordinates" field:
349
+ {
350
+ "name": str,
351
+ "location": str,
352
+ "description": str,
353
+ "coordinates": [latitude, longitude] # Added coordinates
354
+ }
355
+
356
+ Example:
357
+ >>> entities = [{"name": "Eiffel Tower", "location": "Paris, France", "description": "Famous landmark"}]
358
+ >>> entities_with_coords = await extract_coordinates_from_address(entities)
359
+ >>> print(entities_with_coords)
360
+ [{
361
+ "name": "Eiffel Tower",
362
+ "location": "Paris, France",
363
+ "description": "Famous landmark",
364
+ "coordinates": [48.8584, 2.2945]
365
+ }]
366
+
367
+ Notes:
368
+ - Uses automated browser interaction with Google Maps
369
+ - Handles timeouts and errors gracefully
370
+ - Returns coordinates in [latitude, longitude] format
371
+ - Must be used before plot_geojson_unto_map
372
+ - May take longer for large lists of entities due to browser automation
373
+ """
374
+
375
+ def extract_coordinates(url):
376
+ import re
377
+ """Extract coordinates from Google Maps URL"""
378
+ # Pattern to match coordinates after @ symbol
379
+ pattern = r'@(-?\d+\.\d+),(-?\d+\.\d+)'
380
+ match = re.search(pattern, url)
381
+ if match:
382
+ lat, lng = match.groups()
383
+ return float(lat), float(lng)
384
+ return None
385
+
386
+ from playwright.async_api import async_playwright, TimeoutError
387
+
388
+ async with async_playwright() as p:
389
+ try:
390
+ for entity in entity_info:
391
+ name = entity.get('name', '')
392
+
393
+ # Launch browser
394
+ browser = await p.chromium.launch(headless=True)
395
+ context = await browser.new_context(
396
+ locale='en-US', # Set language to English
397
+ geolocation={'latitude': 0, 'longitude': 0},
398
+ permissions=['geolocation']
399
+ )
400
+ page = await context.new_page()
401
+
402
+ # Navigate to Google Maps
403
+ await page.goto('https://www.google.com/maps', wait_until='domcontentloaded')
404
+
405
+ # Wait for search box with timeout
406
+ search_box = await page.wait_for_selector('input[name="q"]', timeout=10000)
407
+ if not search_box:
408
+ print("Search box not found")
409
+ await browser.close()
410
+ entity['coordinates'] = None
411
+ continue
412
+
413
+ # Enter search query
414
+ await search_box.fill(name)
415
+ await search_box.press('Enter')
416
+
417
+ # Wait for results with a reasonable timeout
418
+ try:
419
+ await page.wait_for_selector('[role="article"]', timeout=10000)
420
+ except TimeoutError:
421
+ print("Results did not load within timeout")
422
+
423
+ # Get current URL after search
424
+ current_url = page.url
425
+
426
+ # Extract coordinates
427
+ coords = extract_coordinates(current_url)
428
+ if coords:
429
+ entity['coordinates'] = coords
430
+
431
+ # Close browser
432
+ await browser.close()
433
+
434
+
435
+ return json.dumps({"entity_info":entity_info})
436
+ except Exception as e:
437
+ print(f"An error occurred: {str(e)}")
438
+ if 'browser' in locals():
439
+ await browser.close()
440
+ return f"{e}"
441
+
442
+
443
+ @mcp.tool()
444
+ # async def convert_entities_to_geojson_and_update_map(entity_info:list):
445
+ async def convert_entities_to_geojson_and_update_map(entity_info:list):
446
+
447
+ """Convert entity information with coordinates into a GeoJSON map visualization.
448
+
449
+ This function takes entity information that has been processed by extract_coordinates_from_address
450
+ and converts it into a GeoJSON format for map visualization. The function is designed to work
451
+ in sequence with extract_coordinates_from_address, which must be called first to obtain the
452
+ coordinates for each entity.
453
+
454
+ This tool is particularly useful for:
455
+ - Visualizing multiple locations on an interactive map
456
+ - Creating spatial data visualizations
457
+ - Displaying geographical distributions of entities
458
+ - Supporting location-based analysis and presentations
459
+
460
+ Args:
461
+ entity_info (list): A list of dictionaries containing entity information with coordinates.
462
+ Each dictionary must contain:
463
+ {
464
+ "name": str, # Name of the business, place, or entity
465
+ "location": str, # Physical address or location details
466
+ "description": str, # Brief description of the entity
467
+ "coordinates": list # [latitude, longitude] coordinates from extract_coordinates_from_address
468
+ "url": str # Link from the google maps
469
+ }
470
+
471
+ Returns:
472
+ None: Plot on the map the result of all entities.
473
+
474
+ Example:
475
+ >>> # First, get coordinates using extract_coordinates_from_address
476
+ >>> entities = [{"name": "Eiffel Tower", "location": "Paris, France", "description": "Famous landmark"}]
477
+ >>> entities_with_coords = await extract_coordinates_from_address(entities)
478
+ >>> # Then, convert to GeoJSON for visualization
479
+ >>> await convert_entities_to_geojson_and_update_map(entities_with_coords)
480
+
481
+ Notes:
482
+ - Must be used after extract_coordinates_from_address
483
+ - Requires coordinates to be present in the entity_info
484
+ - Uses EPSG:4326 coordinate reference system
485
+ - Updates the map visualization in real-time
486
+ """
487
+
488
+ import pandas as pd
489
+ from shapely import Point
490
+ import geopandas as gpd
491
+
492
+ # Convert list of dictionaries to pandas DataFrame
493
+ df = pd.DataFrame(entity_info)
494
+
495
+ # Extract coordinates into separate columns if they exist
496
+ if 'coordinates' in df.columns:
497
+ df[['latitude', 'longitude']] = pd.DataFrame(df['coordinates'].tolist(), index=df.index)
498
+
499
+ # Convert DataFrame to GeoDataFrame
500
+ gdf = gpd.GeoDataFrame(
501
+ df,
502
+ geometry=[Point(xy) for xy in zip(df['longitude'], df['latitude'])],
503
+ crs="EPSG:4326"
504
+ )
505
+
506
+ # Convert to GeoJSON
507
+ geojson_data = gdf.to_json()
508
+
509
+ return geojson_data
510
+
511
+ if __name__ == "__main__":
512
+ mcp.run(transport='stdio')
recom.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ class RecommendationUI:
3
+ def __init__(self):
4
+ self.demo = gr.Blocks()
5
+ # Global coordinates
6
+ self.current_lat = -6.1944
7
+ self.current_lon = 106.8229
8
+ self.current_geojson = {
9
+ "type": "FeatureCollection",
10
+ "features": []
11
+ } # Initialize empty GeoJSON
12
+
13
+ def get_demo(self):
14
+ return self.demo
15
+
16
+ def update_geojson(self, new_geojson):
17
+ self.current_geojson = new_geojson
18
+
19
+ def get_current_geojson(self):
20
+ import json
21
+ if isinstance(self.current_geojson, str):
22
+ return json.loads(self.current_geojson)
23
+ return self.current_geojson
24
+
25
+