mgokg commited on
Commit
0abe445
·
verified ·
1 Parent(s): 05ae9d4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +169 -314
app.py CHANGED
@@ -1,332 +1,187 @@
1
  import gradio as gr
2
  import os
3
- import json
4
  import asyncio
5
- import httpx
6
- from typing import Optional, Dict, Any
7
-
8
- class MCPHTTPClient:
9
- """MCP Client für Streamable HTTP Transport"""
10
-
11
- def __init__(self, server_url: str):
12
- self.server_url = server_url.rstrip('/')
13
- self.tools = []
14
- self.client = httpx.AsyncClient(timeout=30.0)
15
-
16
- async def initialize(self):
17
- """Initialisiere den Client und hole verfügbare Tools"""
18
- try:
19
- # MCP über HTTP: Sende initialize request
20
- response = await self.client.post(
21
- f"{self.server_url}/initialize",
22
- json={
23
- "jsonrpc": "2.0",
24
- "id": 1,
25
- "method": "initialize",
26
- "params": {
27
- "protocolVersion": "2024-11-05",
28
- "capabilities": {},
29
- "clientInfo": {
30
- "name": "gradio-mcp-client",
31
- "version": "1.0.0"
32
- }
33
- }
34
- }
35
- )
36
-
37
- if response.status_code == 200:
38
- print("MCP-Server initialisiert")
39
- await self.list_tools()
40
- return True
41
- else:
42
- print(f"Initialisierung fehlgeschlagen: {response.status_code}")
43
- return False
44
-
45
- except Exception as e:
46
- print(f"Verbindungsfehler bei Initialisierung: {e}")
47
- return False
48
-
49
- async def list_tools(self):
50
- """Rufe verfügbare Tools vom MCP-Server ab"""
51
- try:
52
- response = await self.client.post(
53
- f"{self.server_url}/list_tools",
54
- json={
55
- "jsonrpc": "2.0",
56
- "id": 2,
57
- "method": "tools/list",
58
- "params": {}
59
- }
60
- )
61
-
62
- if response.status_code == 200:
63
- data = response.json()
64
- if "result" in data and "tools" in data["result"]:
65
- self.tools = data["result"]["tools"]
66
- print(f"Verfügbare Tools: {[tool['name'] for tool in self.tools]}")
67
- else:
68
- print("Keine Tools gefunden")
69
- else:
70
- print(f"Fehler beim Abrufen der Tools: {response.status_code}")
71
-
72
- except Exception as e:
73
- print(f"Fehler beim Abrufen der Tools: {e}")
74
-
75
- async def call_tool(self, tool_name: str, arguments: Dict[str, Any]):
76
- """Rufe ein Tool auf dem MCP-Server auf"""
77
- try:
78
- response = await self.client.post(
79
- f"{self.server_url}/call_tool",
80
- json={
81
- "jsonrpc": "2.0",
82
- "id": 3,
83
- "method": "tools/call",
84
- "params": {
85
- "name": tool_name,
86
- "arguments": arguments
87
- }
88
- }
89
- )
90
-
91
- if response.status_code == 200:
92
- data = response.json()
93
- if "result" in data:
94
- return data["result"]
95
- else:
96
- print(f"Unerwartete Antwort: {data}")
97
- return None
98
- else:
99
- print(f"Tool-Aufruf fehlgeschlagen: {response.status_code}")
100
- print(f"Response: {response.text}")
101
- return None
102
-
103
- except Exception as e:
104
- print(f"Fehler beim Tool-Aufruf: {e}")
105
- return None
106
-
107
- async def search_connections(self, origin: str, destination: str):
108
- """Suche Zugverbindungen über den MCP-Server"""
109
- try:
110
- # Verwende das db_timetable_api_ui_wrapper Tool
111
- result = await self.call_tool(
112
- "db_timetable_api_ui_wrapper",
113
- {
114
- "origin": origin,
115
- "destination": destination
116
- }
117
- )
118
-
119
- if result:
120
- return self._parse_result(result)
121
- return None
122
-
123
- except Exception as e:
124
- print(f"Fehler bei der Verbindungssuche: {e}")
125
- return None
126
-
127
- def _parse_result(self, result):
128
- """Parse die Antwort vom MCP-Server"""
129
- try:
130
- # MCP-Server gibt Ergebnisse im content-Array zurück
131
- if isinstance(result, dict) and "content" in result:
132
- content = result["content"]
133
- if isinstance(content, list) and len(content) > 0:
134
- if isinstance(content[0], dict) and "text" in content[0]:
135
- return content[0]["text"]
136
- elif hasattr(content[0], 'text'):
137
- return content[0].text
138
- return str(content)
139
-
140
- return str(result)
141
-
142
- except Exception as e:
143
- print(f"Fehler beim Parsen: {e}")
144
- return str(result)
145
 
