marcus941961 Nahemi commited on
Commit
b7fa003
·
verified ·
1 Parent(s): 3e87606

Upload 7 files (#1)

Browse files

- Upload 7 files (23e855c25ddc7902183e32653d807b6f82c007f3)


Co-authored-by: Florian Guerin <[email protected]>

Files changed (7) hide show
  1. .gitattributes +35 -35
  2. .gitignore +6 -0
  3. README.md +12 -12
  4. app.py +371 -503
  5. gradio_mcp_server.py +432 -432
  6. requirements.txt +5 -5
  7. tool_utils.py +56 -0
.gitattributes CHANGED
@@ -1,35 +1,35 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ .venv
2
+ .env
3
+ __pycache__/
4
+ *.pyc
5
+ instance
6
+ .pytest_cache/
README.md CHANGED
@@ -1,12 +1,12 @@
1
- ---
2
- title: Chatbot Hackathon
3
- emoji: 📈
4
- colorFrom: pink
5
- colorTo: yellow
6
- sdk: gradio
7
- sdk_version: 5.46.0
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ ---
2
+ title: Chatbot Hackathon
3
+ emoji: 📈
4
+ colorFrom: pink
5
+ colorTo: yellow
6
+ sdk: gradio
7
+ sdk_version: 5.46.0
8
+ app_file: app.py
9
+ pinned: false
10
+ ---
11
+
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py CHANGED
@@ -1,503 +1,371 @@
1
- import asyncio
2
- import os
3
- import json
4
- from typing import List, Dict, Any, Union, Optional
5
- from contextlib import AsyncExitStack
6
-
7
- import gradio as gr
8
- from gradio.components.chatbot import ChatMessage
9
- from mcp import ClientSession, StdioServerParameters
10
- from mcp.client.stdio import stdio_client
11
- from anthropic import Anthropic
12
- from dotenv import load_dotenv
13
-
14
- load_dotenv()
15
-
16
- loop = asyncio.new_event_loop()
17
- asyncio.set_event_loop(loop)
18
-
19
- class MCPClientWrapper:
20
- def __init__(self):
21
- self.session = None
22
- self.exit_stack = None
23
- self.client = None
24
- self.tools = []
25
- self.connected = False
26
- self.max_iterations = 3 # Prevent infinite loops
27
- self._init_client()
28
-
29
- def _init_client(self):
30
- """Initialize Claude client"""
31
- if os.getenv("ANTHROPIC_API_KEY"):
32
- self.client = Anthropic()
33
- else:
34
- raise ValueError("ANTHROPIC_API_KEY not found in environment")
35
-
36
- def connect(self) -> str:
37
- return loop.run_until_complete(self._connect())
38
-
39
- async def _connect(self) -> str:
40
- if self.exit_stack:
41
- await self.exit_stack.aclose()
42
-
43
- self.exit_stack = AsyncExitStack()
44
- server_path = "gradio_mcp_server.py"
45
-
46
- server_params = StdioServerParameters(
47
- command="python",
48
- args=[server_path],
49
- env={"PYTHONIOENCODING": "utf-8", "PYTHONUNBUFFERED": "1"}
50
- )
51
-
52
- try:
53
- stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
54
- self.stdio, self.write = stdio_transport
55
-
56
- self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
57
- await self.session.initialize()
58
-
59
- response = await self.session.list_tools()
60
- self.tools = [{
61
- "name": tool.name,
62
- "description": tool.description,
63
- "input_schema": tool.inputSchema
64
- } for tool in response.tools]
65
-
66
- self.connected = True
67
- return f"Connected to MCP Server. Available tools: {len(self.tools)}"
68
- except Exception as e:
69
- self.connected = False
70
- return f"Failed to connect to MCP server: {str(e)}"
71
-
72
- def _read_file_content(self, file_path: str) -> str:
73
- """Read and extract text content from uploaded file"""
74
- try:
75
- file_extension = os.path.splitext(file_path)[1].lower()
76
-
77
- if file_extension in ['.txt', '.md', '.py', '.js', '.html', '.css', '.json', '.xml', '.yaml', '.yml']:
78
- with open(file_path, 'r', encoding='utf-8') as f:
79
- return f.read()
80
-
81
- elif file_extension == '.pdf':
82
- try:
83
- import PyPDF2
84
- with open(file_path, 'rb') as f:
85
- reader = PyPDF2.PdfReader(f)
86
- text = ""
87
- for page in reader.pages:
88
- text += page.extract_text() + "\n"
89
- return text
90
- except ImportError:
91
- return f"PDF file detected but PyPDF2 not installed. File name: {os.path.basename(file_path)}"
92
-
93
- elif file_extension == '.csv':
94
- with open(file_path, 'r', encoding='utf-8') as f:
95
- return f.read()
96
-
97
- else:
98
- try:
99
- with open(file_path, 'r', encoding='utf-8') as f:
100
- return f.read()
101
- except UnicodeDecodeError:
102
- file_size = os.path.getsize(file_path)
103
- return f"Binary file detected: {os.path.basename(file_path)} ({file_size} bytes)"
104
-
105
- except Exception as e:
106
- return f"Error reading file {os.path.basename(file_path)}: {str(e)}"
107
-
108
- # In the MCPClientWrapper class...
109
-
110
- def process_message(self, message: str, files: Optional[List], history: List[List[str]]) -> tuple:
111
- if not self.session or not self.connected:
112
- # Format the error message correctly for the chatbot
113
- error_response = "MCP server is not connected. Please check the connection status above."
114
- return history + [[message, error_response]], "", None
115
-
116
- # Process uploaded files (your existing code is fine here)
117
- file_content = ""
118
- if files:
119
- file_content = "\n\n--- UPLOADED FILES ---\n"
120
- for file in files:
121
- if hasattr(file, 'name'):
122
- file_path = file.name
123
- else:
124
- file_path = file
125
-
126
- content = self._read_file_content(file_path)
127
- file_content += f"\nFile: {os.path.basename(file_path)}\n{content}\n"
128
- file_content += "--- END FILES ---\n\n"
129
-
130
- # Combine message with file content
131
- full_message = file_content + message if file_content else message
132
-
133
- # This returns a list of dictionaries like [{'role': 'assistant', 'content': '...'}, ...]
134
- new_messages = loop.run_until_complete(self._process_query(full_message, history))
135
-
136
- # 1. Combine all the assistant's generated messages (tool calls, analysis) into a single string.
137
- assistant_response_parts = [
138
- msg.get('content', '') for msg in new_messages if msg.get('role') == 'assistant'
139
- ]
140
- assistant_full_response = "\n\n".join(assistant_response_parts)
141
-
142
- # 2. Update the history in the format Gradio expects: a list of [user, assistant] pairs.
143
- updated_history = history + [[message, assistant_full_response]]
144
-
145
- # 3. Return the correctly formatted history and other component updates.
146
- return updated_history, "", None
147
-
148
- async def _process_query(self, message: str, history: List[Union[Dict[str, Any], ChatMessage]]):
149
- if not self.client:
150
- return [{"role": "assistant", "content": "Claude client not available"}]
151
-
152
- # Enhanced system prompt that encourages planning and comprehensive analysis
153
- system_prompt = """You are LEXICON, an intelligent agricultural and weather data assistant with access to comprehensive databases through specialized tools.
154
-
155
- CORE MISSION: When users ask questions, you must create and execute detailed plans to provide complete, accurate answers using ALL relevant data sources.
156
-
157
- PLANNING APPROACH:
158
- 1. **ANALYZE** the user's question to identify all information needs
159
- 2. **PLAN** a systematic approach using multiple tools and data sources
160
- 3. **EXECUTE** your plan step-by-step, making multiple tool calls as needed
161
- 4. **SYNTHESIZE** all gathered data into a comprehensive, well-structured answer
162
-
163
- AVAILABLE DATA SOURCES:
164
- - Weather stations and meteorological data (current and historical)
165
- - Municipalities and geographical references
166
- - Cadastral parcels and CAP (Common Agricultural Policy) parcels
167
- - Agricultural production data and statistics
168
- - Seed varieties and vine varieties databases
169
- - Phytosanitary products and crop protection information
170
- - Geolocation data (GeoJSON) for mapping and spatial analysis
171
-
172
- EXECUTION PRINCIPLES:
173
- - Make MULTIPLE tool calls to gather complete information
174
- - Follow up on IDs, codes, or references found in initial results
175
- - Cross-reference data between different sources when relevant
176
- - Always aim for comprehensive coverage of the topic
177
- - If initial results suggest additional relevant data exists, pursue it
178
- - Provide context and explain relationships between different data points
179
-
180
- EXAMPLE WORKFLOWS:
181
- - Location query Search municipalities → Get detailed info → Fetch cadastral/CAP data → Analyze geographical context
182
- - Weather inquiry → Find relevant stations → Get station details → Retrieve weather data → Analyze patterns
183
- - Agricultural question → Search relevant databases → Cross-reference production data → Provide comprehensive agricultural context
184
-
185
- Remember: Your goal is thorough investigation and complete answers, not quick single-tool responses.
186
-
187
- """
188
-
189
- # Build conversation history for Claude
190
- claude_messages = []
191
- for msg in history:
192
- if isinstance(msg, ChatMessage):
193
- role, content = msg.role, msg.content
194
- else:
195
- role, content = msg.get("role"), msg.get("content")
196
-
197
- if role in ["user", "assistant"]:
198
- claude_messages.append({"role": role, "content": content})
199
-
200
- claude_messages.append({"role": "user", "content": message})
201
-
202
- try:
203
- # Start the iterative tool calling process
204
- return await self._iterative_tool_calling(claude_messages)
205
-
206
- except Exception as e:
207
- return [{"role": "assistant", "content": f"Error with Claude: {str(e)}"}]
208
-
209
- async def _iterative_tool_calling(self, messages: List[Dict[str, str]]):
210
- """Enhanced method that allows Claude to reason through multiple tool calls"""
211
- result_messages = []
212
- conversation_messages = messages.copy()
213
- iteration = 0
214
-
215
- while iteration < self.max_iterations:
216
- iteration += 1
217
-
218
- try:
219
- # Make API call to Claude
220
- response = self.client.messages.create(
221
- model="claude-3-5-sonnet-20241022",
222
- max_tokens=8192,
223
- system="""You are LEXICON, an intelligent agricultural and weather data assistant with access to comprehensive databases through specialized tools.
224
-
225
- CORE MISSION: When users ask questions, you must create and execute detailed plans to provide complete, accurate answers using ALL relevant data sources.
226
-
227
- PLANNING APPROACH:
228
- 1. **ANALYZE** the user's question to identify all information needs
229
- 2. **PLAN** a systematic approach using multiple tools and data sources
230
- 3. **EXECUTE** your plan step-by-step, making multiple tool calls as needed
231
- 4. **SYNTHESIZE** all gathered data into a comprehensive, well-structured answer
232
-
233
- AVAILABLE DATA SOURCES:
234
- - Weather stations and meteorological data (current and historical)
235
- - Municipalities and geographical references
236
- - Cadastral parcels and CAP (Common Agricultural Policy) parcels
237
- - Agricultural production data and statistics
238
- - Seed varieties and vine varieties databases
239
- - Phytosanitary products and crop protection information
240
- - Geolocation data (GeoJSON) for mapping and spatial analysis
241
-
242
- EXECUTION PRINCIPLES:
243
- - Make MULTIPLE tool calls to gather complete information
244
- - Follow up on IDs, codes, or references found in initial results
245
- - Cross-reference data between different sources when relevant
246
- - Always aim for comprehensive coverage of the topic
247
- - If initial results suggest additional relevant data exists, pursue it
248
- - Provide context and explain relationships between different data points
249
-
250
- EXAMPLE WORKFLOWS:
251
- - Location query → Search municipalities → Get detailed info → Fetch cadastral/CAP data → Analyze geographical context
252
- - Weather inquiry → Find relevant stations → Get station details → Retrieve weather data → Analyze patterns
253
- - Agricultural question → Search relevant databases → Cross-reference production data → Provide comprehensive agricultural context
254
-
255
- Important: if the user mention "Saint-Porchaire", here's are the latitude and longitude to use to call the tool: 45.8289 and -0.7864,
256
- And never use the short "ST PORCHAIRE", only use "Saint-Porchaire" for all calls.
257
- When your answer involves citing several items, such as parcel IDs, mention many, as long as the output message is easily readable.
258
-
259
- Remember: Your goal is thorough investigation and complete answers, not quick single-tool responses.""",
260
- messages=conversation_messages,
261
- tools=self.tools
262
- )
263
-
264
- # Process the response
265
- has_tool_calls = False
266
- response_text = ""
267
-
268
- for content in response.content:
269
- if content.type == 'text':
270
- response_text = content.text
271
-
272
- elif content.type == 'tool_use':
273
- has_tool_calls = True
274
- tool_name = content.name
275
- tool_args = content.input
276
- tool_call_id = content.id
277
-
278
- try:
279
- # Execute the tool call
280
- result = await self.session.call_tool(tool_name, tool_args)
281
- raw_result = result.content
282
-
283
- if isinstance(raw_result, list):
284
- raw_result = "\n".join(str(item) for item in raw_result)
285
-
286
- # Add tool result to conversation for Claude to see
287
- conversation_messages.append({
288
- "role": "assistant",
289
- "content": [
290
- {"type": "tool_use", "id": tool_call_id, "name": tool_name, "input": tool_args}
291
- ]
292
- })
293
- conversation_messages.append({
294
- "role": "user",
295
- "content": [
296
- {
297
- "type": "tool_result",
298
- "tool_use_id": tool_call_id,
299
- "content": str(raw_result)
300
- }
301
- ]
302
- })
303
-
304
- # Add to result messages for display
305
- tool_display = f"🔧 **{tool_name}**({', '.join(f'{k}={v}' for k, v in tool_args.items())})\n\n```json\n{raw_result}\n```"
306
- result_messages.append({
307
- "role": "assistant",
308
- "content": tool_display
309
- })
310
-
311
- except Exception as e:
312
- error_msg = f"❌ Error calling {tool_name}: {str(e)}"
313
- result_messages.append({
314
- "role": "assistant",
315
- "content": error_msg
316
- })
317
-
318
- # Add error to conversation
319
- conversation_messages.append({
320
- "role": "user",
321
- "content": [
322
- {
323
- "type": "tool_result",
324
- "tool_use_id": tool_call_id,
325
- "content": f"Error: {str(e)}"
326
- }
327
- ]
328
- })
329
-
330
- # If there was response text, add it
331
- if response_text.strip():
332
- result_messages.append({
333
- "role": "assistant",
334
- "content": response_text
335
- })
336
-
337
- # Add to conversation if this isn't just a tool call
338
- if not has_tool_calls:
339
- conversation_messages.append({
340
- "role": "assistant",
341
- "content": response_text
342
- })
343
-
344
- # If no tool calls were made, we're done with data gathering
345
- if not has_tool_calls:
346
- break
347
-
348
- except Exception as e:
349
- result_messages.append({
350
- "role": "assistant",
351
- "content": f"❌ Error in iteration {iteration}: {str(e)}"
352
- })
353
- break
354
-
355
- # After all tool calls, get Claude's final synthesis
356
- if iteration > 1: # Only if we actually made tool calls
357
- try:
358
- final_prompt = "Based on all the data you've gathered above, please provide a comprehensive, well-structured answer to the user's original question. Synthesize the information from all sources and present it in a clear, useful format. Don't repeat the raw data - instead, analyze and explain what it means for the user."
359
-
360
- conversation_messages.append({
361
- "role": "user",
362
- "content": final_prompt
363
- })
364
-
365
- final_response = self.client.messages.create(
366
- model="claude-3-5-sonnet-20241022",
367
- max_tokens=8192,
368
- system="You are LEXICON. Provide a comprehensive, synthesized answer based on all the data gathered. Focus on insights, analysis, and practical information rather than repeating raw data.",
369
- messages=conversation_messages,
370
- tools=[] # No tools for final synthesis
371
- )
372
-
373
- for content in final_response.content:
374
- if content.type == 'text':
375
- result_messages.append({
376
- "role": "assistant",
377
- "content": f"## 📋 **Comprehensive Analysis**\n\n{content.text}"
378
- })
379
-
380
- except Exception as e:
381
- result_messages.append({
382
- "role": "assistant",
383
- "content": f"❌ Error generating final analysis: {str(e)}"
384
- })
385
-
386
- # # If we hit max iterations, add a note
387
- # if iteration >= self.max_iterations:
388
- # result_messages.append({
389
- # "role": "assistant",
390
- # "content": f"ℹ️ Reached maximum analysis depth ({self.max_iterations} steps)."
391
- # })
392
-
393
- return result_messages
394
-
395
- client = MCPClientWrapper()
396
-
397
- def gradio_interface():
398
- # Keep the custom orange and red theme
399
- theme = gr.themes.Default(
400
- primary_hue=gr.themes.colors.orange,
401
- secondary_hue=gr.themes.colors.red,
402
- neutral_hue=gr.themes.colors.slate,
403
- )
404
-
405
- with gr.Blocks(title="MCP LEXICON", theme=theme, css=".gradio-container {max-width: 95% !important;}") as demo:
406
-
407
- # 1. Top row with title and the new dynamic status button
408
- with gr.Row():
409
- with gr.Column(scale=8):
410
- gr.Markdown("## 🌾 LEXICON CHATBOT")
411
- with gr.Column(scale=10, min_width=220):
412
- status_button = gr.Button(
413
- "Connecting...",
414
- variant="stop",
415
- interactive=False
416
- )
417
-
418
- # 2. Main chat interface with a clear button
419
- with gr.Row():
420
- chatbot = gr.Chatbot(
421
- label="Conversation",
422
- value=[],
423
- height=650,
424
- show_copy_button=True,
425
- avatar_images=("👤", "🌾"),
426
- bubble_full_width=False,
427
- )
428
- clear_btn = gr.Button("🗑️ Clear", scale=0)
429
-
430
- # 3. Concise input bar at the bottom (standard chatbot layout)
431
- with gr.Row():
432
- with gr.Column(scale=10):
433
- msg = gr.Textbox(
434
- label="User Prompt",
435
- placeholder="Ask a question about agriculture, weather, or geography...",
436
- show_label=False,
437
- container=False # Removes border for a cleaner look
438
- )
439
-
440
- file_btn = gr.UploadButton("📎", file_count="multiple", scale=1)
441
-
442
- submit_btn = gr.Button(
443
- "Ask",
444
- variant="primary",
445
- scale=1
446
- )
447
-
448
- # Examples accordion remains at the bottom
449
- with gr.Accordion("💡 Example Queries", open=False):
450
- gr.Examples(
451
- examples=[
452
- "What's the complete agricultural profile of Bignan including weather stations, cadastral parcels, and production data?",
453
- "Find all weather stations near Paris, get their latest data, and analyze weather patterns",
454
- "I need comprehensive information about vine varieties and which phytosanitary products are recommended for vineyard management",
455
- ],
456
- inputs=msg
457
- )
458
-
459
- # Event handlers
460
- def auto_connect():
461
- for status_update in client.connect():
462
- yield status_update
463
-
464
- def process_and_clear(message, files, history):
465
- if not message.strip() and not files:
466
- return history, "", None
467
- # Simply return the result from the client method
468
- return client.process_message(message, files, history)
469
-
470
- # Setup events
471
- demo.load(auto_connect, outputs=status_button)
472
- status_button.click(auto_connect, outputs=status_button)
473
-
474
- submit_btn.click(
475
- process_and_clear,
476
- inputs=[msg, file_btn, chatbot],
477
- outputs=[chatbot, msg, file_btn]
478
- )
479
-
480
- msg.submit(
481
- process_and_clear,
482
- inputs=[msg, file_btn, chatbot],
483
- outputs=[chatbot, msg, file_btn]
484
- )
485
-
486
- clear_btn.click(lambda: ([], "", None), outputs=[chatbot, msg, file_btn], queue=False)
487
-
488
- return demo
489
-
490
- if __name__ == "__main__":
491
- if not os.getenv("ANTHROPIC_API_KEY"):
492
- print("Warning: ANTHROPIC_API_KEY not found in environment.")
493
- print("Please set it in your .env file: ANTHROPIC_API_KEY=your_key_here")
494
- else:
495
- print("Found Anthropic API key")
496
-
497
- print("Starting Enhanced MCP Client with Multi-Step Planning...")
498
- print("API endpoint: https://lexicon.osfarm.org")
499
-
500
- interface = gradio_interface()
501
- interface.launch(debug=True, share=True)
502
-
503
- # provide everything you know about the municipality of ABANCOURT
 
1
+ import asyncio
2
+ import os
3
+ from typing import List, Dict, Any, Optional
4
+ from contextlib import AsyncExitStack
5
+
6
+ import gradio as gr
7
+
8
+
9
+ import asyncio, os
10
+ from typing import List, Dict, Any, Optional
11
+ import gradio as gr
12
+ from mcp import ClientSession, StdioServerParameters
13
+ from mcp.client.stdio import stdio_client
14
+ from anthropic import Anthropic
15
+ from dotenv import load_dotenv
16
+ from tool_utils import filter_tools_for_context, summarize_latest_results, count_tokens, trim_conversation
17
+
18
+ load_dotenv()
19
+
20
+ import os
21
+ import re
22
+ import asyncio
23
+ import logging
24
+ from typing import List, Dict, Any, Optional, Tuple
25
+ from contextlib import AsyncExitStack
26
+ from anthropic import Anthropic
27
+ from mcp.client.session import ClientSession
28
+ from mcp.client.stdio import stdio_client
29
+ from mcp.client.stdio import StdioServerParameters
30
+
31
+ # Logger configuré
32
+ logging.basicConfig(level=logging.INFO)
33
+ logger = logging.getLogger("MCPClient")
34
+
35
+ MAX_HISTORY_MESSAGES = 5
36
+
37
+
38
+ def retry_async(max_attempts: int = 3, delay: float = 1.0):
39
+ """Décorateur de retry pour fonctions async."""
40
+ def decorator(func):
41
+ async def wrapper(*args, **kwargs):
42
+ for attempt in range(1, max_attempts + 1):
43
+ try:
44
+ return await func(*args, **kwargs)
45
+ except Exception as e:
46
+ if attempt == max_attempts:
47
+ raise
48
+ logger.warning(f"Échec tentative {attempt}/{max_attempts} : {e}. Retry dans {delay}s...")
49
+ await asyncio.sleep(delay)
50
+ # Ne devrait jamais arriver
51
+ raise RuntimeError("Retry loop exited unexpectedly")
52
+ return wrapper
53
+ return decorator
54
+
55
+
56
+ class MCPClient:
57
+ """Client MCP robuste avec gestion de connexion et retries."""
58
+
59
+ def __init__(self):
60
+ self.loop = asyncio.new_event_loop()
61
+ asyncio.set_event_loop(self.loop)
62
+ self.session: Optional[ClientSession] = None
63
+ self.tools: List[Dict[str, Any]] = []
64
+ self.connected: bool = False
65
+ self.max_iterations: int = 3
66
+ self.client: Optional[Anthropic] = None
67
+ self.exit_stack: Optional[AsyncExitStack] = None
68
+ self._init_client()
69
+
70
+ def _init_client(self):
71
+ key = os.getenv("ANTHROPIC_API_KEY")
72
+ if not key:
73
+ raise EnvironmentError(" ANTHROPIC_API_KEY manquant dans l'environnement")
74
+ self.client = Anthropic()
75
+
76
+ def connect(self) -> str:
77
+ """Connexion synchrone MCP (wrap async)."""
78
+ return self.loop.run_until_complete(self._connect())
79
+
80
+ @retry_async(max_attempts=3, delay=2)
81
+ async def _connect(self) -> str:
82
+ """Connexion asynchrone avec MCP via stdio."""
83
+ if self.exit_stack:
84
+ await self.exit_stack.aclose()
85
+
86
+ self.exit_stack = AsyncExitStack()
87
+
88
+ params = StdioServerParameters(
89
+ command="python",
90
+ args=["gradio_mcp_server.py"],
91
+ env={"PYTHONIOENCODING": "utf-8", "PYTHONUNBUFFERED": "1"},
92
+ )
93
+
94
+ try:
95
+ stdio_transport = await self.exit_stack.enter_async_context(stdio_client(params))
96
+ self.stdio, self.write = stdio_transport
97
+ self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
98
+ await self.session.initialize()
99
+
100
+ resp = await self.session.list_tools()
101
+ self.tools = [
102
+ {"name": t.name, "description": t.description, "input_schema": t.inputSchema}
103
+ for t in resp.tools
104
+ ]
105
+
106
+ self.connected = True
107
+ return f"✅ MCP connecté ({len(self.tools)} outils disponibles)"
108
+ except Exception as e:
109
+ self.connected = False
110
+ return f"❌ Connexion MCP échouée : {e}"
111
+
112
+ def _read_file(self, path: str) -> str:
113
+ """Lecture robuste de fichiers selon leur extension."""
114
+ import PyPDF2
115
+
116
+ ext = os.path.splitext(path)[1].lower()
117
+ try:
118
+ if ext in [".txt", ".md", ".py", ".json", ".csv"]:
119
+ with open(path, "r", encoding="utf-8") as f:
120
+ return f.read()
121
+ elif ext == ".pdf":
122
+ with open(path, "rb") as f:
123
+ return "\n".join(page.extract_text() for page in PyPDF2.PdfReader(f).pages)
124
+ else:
125
+ with open(path, "r", encoding="utf-8") as f:
126
+ return f.read()
127
+ except Exception as e:
128
+ return f"[Erreur lecture fichier {os.path.basename(path)}: {e}]"
129
+
130
+ def process_message(
131
+ self, message: str, files: Optional[List] = None, history: Optional[List[List[str]]] = None
132
+ ) -> Tuple[List[List[str]], str, None]:
133
+ """Pipeline haut-niveau (message + fichiers réponse)."""
134
+ if not self.session or not self.connected:
135
+ return history + [[message, "❌ Serveur MCP non connecté."]], "", None
136
+
137
+ file_content = ""
138
+ if files:
139
+ for file in files:
140
+ path = getattr(file, "name", file)
141
+ file_content += f"\nFichier: {os.path.basename(path)}\n{self._read_file(path)}\n"
142
+
143
+ full_message = (file_content + message).strip()
144
+ new_msgs = self.loop.run_until_complete(self._process_query(full_message, history or []))
145
+ assistant_reply = "\n\n".join(m.get("content", "") for m in new_msgs if m.get("role") == "assistant")
146
+
147
+ return (history or []) + [[message, assistant_reply]], "", None
148
+
149
+ async def _process_query(self, message: str, history: List[Any]):
150
+ """Exécution de la requête utilisateur avec gestion outils."""
151
+ if not self.client:
152
+ return [{"role": "assistant", "content": "❌ Client Claude indisponible."}]
153
+
154
+ # Prompt système (LEXICON)
155
+ sys_prompt = (
156
+ "You are LEXICON, an intelligent agricultural and weather data assistant with access to "
157
+ "specialized tools. Your mission: produce complete, accurate answers using planning + multiple tool calls."
158
+ )
159
+
160
+ conv = [{"role": r, "content": c} for h in history for r, c in zip(["user", "assistant"], h)]
161
+ conv.append({"role": "user", "content": message})
162
+
163
+ try:
164
+ return await self._tool_loop(conv, sys_prompt)
165
+ except Exception as e:
166
+ return [{"role": "assistant", "content": f"❌ Erreur Claude : {e}"}]
167
+
168
+ @retry_async(max_attempts=3, delay=2)
169
+ async def _tool_loop(self, messages: List[Dict[str, str]], sys_prompt: str):
170
+ """Boucle principale de planification/exécution avec outils MCP."""
171
+ result_msgs: List[Dict[str, str]] = []
172
+ conv = messages.copy()
173
+ seen_tool_calls = set()
174
+ iteration = 0
175
+ last_summary = None
176
+ max_context_tokens = 2000
177
+ tool_timeout = 10.0
178
+
179
+ while iteration < self.max_iterations:
180
+ iteration += 1
181
+ tools_this_round = filter_tools_for_context(self.tools, conv, [])
182
+
183
+ try:
184
+ resp = self.client.messages.create(
185
+ model=os.getenv("CLAUDE_MODEL", "claude-3-5-sonnet-20241022"),
186
+ max_tokens=int(os.getenv("CLAUDE_MAX_TOKENS", "8192")),
187
+ system=sys_prompt,
188
+ messages=conv,
189
+ tools=tools_this_round,
190
+ )
191
+ except Exception as e:
192
+ result_msgs.append({"role": "assistant", "content": f"❌ Erreur appel modèle : {e}"})
193
+ break
194
+
195
+ has_tool_calls = False
196
+ iteration_changes = False
197
+
198
+ for c in resp.content:
199
+ if c.type == "tool_use":
200
+ has_tool_calls = True
201
+ tool_name, tool_args, tool_call_id = c.name, c.input, c.id
202
+ key = (tool_name, tuple(sorted(tool_args.items())))
203
+ if key in seen_tool_calls:
204
+ result_msgs.append({"role": "assistant", "content": f"ℹ️ Tool déjà appelé {tool_name}({tool_args})"})
205
+ continue
206
+ seen_tool_calls.add(key)
207
+
208
+ try:
209
+ tool_result = await asyncio.wait_for(
210
+ self.session.call_tool(tool_name, tool_args), timeout=tool_timeout
211
+ )
212
+ raw_str = "\n".join(str(item) for item in tool_result.content)
213
+ conv.extend([
214
+ {"role": "assistant", "content": [{"type": "tool_use", "id": tool_call_id, "name": tool_name, "input": tool_args}]},
215
+ {"role": "user", "content": [{"type": "tool_result", "tool_use_id": tool_call_id, "content": raw_str}]}
216
+ ])
217
+ result_msgs.append({"role": "assistant", "content": f"🔧 {tool_name}({tool_args})\n```json\n{raw_str}\n```"})
218
+ iteration_changes = True
219
+ except asyncio.TimeoutError:
220
+ msg = f"❌ Timeout outil {tool_name}({tool_args})"
221
+ result_msgs.append({"role": "assistant", "content": msg})
222
+ except Exception as e:
223
+ msg = f" Erreur outil {tool_name}({tool_args}) : {e}"
224
+ result_msgs.append({"role": "assistant", "content": msg})
225
+
226
+ elif c.type == "text":
227
+ text = c.text.strip()
228
+ if text:
229
+ result_msgs.append({"role": "assistant", "content": text})
230
+ conv.append({"role": "assistant", "content": text})
231
+ iteration_changes = True
232
+
233
+ # Conditions d'arrêt
234
+ if not has_tool_calls or not iteration_changes:
235
+ break
236
+
237
+ summary = summarize_latest_results(conv)
238
+ if last_summary is not None and summary == last_summary:
239
+ result_msgs.append({"role": "assistant", "content": "ℹ️ Pas de nouvelles infos, arrêt."})
240
+ break
241
+ last_summary = summary
242
+
243
+ if max_context_tokens and count_tokens(conv) > max_context_tokens:
244
+ conv = trim_conversation(conv, keep_last_n=MAX_HISTORY_MESSAGES)
245
+
246
+ # Synthèse finale
247
+ result_msgs.append({"role": "assistant", "content": "## 📋 Synthèse finale :"})
248
+ try:
249
+ final_prompt = "Basé sur les données collectées, rédige une réponse claire et utile à la question initiale."
250
+ conv.append({"role": "user", "content": final_prompt})
251
+ final_resp = self.client.messages.create(
252
+ model=os.getenv("CLAUDE_MODEL", "claude-3-5-sonnet-20241022"),
253
+ max_tokens=int(os.getenv("CLAUDE_MAX_TOKENS", "8192")),
254
+ system="You are the assistant producing the final analysis.",
255
+ messages=conv,
256
+ tools=[],
257
+ )
258
+ for c in final_resp.content:
259
+ if c.type == "text":
260
+ result_msgs.append({"role": "assistant", "content": c.text.strip()})
261
+ except Exception as e:
262
+ result_msgs.append({"role": "assistant", "content": f"❌ Erreur synthèse finale : {e}"})
263
+
264
+ return result_msgs
265
+
266
+ client = MCPClient()
267
+
268
+ def gradio_interface():
269
+ # Keep the custom orange and red theme
270
+ theme = gr.themes.Default(
271
+ primary_hue=gr.themes.colors.orange,
272
+ secondary_hue=gr.themes.colors.red,
273
+ neutral_hue=gr.themes.colors.slate,
274
+ )
275
+
276
+ with gr.Blocks(title="MCP LEXICON", theme=theme, css=".gradio-container {max-width: 95% !important;}") as demo:
277
+
278
+ # 1. Top row with title and the new dynamic status button
279
+ with gr.Row():
280
+ with gr.Column(scale=8):
281
+ gr.Markdown("## 🌾 LEXICON CHATBOT")
282
+ with gr.Column(scale=10, min_width=220):
283
+ status_button = gr.Button(
284
+ "Connecting...",
285
+ variant="stop",
286
+ interactive=False
287
+ )
288
+
289
+ # 2. Main chat interface with a clear button
290
+ with gr.Row():
291
+ chatbot = gr.Chatbot(
292
+ label="Conversation",
293
+ value=[],
294
+ height=650,
295
+ show_copy_button=True,
296
+ avatar_images=("👤", "🌾"),
297
+ bubble_full_width=False,
298
+ )
299
+ clear_btn = gr.Button("🗑️ Clear", scale=0)
300
+
301
+ # 3. Concise input bar at the bottom (standard chatbot layout)
302
+ with gr.Row():
303
+ with gr.Column(scale=10):
304
+ msg = gr.Textbox(
305
+ label="User Prompt",
306
+ placeholder="Ask a question about agriculture, weather, or geography...",
307
+ show_label=False,
308
+ container=False # Removes border for a cleaner look
309
+ )
310
+
311
+ file_btn = gr.UploadButton("📎", file_count="multiple", scale=1)
312
+
313
+ submit_btn = gr.Button(
314
+ "Ask",
315
+ variant="primary",
316
+ scale=1
317
+ )
318
+
319
+ # Examples accordion remains at the bottom
320
+ with gr.Accordion("💡 Example Queries", open=False):
321
+ gr.Examples(
322
+ examples=[
323
+ "What's the complete agricultural profile of Bignan including weather stations, cadastral parcels, and production data?",
324
+ "Find all weather stations near Paris, get their latest data, and analyze weather patterns",
325
+ "I need comprehensive information about vine varieties and which phytosanitary products are recommended for vineyard management",
326
+ ],
327
+ inputs=msg
328
+ )
329
+
330
+ # Event handlers
331
+ def auto_connect():
332
+ return client.connect()
333
+
334
+ def process_and_clear(message, files, history):
335
+ if not message.strip() and not files:
336
+ return history, "", None
337
+ # Simply return the result from the client method
338
+ return client.process_message(message, files, history)
339
+
340
+ # Setup events
341
+ demo.load(auto_connect, outputs=status_button)
342
+ status_button.click(auto_connect, outputs=status_button)
343
+
344
+ submit_btn.click(
345
+ process_and_clear,
346
+ inputs=[msg, file_btn, chatbot],
347
+ outputs=[chatbot, msg, file_btn]
348
+ )
349
+
350
+ msg.submit(
351
+ process_and_clear,
352
+ inputs=[msg, file_btn, chatbot],
353
+ outputs=[chatbot, msg, file_btn]
354
+ )
355
+
356
+ clear_btn.click(lambda: ([], "", None), outputs=[chatbot, msg, file_btn], queue=False)
357
+
358
+ return demo
359
+
360
+ if __name__ == "__main__":
361
+ if not os.getenv("ANTHROPIC_API_KEY"):
362
+ print("Warning: ANTHROPIC_API_KEY not found in environment.")
363
+ print("Please set it in your .env file: ANTHROPIC_API_KEY=your_key_here")
364
+ else:
365
+ print("Found Anthropic API key")
366
+
367
+ print("Starting Enhanced MCP Client with Multi-Step Planning...")
368
+ print("API endpoint: https://lexicon.osfarm.org")
369
+
370
+ interface = gradio_interface()
371
+ interface.launch(debug=True, share=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
gradio_mcp_server.py CHANGED
@@ -1,433 +1,433 @@
1
- from mcp.server.fastmcp import FastMCP
2
- import json
3
- import sys
4
- import io
5
- import requests
6
-
7
- sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
8
- sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
9
-
10
- mcp = FastMCP("lexicon_api_server")
11
-
12
- BASE_URL = "https://lexicon.osfarm.org"
13
-
14
- def make_api_request(endpoint: str, params: dict = None) -> str:
15
- """Helper function to make API requests with consistent error handling"""
16
- url = f"{BASE_URL}{endpoint}"
17
-
18
- try:
19
- response = requests.get(url, params=params, timeout=30)
20
- response.raise_for_status()
21
-
22
- # Check if response is GeoJSON or regular JSON
23
- content_type = response.headers.get('content-type', '')
24
- if 'application/geo+json' in content_type or endpoint.endswith('.geojson'):
25
- data_type = "geojson"
26
- else:
27
- data_type = "json"
28
-
29
- data = response.json()
30
-
31
- return json.dumps({
32
- "type": "success",
33
- "data_type": data_type,
34
- "endpoint": endpoint,
35
- "data": data,
36
- "message": f"Successfully retrieved data from {endpoint}"
37
- }, indent=2)
38
-
39
- except requests.exceptions.ConnectionError:
40
- return json.dumps({
41
- "type": "error",
42
- "endpoint": endpoint,
43
- "message": f"Could not connect to API. Please ensure the service is running."
44
- })
45
- except requests.exceptions.Timeout:
46
- return json.dumps({
47
- "type": "error",
48
- "endpoint": endpoint,
49
- "message": f"Request timed out for {endpoint}"
50
- })
51
- except requests.exceptions.HTTPError as e:
52
- status_code = e.response.status_code if e.response else "unknown"
53
- return json.dumps({
54
- "type": "error",
55
- "endpoint": endpoint,
56
- "status_code": status_code,
57
- "message": f"HTTP error {status_code} for {endpoint}. Resource may not exist or API may be unavailable."
58
- })
59
- except json.JSONDecodeError:
60
- return json.dumps({
61
- "type": "error",
62
- "endpoint": endpoint,
63
- "message": f"Invalid JSON response from {url}"
64
- })
65
- except Exception as e:
66
- return json.dumps({
67
- "type": "error",
68
- "endpoint": endpoint,
69
- "message": f"Unexpected error: {str(e)}"
70
- })
71
-
72
- @mcp.tool()
73
- async def get_parcel_identifier_json(latitude: float, longitude: float) -> str:
74
- """
75
- Retrieve parcel identifier information in JSON format for a given geographic location. -- ST PORCHAIRE
76
-
77
- Args:
78
- latitude (float): Latitude of the point of interest.
79
- longitude (float): Longitude of the point of interest.
80
-
81
- Returns:
82
- str: JSON string containing parcel identifier data for the specified coordinates.
83
-
84
- This tool allows you to obtain parcel identification data by providing precise geographic coordinates.
85
- Useful for reverse-geocoding a location to its cadastral reference.
86
- """
87
- params = {"latitude": latitude, "longitude": longitude}
88
- return make_api_request("/tools/parcel-identifier.json", params)[:20000]
89
-
90
- @mcp.tool()
91
- async def get_cadastral_parcels(
92
- page: int = 1,
93
- code: str = None,
94
- prefix: str = None,
95
- section: str = None,
96
- number: str = None
97
- ) -> str:
98
- """
99
- Retrieve a paginated list of cadastral parcels, with optional filters.
100
-
101
- Args:
102
- page (int, optional): Page number for pagination (default: 1).
103
- code (str, optional): Commune code to filter parcels.
104
- prefix (str, optional): Parcel prefix for more precise filtering.
105
- section (str, optional): Parcel section identifier.
106
- number (str, optional): Parcel number.
107
-
108
- Returns:
109
- str: JSON string containing a list of cadastral parcels matching the filters.
110
-
111
- This tool enables searching for cadastral parcels using various administrative and parcel-specific filters.
112
- Useful for exploring land registry data at different levels of granularity.
113
- Using a single postal_code, gives every cadastral parcels codes in that city, for example.
114
- """
115
- params = {"page": page}
116
- if code: params["code"] = code
117
- if prefix: params["prefix"] = prefix
118
- if section: params["section"] = section
119
- if number: params["number"] = number
120
- return make_api_request("/geographical-references/cadastral-parcels.json", params)
121
-
122
- @mcp.tool()
123
- async def get_cadastral_parcel(parcel_id: str) -> str:
124
- """
125
- Retrieve detailed information about a specific cadastral parcel.
126
-
127
- Args:
128
- parcel_id (str): Unique identifier of the cadastral parcel.
129
-
130
- Returns:
131
- str: JSON string with detailed information about the parcel.
132
-
133
- Use this tool to get all available data for a single cadastral parcel, including administrative and spatial attributes.
134
- """
135
- return make_api_request(f"/geographical-references/cadastral-parcels/{parcel_id}.json")
136
-
137
-
138
- @mcp.tool()
139
- async def get_cadastral_parcel_prices(postal_code: str = None, city: str = None, department: str = None) -> str:
140
- """
141
- Retrieve cadastral parcel price information, filtered by postal code, city, or department,
142
-
143
- Args:
144
- postal_code (str, optional): Postal code to filter results.
145
- city (str, optional): City name for filtering.
146
- department (str, optional): Department code or name.
147
-
148
- Returns:
149
- str: JSON string with price information for cadastral parcels.
150
-
151
- This tool provides access to price data for cadastral parcels, supporting multiple administrative filters.
152
- It is possible, with a postal code and a city name, to get the prices of every cadastral parcel sold in that city and when,
153
- for example. So it is possible to know which one is the most expensive or the cheapest.
154
- """
155
- params = {}
156
- if postal_code: params["postal_code"] = postal_code
157
- if city: params["city"] = city
158
- if department: params["department"] = department
159
- ## FOR DEMO PURPOSES:
160
- params["page"] = 2
161
- return make_api_request("/geographical-references/cadastral-parcel-prices.json", params)
162
-
163
- # geographical-references/cadastral-parcel-prices?postal_code=&city=Saint-Porchaire&department=&page=2
164
-
165
- @mcp.tool()
166
- async def get_cap_parcels(page: int = 1, city: str = None) -> str:
167
- """
168
- Retrieve a paginated list of CAP (Common Agricultural Policy) parcels,
169
- allowing to access the crops available in the filtered city.
170
-
171
- Args:
172
- page (int, optional): Page number for pagination (default: 1).
173
- city (str, optional): City name to filter CAP parcels.
174
-
175
- Returns:
176
- str: JSON string containing CAP parcels matching the filters.
177
-
178
- This tool allows you to explore CAP parcels, which are relevant for agricultural policy and subsidy management.
179
- """
180
- return make_api_request(f"/geographical-references/cap-parcels.json?city={city}")
181
-
182
- @mcp.tool()
183
- async def get_municipalities(page: int = 1, country: str = None, city: str = None) -> str:
184
- """
185
- Retrieve a paginated list of municipalities, with optional filters for country and city.
186
-
187
- Args:
188
- page (int, optional): Page number for pagination (default: 1).
189
- country (str, optional): Country name to filter municipalities.
190
- city (str, optional): City name for more precise filtering.
191
-
192
- Returns:
193
- str: JSON string containing municipalities matching the filters.
194
-
195
- This tool is useful for exploring administrative boundaries and locating municipalities by name or country.
196
- """
197
- params = {"page": page}
198
- if country: params["country"] = country
199
- if city: params["city"] = city
200
- return make_api_request("/geographical-references/municipalities.json", params)
201
-
202
- @mcp.tool()
203
- async def get_municipality(municipality_id: str) -> str:
204
- """
205
- Retrieve detailed information about a specific municipality.
206
-
207
- Args:
208
- municipality_id (str): Unique identifier of the municipality.
209
-
210
- Returns:
211
- str: JSON string with detailed information about the municipality.
212
-
213
- Use this tool to access all available data for a single municipality, including administrative and spatial attributes.
214
- """
215
- return make_api_request(f"/geographical-references/municipalities/{municipality_id}.json")
216
-
217
- @mcp.tool()
218
- async def get_productions(page: int = 1, family: str = None, usage: str = None) -> str:
219
- """
220
- Retrieve a paginated list of production data, with optional filters for family and usage.
221
-
222
- Args:
223
- page (int, optional): Page number for pagination (default: 1).
224
- family (str, optional): Production family (e.g., crop type).
225
- usage (str, optional): Usage type (e.g., food, feed).
226
-
227
- Returns:
228
- str: JSON string containing production data matching the filters.
229
-
230
- This tool is useful for analyzing agricultural production by type and intended use.
231
- """
232
- params = {"page": page}
233
- if family: params["family"] = family
234
- if usage: params["usage"] = usage
235
- return make_api_request("/production/productions.json", params)
236
-
237
- @mcp.tool()
238
- async def get_cropsets(page: int = 1) -> str:
239
- """
240
- Retrieve a paginated list of phytosanitary cropsets.
241
-
242
- Args:
243
- page (int, optional): Page number for pagination (default: 1).
244
-
245
- Returns:
246
- str: JSON string containing cropset data.
247
-
248
- This tool provides access to phytosanitary cropsets, which are important for plant protection and regulatory compliance.
249
- """
250
- params = {"page": page}
251
- return make_api_request("/phytosanitary/cropsets.json", params)
252
-
253
- @mcp.tool()
254
- async def get_phytosanitary_products(page: int = 1) -> str:
255
- """
256
- Retrieve a paginated list of phytosanitary products, and returns:
257
- Name
258
- firm
259
- Type
260
- Active compounds
261
- Usage state
262
-
263
- Args:
264
- page (int, optional): Page number for pagination (default: 1).
265
- type (str, optional): Product type (e.g., herbicide, fungicide).
266
- state (str, optional): Product state (e.g., approved, withdrawn).
267
-
268
- Returns:
269
- str: JSON string containing phytosanitary products matching the filters.
270
-
271
- This tool is useful for exploring available plant protection products and their regulatory status.
272
- """
273
- return make_api_request("/phytosanitary/products.json")
274
-
275
- @mcp.tool()
276
- async def get_phytosanitary_symbols(page: int = 1) -> str:
277
- """
278
- Retrieve a paginated list of phytosanitary symbols.
279
-
280
- Args:
281
- page (int, optional): Page number for pagination (default: 1).
282
-
283
- Returns:
284
- str: JSON string containing phytosanitary symbols.
285
-
286
- This tool provides access to symbols used in plant protection and regulatory documentation.
287
- """
288
- params = {"page": page}
289
- return make_api_request("/phytosanitary/symbols.json", params)
290
-
291
- @mcp.tool()
292
- async def get_seed_varieties(page: int = 1, species: str = None) -> str:
293
- """
294
- Retrieve a paginated list of seed varieties, optionally filtered by species.
295
-
296
- Args:
297
- page (int, optional): Page number for pagination (default: 1).
298
- species (str, optional): Species name to filter seed varieties, ALWAYS IN CAPITAL LETTERS, e.g for "avoine", use "AVOINE",
299
- to find the right species, for example "AVOINE". Then varieties will be filtered.
300
-
301
- Returns:
302
- str: JSON string containing seed varieties matching the filters.
303
-
304
- This tool is useful for exploring available seed varieties for different crops.
305
- """
306
- params = {"page": page}
307
- if species: params["species"] = species
308
- return make_api_request("/seeds/varieties.json", params)
309
-
310
-
311
- @mcp.tool()
312
- async def get_vine_varieties(page: int = 1, category: str = None, color: str = None) -> str:
313
- """
314
- Retrieve a paginated list of vine varieties, with optional filters for category and color.
315
-
316
- Args:
317
- page (int, optional): Page number for pagination (default: 1).
318
- category (str, optional): Vine category (e.g., table, wine).
319
- color (str, optional): Grape color (e.g., red, white).
320
-
321
- Returns:
322
- str: JSON string containing vine varieties matching the filters.
323
-
324
- This tool is useful for exploring grapevine diversity and selecting varieties for viticulture.
325
- """
326
- params = {"page": page}
327
- if category: params["category"] = category
328
- if color: params["color"] = color
329
- return make_api_request("/viticulture/vine-varieties.json", params)
330
-
331
- @mcp.tool()
332
- async def get_weather_stations(page: int = 1, country: str = None, name: str = None) -> str:
333
- """
334
- Retrieve a paginated list of weather stations, with optional filters for country and station name.
335
-
336
- Args:
337
- page (int, optional): Page number for pagination (default: 1).
338
- country (str, optional): Country name to filter stations.
339
- name (str, optional): Station name for more precise filtering.
340
-
341
- Returns:
342
- str: JSON string containing weather stations matching the filters.
343
-
344
- This tool is useful for discovering available weather stations and narrowing down by location or name.
345
- """
346
- params = {"page": page}
347
- if country: params["country"] = country
348
- if name: params["name"] = name
349
- return make_api_request("/weather/stations.json", params)
350
-
351
- @mcp.tool()
352
- async def get_weather_station(station_code: str) -> str:
353
- """
354
- Retrieve detailed information about a specific weather station.
355
-
356
- Args:
357
- station_code (str): Unique code identifying the weather station.
358
-
359
- Returns:
360
- str: JSON string with detailed information about the weather station.
361
-
362
- Use this tool to access metadata and attributes for a single weather station.
363
- """
364
- return make_api_request(f"/weather/stations/{station_code}.json")
365
-
366
- @mcp.tool()
367
- async def get_weather_data(station_code: str, start: str = None, end: str = None) -> str:
368
- """
369
- Retrieve hourly weather reports for a specific station, optionally filtered by date range.
370
-
371
- Args:
372
- station_code (str): Unique code identifying the weather station.
373
- start (str, optional): Start date/time in ISO format (e.g., '2024-01-01T00:00:00Z').
374
- end (str, optional): End date/time in ISO format (e.g., '2024-01-31T23:59:59Z').
375
-
376
- Returns:
377
- str: JSON string containing hourly weather reports for the specified station and date range.
378
-
379
- This tool is useful for analyzing weather data over time for a given location.
380
- """
381
- params = {}
382
- if start: params["start"] = start
383
- if end: params["end"] = end
384
- return make_api_request(f"/weather/stations/{station_code}/hourly-reports.json", params)
385
-
386
-
387
- @mcp.tool()
388
- async def get_cadastral_parcel_geolocation(parcel_id: str) -> str:
389
- """
390
- Retrieve geolocation data for a specific cadastral parcel in GeoJSON format.
391
-
392
- Args:
393
- parcel_id (str): Unique identifier of the cadastral parcel.
394
-
395
- Returns:
396
- str: GeoJSON string with spatial data for the parcel.
397
-
398
- Use this tool to obtain the geometry of a cadastral parcel for mapping or spatial analysis.
399
- """
400
- return make_api_request(f"/geographical-references/cadastral-parcels/{parcel_id}/geolocation.geojson")
401
-
402
- @mcp.tool()
403
- async def get_cap_parcel_geolocation(cap_id: str) -> str:
404
- """
405
- Retrieve geolocation data for a specific CAP parcel in GeoJSON format.
406
-
407
- Args:
408
- cap_id (str): Unique identifier of the CAP parcel.
409
-
410
- Returns:
411
- str: GeoJSON string with spatial data for the CAP parcel.
412
-
413
- Use this tool to obtain the geometry of a CAP parcel for mapping or spatial analysis.
414
- """
415
- return make_api_request(f"/geographical-references/cap-parcels/{cap_id}/geolocation.geojson")
416
-
417
- @mcp.tool()
418
- async def get_municipality_cadastre(municipality_id: str) -> str:
419
- """
420
- Retrieve cadastre data for a municipality in GeoJSON format.
421
-
422
- Args:
423
- municipality_id (str): Unique identifier of the municipality.
424
-
425
- Returns:
426
- str: GeoJSON string with cadastre data for the municipality.
427
-
428
- Use this tool to obtain the spatial extent of a municipality's cadastre for mapping or GIS analysis.
429
- """
430
- return make_api_request(f"/geographical-references/municipalities/{municipality_id}/cadastre.geojson")
431
-
432
- if __name__ == "__main__":
433
  mcp.run(transport='stdio')
 
1
+ from mcp.server.fastmcp import FastMCP
2
+ import json
3
+ import sys
4
+ import io
5
+ import requests
6
+
7
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
8
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
9
+
10
+ mcp = FastMCP("lexicon_api_server")
11
+
12
+ BASE_URL = "https://lexicon.osfarm.org"
13
+
14
+ def make_api_request(endpoint: str, params: dict = None) -> str:
15
+ """Helper function to make API requests with consistent error handling"""
16
+ url = f"{BASE_URL}{endpoint}"
17
+
18
+ try:
19
+ response = requests.get(url, params=params, timeout=30)
20
+ response.raise_for_status()
21
+
22
+ # Check if response is GeoJSON or regular JSON
23
+ content_type = response.headers.get('content-type', '')
24
+ if 'application/geo+json' in content_type or endpoint.endswith('.geojson'):
25
+ data_type = "geojson"
26
+ else:
27
+ data_type = "json"
28
+
29
+ data = response.json()
30
+
31
+ return json.dumps({
32
+ "type": "success",
33
+ "data_type": data_type,
34
+ "endpoint": endpoint,
35
+ "data": data,
36
+ "message": f"Successfully retrieved data from {endpoint}"
37
+ }, indent=2)
38
+
39
+ except requests.exceptions.ConnectionError:
40
+ return json.dumps({
41
+ "type": "error",
42
+ "endpoint": endpoint,
43
+ "message": f"Could not connect to API. Please ensure the service is running."
44
+ })
45
+ except requests.exceptions.Timeout:
46
+ return json.dumps({
47
+ "type": "error",
48
+ "endpoint": endpoint,
49
+ "message": f"Request timed out for {endpoint}"
50
+ })
51
+ except requests.exceptions.HTTPError as e:
52
+ status_code = e.response.status_code if e.response else "unknown"
53
+ return json.dumps({
54
+ "type": "error",
55
+ "endpoint": endpoint,
56
+ "status_code": status_code,
57
+ "message": f"HTTP error {status_code} for {endpoint}. Resource may not exist or API may be unavailable."
58
+ })
59
+ except json.JSONDecodeError:
60
+ return json.dumps({
61
+ "type": "error",
62
+ "endpoint": endpoint,
63
+ "message": f"Invalid JSON response from {url}"
64
+ })
65
+ except Exception as e:
66
+ return json.dumps({
67
+ "type": "error",
68
+ "endpoint": endpoint,
69
+ "message": f"Unexpected error: {str(e)}"
70
+ })
71
+
72
+ @mcp.tool()
73
+ async def get_parcel_identifier_json(latitude: float, longitude: float) -> str:
74
+ """
75
+ Retrieve parcel identifier information in JSON format for a given geographic location. -- ST PORCHAIRE
76
+
77
+ Args:
78
+ latitude (float): Latitude of the point of interest.
79
+ longitude (float): Longitude of the point of interest.
80
+
81
+ Returns:
82
+ str: JSON string containing parcel identifier data for the specified coordinates.
83
+
84
+ This tool allows you to obtain parcel identification data by providing precise geographic coordinates.
85
+ Useful for reverse-geocoding a location to its cadastral reference.
86
+ """
87
+ params = {"latitude": latitude, "longitude": longitude}
88
+ return make_api_request("/tools/parcel-identifier.json", params)[:20000]
89
+
90
+ @mcp.tool()
91
+ async def get_cadastral_parcels(
92
+ page: int = 1,
93
+ code: str = None,
94
+ prefix: str = None,
95
+ section: str = None,
96
+ number: str = None
97
+ ) -> str:
98
+ """
99
+ Retrieve a paginated list of cadastral parcels, with optional filters.
100
+
101
+ Args:
102
+ page (int, optional): Page number for pagination (default: 1).
103
+ code (str, optional): Commune code to filter parcels.
104
+ prefix (str, optional): Parcel prefix for more precise filtering.
105
+ section (str, optional): Parcel section identifier.
106
+ number (str, optional): Parcel number.
107
+
108
+ Returns:
109
+ str: JSON string containing a list of cadastral parcels matching the filters.
110
+
111
+ This tool enables searching for cadastral parcels using various administrative and parcel-specific filters.
112
+ Useful for exploring land registry data at different levels of granularity.
113
+ Using a single postal_code, gives every cadastral parcels codes in that city, for example.
114
+ """
115
+ params = {"page": page}
116
+ if code: params["code"] = code
117
+ if prefix: params["prefix"] = prefix
118
+ if section: params["section"] = section
119
+ if number: params["number"] = number
120
+ return make_api_request("/geographical-references/cadastral-parcels.json", params)
121
+
122
+ @mcp.tool()
123
+ async def get_cadastral_parcel(parcel_id: str) -> str:
124
+ """
125
+ Retrieve detailed information about a specific cadastral parcel.
126
+
127
+ Args:
128
+ parcel_id (str): Unique identifier of the cadastral parcel.
129
+
130
+ Returns:
131
+ str: JSON string with detailed information about the parcel.
132
+
133
+ Use this tool to get all available data for a single cadastral parcel, including administrative and spatial attributes.
134
+ """
135
+ return make_api_request(f"/geographical-references/cadastral-parcels/{parcel_id}.json")
136
+
137
+
138
+ @mcp.tool()
139
+ async def get_cadastral_parcel_prices(postal_code: str = None, city: str = None, department: str = None) -> str:
140
+ """
141
+ Retrieve cadastral parcel price information, filtered by postal code, city, or department,
142
+
143
+ Args:
144
+ postal_code (str, optional): Postal code to filter results.
145
+ city (str, optional): City name for filtering.
146
+ department (str, optional): Department code or name.
147
+
148
+ Returns:
149
+ str: JSON string with price information for cadastral parcels.
150
+
151
+ This tool provides access to price data for cadastral parcels, supporting multiple administrative filters.
152
+ It is possible, with a postal code and a city name, to get the prices of every cadastral parcel sold in that city and when,
153
+ for example. So it is possible to know which one is the most expensive or the cheapest.
154
+ """
155
+ params = {}
156
+ if postal_code: params["postal_code"] = postal_code
157
+ if city: params["city"] = city
158
+ if department: params["department"] = department
159
+ ## FOR DEMO PURPOSES:
160
+ params["page"] = 2
161
+ return make_api_request("/geographical-references/cadastral-parcel-prices.json", params)
162
+
163
+ # geographical-references/cadastral-parcel-prices?postal_code=&city=Saint-Porchaire&department=&page=2
164
+
165
+ @mcp.tool()
166
+ async def get_cap_parcels(page: int = 1, city: str = None) -> str:
167
+ """
168
+ Retrieve a paginated list of CAP (Common Agricultural Policy) parcels,
169
+ allowing to access the crops available in the filtered city.
170
+
171
+ Args:
172
+ page (int, optional): Page number for pagination (default: 1).
173
+ city (str, optional): City name to filter CAP parcels.
174
+
175
+ Returns:
176
+ str: JSON string containing CAP parcels matching the filters.
177
+
178
+ This tool allows you to explore CAP parcels, which are relevant for agricultural policy and subsidy management.
179
+ """
180
+ return make_api_request(f"/geographical-references/cap-parcels.json?city={city}")
181
+
182
+ @mcp.tool()
183
+ async def get_municipalities(page: int = 1, country: str = None, city: str = None) -> str:
184
+ """
185
+ Retrieve a paginated list of municipalities, with optional filters for country and city.
186
+
187
+ Args:
188
+ page (int, optional): Page number for pagination (default: 1).
189
+ country (str, optional): Country name to filter municipalities.
190
+ city (str, optional): City name for more precise filtering.
191
+
192
+ Returns:
193
+ str: JSON string containing municipalities matching the filters.
194
+
195
+ This tool is useful for exploring administrative boundaries and locating municipalities by name or country.
196
+ """
197
+ params = {"page": page}
198
+ if country: params["country"] = country
199
+ if city: params["city"] = city
200
+ return make_api_request("/geographical-references/municipalities.json", params)
201
+
202
+ @mcp.tool()
203
+ async def get_municipality(municipality_id: str) -> str:
204
+ """
205
+ Retrieve detailed information about a specific municipality.
206
+
207
+ Args:
208
+ municipality_id (str): Unique identifier of the municipality.
209
+
210
+ Returns:
211
+ str: JSON string with detailed information about the municipality.
212
+
213
+ Use this tool to access all available data for a single municipality, including administrative and spatial attributes.
214
+ """
215
+ return make_api_request(f"/geographical-references/municipalities/{municipality_id}.json")
216
+
217
+ @mcp.tool()
218
+ async def get_productions(page: int = 1, family: str = None, usage: str = None) -> str:
219
+ """
220
+ Retrieve a paginated list of production data, with optional filters for family and usage.
221
+
222
+ Args:
223
+ page (int, optional): Page number for pagination (default: 1).
224
+ family (str, optional): Production family (e.g., crop type).
225
+ usage (str, optional): Usage type (e.g., food, feed).
226
+
227
+ Returns:
228
+ str: JSON string containing production data matching the filters.
229
+
230
+ This tool is useful for analyzing agricultural production by type and intended use.
231
+ """
232
+ params = {"page": page}
233
+ if family: params["family"] = family
234
+ if usage: params["usage"] = usage
235
+ return make_api_request("/production/productions.json", params)
236
+
237
+ @mcp.tool()
238
+ async def get_cropsets(page: int = 1) -> str:
239
+ """
240
+ Retrieve a paginated list of phytosanitary cropsets.
241
+
242
+ Args:
243
+ page (int, optional): Page number for pagination (default: 1).
244
+
245
+ Returns:
246
+ str: JSON string containing cropset data.
247
+
248
+ This tool provides access to phytosanitary cropsets, which are important for plant protection and regulatory compliance.
249
+ """
250
+ params = {"page": page}
251
+ return make_api_request("/phytosanitary/cropsets.json", params)
252
+
253
+ @mcp.tool()
254
+ async def get_phytosanitary_products(page: int = 1) -> str:
255
+ """
256
+ Retrieve a paginated list of phytosanitary products, and returns:
257
+ Name
258
+ firm
259
+ Type
260
+ Active compounds
261
+ Usage state
262
+
263
+ Args:
264
+ page (int, optional): Page number for pagination (default: 1).
265
+ type (str, optional): Product type (e.g., herbicide, fungicide).
266
+ state (str, optional): Product state (e.g., approved, withdrawn).
267
+
268
+ Returns:
269
+ str: JSON string containing phytosanitary products matching the filters.
270
+
271
+ This tool is useful for exploring available plant protection products and their regulatory status.
272
+ """
273
+ return make_api_request("/phytosanitary/products.json")
274
+
275
+ @mcp.tool()
276
+ async def get_phytosanitary_symbols(page: int = 1) -> str:
277
+ """
278
+ Retrieve a paginated list of phytosanitary symbols.
279
+
280
+ Args:
281
+ page (int, optional): Page number for pagination (default: 1).
282
+
283
+ Returns:
284
+ str: JSON string containing phytosanitary symbols.
285
+
286
+ This tool provides access to symbols used in plant protection and regulatory documentation.
287
+ """
288
+ params = {"page": page}
289
+ return make_api_request("/phytosanitary/symbols.json", params)
290
+
291
+ @mcp.tool()
292
+ async def get_seed_varieties(page: int = 1, species: str = None) -> str:
293
+ """
294
+ Retrieve a paginated list of seed varieties, optionally filtered by species.
295
+
296
+ Args:
297
+ page (int, optional): Page number for pagination (default: 1).
298
+ species (str, optional): Species name to filter seed varieties, ALWAYS IN CAPITAL LETTERS, e.g for "avoine", use "AVOINE",
299
+ to find the right species, for example "AVOINE". Then varieties will be filtered.
300
+
301
+ Returns:
302
+ str: JSON string containing seed varieties matching the filters.
303
+
304
+ This tool is useful for exploring available seed varieties for different crops.
305
+ """
306
+ params = {"page": page}
307
+ if species: params["species"] = species
308
+ return make_api_request("/seeds/varieties.json", params)
309
+
310
+
311
+ @mcp.tool()
312
+ async def get_vine_varieties(page: int = 1, category: str = None, color: str = None) -> str:
313
+ """
314
+ Retrieve a paginated list of vine varieties, with optional filters for category and color.
315
+
316
+ Args:
317
+ page (int, optional): Page number for pagination (default: 1).
318
+ category (str, optional): Vine category (e.g., table, wine).
319
+ color (str, optional): Grape color (e.g., red, white).
320
+
321
+ Returns:
322
+ str: JSON string containing vine varieties matching the filters.
323
+
324
+ This tool is useful for exploring grapevine diversity and selecting varieties for viticulture.
325
+ """
326
+ params = {"page": page}
327
+ if category: params["category"] = category
328
+ if color: params["color"] = color
329
+ return make_api_request("/viticulture/vine-varieties.json", params)
330
+
331
+ @mcp.tool()
332
+ async def get_weather_stations(page: int = 1, country: str = None, name: str = None) -> str:
333
+ """
334
+ Retrieve a paginated list of weather stations, with optional filters for country and station name.
335
+
336
+ Args:
337
+ page (int, optional): Page number for pagination (default: 1).
338
+ country (str, optional): Country name to filter stations.
339
+ name (str, optional): Station name for more precise filtering.
340
+
341
+ Returns:
342
+ str: JSON string containing weather stations matching the filters.
343
+
344
+ This tool is useful for discovering available weather stations and narrowing down by location or name.
345
+ """
346
+ params = {"page": page}
347
+ if country: params["country"] = country
348
+ if name: params["name"] = name
349
+ return make_api_request("/weather/stations.json", params)
350
+
351
+ @mcp.tool()
352
+ async def get_weather_station(station_code: str) -> str:
353
+ """
354
+ Retrieve detailed information about a specific weather station.
355
+
356
+ Args:
357
+ station_code (str): Unique code identifying the weather station.
358
+
359
+ Returns:
360
+ str: JSON string with detailed information about the weather station.
361
+
362
+ Use this tool to access metadata and attributes for a single weather station.
363
+ """
364
+ return make_api_request(f"/weather/stations/{station_code}.json")
365
+
366
+ @mcp.tool()
367
+ async def get_weather_data(station_code: str, start: str = None, end: str = None) -> str:
368
+ """
369
+ Retrieve hourly weather reports for a specific station, optionally filtered by date range.
370
+
371
+ Args:
372
+ station_code (str): Unique code identifying the weather station.
373
+ start (str, optional): Start date/time in ISO format (e.g., '2024-01-01T00:00:00Z').
374
+ end (str, optional): End date/time in ISO format (e.g., '2024-01-31T23:59:59Z').
375
+
376
+ Returns:
377
+ str: JSON string containing hourly weather reports for the specified station and date range.
378
+
379
+ This tool is useful for analyzing weather data over time for a given location.
380
+ """
381
+ params = {}
382
+ if start: params["start"] = start
383
+ if end: params["end"] = end
384
+ return make_api_request(f"/weather/stations/{station_code}/hourly-reports.json", params)
385
+
386
+
387
+ @mcp.tool()
388
+ async def get_cadastral_parcel_geolocation(parcel_id: str) -> str:
389
+ """
390
+ Retrieve geolocation data for a specific cadastral parcel in GeoJSON format.
391
+
392
+ Args:
393
+ parcel_id (str): Unique identifier of the cadastral parcel.
394
+
395
+ Returns:
396
+ str: GeoJSON string with spatial data for the parcel.
397
+
398
+ Use this tool to obtain the geometry of a cadastral parcel for mapping or spatial analysis.
399
+ """
400
+ return make_api_request(f"/geographical-references/cadastral-parcels/{parcel_id}/geolocation.geojson")
401
+
402
+ @mcp.tool()
403
+ async def get_cap_parcel_geolocation(cap_id: str) -> str:
404
+ """
405
+ Retrieve geolocation data for a specific CAP parcel in GeoJSON format.
406
+
407
+ Args:
408
+ cap_id (str): Unique identifier of the CAP parcel.
409
+
410
+ Returns:
411
+ str: GeoJSON string with spatial data for the CAP parcel.
412
+
413
+ Use this tool to obtain the geometry of a CAP parcel for mapping or spatial analysis.
414
+ """
415
+ return make_api_request(f"/geographical-references/cap-parcels/{cap_id}/geolocation.geojson")
416
+
417
+ @mcp.tool()
418
+ async def get_municipality_cadastre(municipality_id: str) -> str:
419
+ """
420
+ Retrieve cadastre data for a municipality in GeoJSON format.
421
+
422
+ Args:
423
+ municipality_id (str): Unique identifier of the municipality.
424
+
425
+ Returns:
426
+ str: GeoJSON string with cadastre data for the municipality.
427
+
428
+ Use this tool to obtain the spatial extent of a municipality's cadastre for mapping or GIS analysis.
429
+ """
430
+ return make_api_request(f"/geographical-references/municipalities/{municipality_id}/cadastre.geojson")
431
+
432
+ if __name__ == "__main__":
433
  mcp.run(transport='stdio')
requirements.txt CHANGED
@@ -1,6 +1,6 @@
1
- gradio[mcp]
2
- anthropic
3
- mcp
4
- openai
5
- mistralai==0.4.2
6
  PyPDF2
 
1
+ gradio[mcp]
2
+ anthropic
3
+ mcp
4
+ openai
5
+ mistralai==0.4.2
6
  PyPDF2
tool_utils.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Fonctions utilitaires
2
+ from typing import Any, Dict, List
3
+
4
+
5
+ def filter_tools_for_context(tools: List[Any], conversation_messages: List[Dict[str, Any]], keywords: List[str]) -> List[Any]:
6
+ """Filtrage simple des outils selon contexte récent et mots-clés."""
7
+ recent_text = " ".join(
8
+ msg["content"] if isinstance(msg.get("content"), str) else " ".join(
9
+ item.get("text", "") for item in msg.get("content", []) if isinstance(item, dict)
10
+ )
11
+ for msg in conversation_messages[-5:]
12
+ ).lower()
13
+
14
+ selected = []
15
+ for tool in tools:
16
+ name = (tool.get("name") if isinstance(tool, dict) else getattr(tool, "name", "")).lower()
17
+ desc = (tool.get("description") if isinstance(tool, dict) else getattr(tool, "description", "")).lower()
18
+ if not keywords or any(kw.lower() in (name + desc + recent_text) for kw in keywords):
19
+ selected.append(tool)
20
+ return selected
21
+
22
+
23
+ def summarize_latest_results(conversation_messages: List[Dict[str, Any]]) -> str:
24
+ """Résumé des derniers résultats outils."""
25
+ summaries = []
26
+ for msg in conversation_messages:
27
+ if msg.get("role") == "user" and isinstance(msg.get("content"), list):
28
+ for item in msg["content"]:
29
+ if isinstance(item, dict) and item.get("type") == "tool_result":
30
+ summaries.append(item.get("content", ""))
31
+ return "\n".join(summaries[-5:]).strip()
32
+
33
+
34
+ def count_tokens(conversation_messages: List[Dict[str, Any]]) -> int:
35
+ """Comptage naïf de tokens (optimisation mémoire)."""
36
+ total = 0
37
+ for msg in conversation_messages:
38
+ content = msg.get("content")
39
+ if isinstance(content, str):
40
+ total += len(content.split())
41
+ elif isinstance(content, list):
42
+ for item in content:
43
+ if isinstance(item, dict) and "text" in item:
44
+ total += len(item["text"].split())
45
+ elif isinstance(item, str):
46
+ total += len(item.split())
47
+ return total
48
+
49
+
50
+ def trim_conversation(conversation_messages: List[Dict[str, Any]], keep_last_n: int = 5) -> List[Dict[str, Any]]:
51
+ """Réduction du contexte conversationnel."""
52
+ if len(conversation_messages) <= keep_last_n:
53
+ return conversation_messages
54
+ trimmed = conversation_messages[-keep_last_n:]
55
+ trimmed.insert(0, {"role": "system", "content": "Résumé des messages précédents supprimés pour raison de contexte."})
56
+ return trimmed