146
- async def close(self):
147
- """Schließe die HTTP-Verbindung"""
148
- await self.client.aclose()
149
-
150
-
151
- # Globaler MCP-Client
152
- mcp_client: Optional[MCPHTTPClient] = None
153
- MCP_SERVER_URL = "https://mgokg-db-timetable-api.hf.space/gradio_api/mcp"
154
-
155
-
156
- async def initialize_mcp_client():
157
- """Initialisiere den MCP-Client"""
158
- global mcp_client
159
  try:
160
- mcp_client = MCPHTTPClient(MCP_SERVER_URL)
161
- success = await mcp_client.initialize()
162
-
163
- if success:
164
- return "✅ MCP-Client erfolgreich initialisiert"
165
- else:
166
- return "⚠️ MCP-Client initialisiert, aber keine Tools verfügbar"
167
-
168
  except Exception as e:
169
- print(f"Fehler bei der Initialisierung: {e}")
170
- return f"❌ Fehler bei der Initialisierung: {e}"
171
 
172
-
173
- async def search_train_connections_async(user_input: str):
174
- """Asynchrone Funktion zur Suche von Zugverbindungen"""
175
- global mcp_client
176
-
177
- # Initialisiere Client falls noch nicht geschehen
178
- if mcp_client is None:
179
- init_msg = await initialize_mcp_client()
180
- if "❌" in init_msg:
181
- return f"{init_msg}\n\nBitte versuchen Sie es später erneut."
182
-
183
  try:
184
- # Extrahiere Start und Ziel aus der Benutzereingabe
185
- user_input_lower = user_input.lower().strip()
186
-
187
- # Verschiedene Parsing-Strategien
188
- origin, destination = None, None
189
-
190
- if " nach " in user_input_lower:
191
- parts = user_input_lower.split(" nach ")
192
- origin = parts[0].replace("von", "").strip()
193
- destination = parts[1].strip()
194
- elif " - " in user_input_lower:
195
- parts = user_input_lower.split(" - ")
196
- origin = parts[0].strip()
197
- destination = parts[1].strip()
198
- else:
199
- # Versuche einfache Tokenisierung
200
- words = user_input_lower.replace("von", "").replace(",", "").split()
201
- if len(words) >= 2:
202
- origin = words[0]
203
- destination = words[-1]
204
-
205
- if not origin or not destination:
206
- return """❓ Bitte geben Sie Start- und Zielort an.
207
-
208
- **Beispiele:**
209
- - Frankfurt nach Berlin
210
- - Von München nach Hamburg
211
- - Köln - Stuttgart
212
- - Hamburg Berlin"""
213
-
214
- # Kapitalisiere die Namen
215
- origin = origin.capitalize()
216
- destination = destination.capitalize()
217
-
218
- # Zeige Suche an
219
- print(f"Suche Verbindungen: {origin} → {destination}")
220
-
221
- # Rufe MCP-Server auf
222
- result = await mcp_client.search_connections(origin, destination)
223
-
224
- if result:
225
- # Formatiere Ergebnis als Markdown
226
- response = f"## 🚂 Zugverbindungen von {origin} nach {destination}\n\n"
227
- response += result
228
- return response
229
- else:
230
- return f"""⚠️ Keine Verbindungen gefunden oder Fehler bei der Abfrage.
231
 
232
- **Gesuchte Route:** {origin} {destination}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
 
234
- Bitte prüfen Sie:
235
- - Sind die Ortsnamen korrekt geschrieben?
236
- - Existieren die Bahnhöfe?
237
- - Versuchen Sie es mit vollständigen Stadtnamen"""
238
-
239
  except Exception as e:
240
- error_msg = str(e)
241
- print(f"Fehler bei der Suche: {error_msg}")
242
- return f"""❌ Fehler bei der Suche: {error_msg}
243
-
244
- Bitte versuchen Sie es erneut oder verwenden Sie ein anderes Format.
245
 
246
- **Beispiele:**
247
- - Frankfurt nach Berlin
248
- - Von München nach Hamburg"""
249
 
 
 
 
 
 
 
 
250
 
251
- def search_train_connections(user_input: str):
252
- """Synchrone Wrapper-Funktion für Gradio"""
253
- if not user_input or not user_input.strip():
254
- return "Bitte geben Sie eine Anfrage ein.", ""
255
-
256
- try:
257
- # Erstelle neuen Event Loop für die asynchrone Funktion
258
- loop = asyncio.new_event_loop()
259
- asyncio.set_event_loop(loop)
260
- result = loop.run_until_complete(search_train_connections_async(user_input))
261
- loop.close()
262
- return result, ""
263
- except Exception as e:
264
- return f"❌ Fehler: {str(e)}", ""
265
 
 
 
 
 
 
 
 
 
 
 
 
266
 
267
- if __name__ == '__main__':
268
- # Initialisiere MCP-Client beim Start
269
- print("=" * 60)
270
- print("🚂 Deutsche Bahn MCP-Client wird gestartet...")
271
- print("=" * 60)
272
-
273
- try:
274
- loop = asyncio.new_event_loop()
275
- asyncio.set_event_loop(loop)
276
- init_result = loop.run_until_complete(initialize_mcp_client())
277
- print(init_result)
278
- loop.close()
279
- except Exception as e:
280
- print(f"⚠️ Warnung bei Initialisierung: {e}")
281
- print("Client wird bei erster Anfrage initialisiert.")
282
-
283
- print("=" * 60)
284
-
285
- # Gradio Interface
286
- with gr.Blocks(theme=gr.themes.Soft()) as demo:
287
- title = gr.Markdown("# 🚂 Deutsche Bahn Zugverbindungen")
288
- gr.Markdown("### MCP-Client für DB Timetable API")
289
-
290
- description = gr.Markdown("""
291
- Geben Sie Start- und Zielort ein, um aktuelle Zugverbindungen zu finden.
292
-
293
- **Beispiele für Eingaben:**
294
- - `Frankfurt nach Berlin`
295
- - `Von München nach Hamburg`
296
- - `Köln - Stuttgart`
297
- - `Hamburg Berlin`
298
- """)
299
-
300
- with gr.Row():
301
- with gr.Column(scale=4):
302
- input_textbox = gr.Textbox(
303
- lines=2,
304
- label="Ihre Anfrage",
305
- placeholder="z.B. 'Frankfurt nach Berlin' oder 'Von München nach Hamburg'",
306
- autofocus=True
307
- )
308
- with gr.Column(scale=1):
309
- submit_button = gr.Button("🔍 Suchen", variant="primary", size="lg")
310
-
311
- output_textbox = gr.Markdown(label="Ergebnisse")
312
-
313
- gr.Markdown("""
314
- ---
315
- 💡 **Hinweis:** Dieser Client nutzt die Deutsche Bahn Timetable API über einen MCP-Server.
316
- """)
317
-
318
- # Event Handler
319
- submit_button.click(
320
- fn=search_train_connections,
321
- inputs=input_textbox,
322
- outputs=[output_textbox, input_textbox]
323
  )
324
-
325
- # Enter-Taste funktioniert auch
326
- input_textbox.submit(
327
- fn=search_train_connections,
328
- inputs=input_textbox,
329
- outputs=[output_textbox, input_textbox]
330
- )
331
-
332
  demo.launch(show_error=True)
 
1
  import gradio as gr
2
  import os
 
3
  import asyncio
4
+ import json
5
+ from google import genai
6
+ from google.genai import types
7
+
8
+ # MCP Imports
9
+ from mcp import ClientSession, StdioServerParameters
10
+ from mcp.client.sse import sse_client
11
+ from mcp.types import CallToolResult
12
+
13
+ # Konfiguration
14
+ MCP_SERVER_URL = "https://mgokg-db-timetable-api.hf.space/gradio_api/mcp/"
15
+ MODEL_ID = "gemini-2.0-flash-exp" # Oder "gemini-flash-latest" je nach Verfügbarkeit
16
+
17
+ async def generate(input_text, history):
18
+ """
19
+ Hauptfunktion:
20
+ 1. Verbindet sich via SSE mit dem MCP Server.
21
+ 2. Holt Tool-Definitionen.
22
+ 3. Sendet User-Input + Tools an Gemini.
23
+ 4. Führt Tool-Calls aus (falls Gemini das will).
24
+ 5. Gibt die Antwort zurück.
25
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
+ # 1. Gemini Client initialisieren
 
 
 
 
 
 
 
 
 
 
 
 
28
  try:
29
+ client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY"))
 
 
 
 
 
 
 
30
  except Exception as e:
31
+ yield f"Error initializing Gemini client: {e}. Make sure GEMINI_API_KEY is set."
32
+ return
33
 
34
+ # 2. Verbindung zum MCP Server aufbauen (Context Manager)
 
 
 
 
 
 
 
 
 
 
35
  try:
36
+ async with sse_client(url=MCP_SERVER_URL) as streams:
37
+ async with ClientSession(streams.read, streams.write) as session:
38
+ await session.initialize()
39
+
40
+ # Verfügbare Tools vom MCP Server abrufen
41
+ mcp_list_tools_result = await session.list_tools()
42
+ mcp_tools = mcp_list_tools_result.tools
43
+
44
+ # 3. Tools für Gemini konvertieren
45
+ gemini_tools_declarations = []
46
+ for tool in mcp_tools:
47
+ # Fallback Description, falls leer (wie in der Warnung beschrieben)
48
+ description = tool.description or "Tool to query DB (Deutsche Bahn) train timetables and connections."
49
+
50
+ gemini_tools_declarations.append(
51
+ types.FunctionDeclaration(
52
+ name=tool.name,
53
+ description=description,
54
+ parameters=tool.inputSchema
55
+ )
56
+ )
57
+
58
+ # Zusammenbau der Tools (Websearch + MCP Functions)
59
+ tools_config = [
60
+ types.Tool(google_search=types.GoogleSearch()),
61
+ types.Tool(function_declarations=gemini_tools_declarations)
62
+ ]
63
+
64
+ # Konfiguration für den Chat
65
+ generate_content_config = types.GenerateContentConfig(
66
+ temperature=0.4,
67
+ tools=tools_config,
68
+ system_instruction="You are a helpful assistant. You have access to Google Search and a tool called 'db_timetable_api_ui_wrapper' to check train connections (Deutsche Bahn). Use the train tool when the user asks for train times.",
69
+ response_mime_type="text/plain",
70
+ )
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
+ # Chat History aufbauen (optional, hier vereinfacht nur aktueller Turn)
73
+ # Für echte History müsste man 'history' parsen.
74
+ contents = [
75
+ types.Content(
76
+ role="user",
77
+ parts=[types.Part.from_text(text=input_text)],
78
+ ),
79
+ ]
80
+
81
+ # --- Die Chat-Schleife (Model -> Tool -> Model) ---
82
+ while True:
83
+ response_stream = await client.models.generate_content_stream(
84
+ model=MODEL_ID,
85
+ contents=contents,
86
+ config=generate_content_config,
87
+ )
88
+
89
+ full_response_text = ""
90
+ function_calls = []
91
+
92
+ # Stream verarbeiten
93
+ async for chunk in response_stream:
94
+ # Text Teile sammeln
95
+ if chunk.text:
96
+ full_response_text += chunk.text
97
+ yield full_response_text
98
+
99
+ # Funktionsaufrufe sammeln (passieren meist am Ende oder in einem Block)
100
+ if chunk.function_calls:
101
+ for fc in chunk.function_calls:
102
+ function_calls.append(fc)
103
+
104
+ # Wenn keine Funktionsaufrufe -> Fertig
105
+ if not function_calls:
106
+ break
107
+
108
+ # Wenn Funktionsaufrufe vorhanden sind -> Ausführen
109
+ # Wir fügen die Antwort des Modells (die den Aufruf enthält) zur Historie hinzu
110
+ contents.append(types.Content(
111
+ role="model",
112
+ parts=[types.Part.from_function_call(name=fc.name, args=fc.args) for fc in function_calls]
113
+ ))
114
+
115
+ # Jeden Call ausführen
116
+ for fc in function_calls:
117
+ yield f"\n\n*Calling MCP Tool: {fc.name}...*\n"
118
+
119
+ try:
120
+ # Aufruf an den MCP Server
121
+ result: CallToolResult = await session.call_tool(
122
+ name=fc.name,
123
+ arguments=fc.args
124
+ )
125
+
126
+ # Ergebnis extrahieren (Text aus dem Content)
127
+ tool_output_text = ""
128
+ if result.content:
129
+ for content_item in result.content:
130
+ if content_item.type == "text":
131
+ tool_output_text += content_item.text
132
+
133
+ # Ergebnis zur Historie hinzufügen
134
+ contents.append(types.Content(
135
+ role="tool",
136
+ parts=[types.Part.from_function_response(
137
+ name=fc.name,
138
+ response={"result": tool_output_text}
139
+ )]
140
+ ))
141
+
142
+ except Exception as tool_error:
143
+ error_msg = f"Error executing tool {fc.name}: {str(tool_error)}"
144
+ contents.append(types.Content(
145
+ role="tool",
146
+ parts=[types.Part.from_function_response(
147
+ name=fc.name,
148
+ response={"error": error_msg}
149
+ )]
150
+ ))
151
+
152
+ # Schleife läuft weiter -> Nächster Aufruf an Gemini mit den Tool-Ergebnissen
153
 
 
 
 
 
 
154
  except Exception as e:
155
+ yield f"An error occurred: {e}"
 
 
 
 
156
 
 
 
 
157
 
158
+ if __name__ == '__main__':
159
+ with gr.Blocks() as demo:
160
+ gr.Markdown("# Gemini 2.0 Flash + Websearch + MCP (DB Timetable)")
161
+
162
+ chatbot = gr.Chatbot(height=600, type="messages")
163
+ msg = gr.Textbox(label="Nachricht eingeben (z.B. 'Zug von Berlin nach München morgen früh')", lines=2)
164
+ clear = gr.Button("Clear")
165
 
166
+ async def user_message(user_input, history):
167
+ # Zeige User Nachricht sofort an
168
+ return "", history + [{"role": "user", "content": user_input}]
 
 
 
 
 
 
 
 
 
 
 
169
 
170
+ async def bot_response(history):
171
+ # Letzte User Nachricht holen
172
+ user_input = history[-1]["content"]
173
+
174
+ # Generator starten
175
+ # Wir bauen die Antwort Stück für Stück auf
176
+ history.append({"role": "assistant", "content": ""})
177
+
178
+ async for chunk in generate(user_input, history[:-1]):
179
+ history[-1]["content"] = chunk
180
+ yield history
181
 
182
+ msg.submit(user_message, [msg, chatbot], [msg, chatbot], queue=False).then(
183
+ bot_response, [chatbot], [chatbot]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  )
185
+ clear.click(lambda: None, None, chatbot, queue=False)
186
+
 
 
 
 
 
 
187
  demo.launch(show_error=True)