PD03 commited on
Commit
d207e49
Β·
verified Β·
1 Parent(s): c1df0d9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +424 -40
app.py CHANGED
@@ -3,71 +3,455 @@ import openai
3
  import requests
4
  import json
5
  from typing import Dict, Any, List, Tuple
 
 
6
 
7
  class MCPClient:
 
 
8
  def __init__(self, server_url: str):
9
  self.server_url = server_url.rstrip('/')
10
-
11
  def call_tool_sync(self, tool_name: str, arguments: Dict[str, Any] = None) -> Dict[str, Any]:
 
12
  if arguments is None:
13
  arguments = {}
14
-
15
- mcp_request = {"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": tool_name, "arguments": arguments}}
16
- response = requests.post(f"{self.server_url}/mcp", json=mcp_request, headers={"Content-Type": "application/json"})
17
- return response.json()
18
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  def list_tools_sync(self) -> List[Dict[str, Any]]:
20
- response = requests.post(f"{self.server_url}/mcp", json={"jsonrpc": "2.0", "id": 1, "method": "tools/list"}, headers={"Content-Type": "application/json"})
21
- return response.json().get("result", {}).get("tools", [])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
  class AIAssistant:
 
 
24
  def __init__(self, openai_api_key: str, mcp_client: MCPClient):
25
- openai.api_key = openai_api_key
 
 
 
 
 
 
 
 
26
  self.mcp_client = mcp_client
 
 
 
 
27
  self.available_tools = self.mcp_client.list_tools_sync()
28
-
29
  def get_system_prompt(self) -> str:
30
- return f"Available tools: {[tool['name'] for tool in self.available_tools]}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  def extract_tool_calls(self, response: str) -> List[Dict[str, Any]]:
 
33
  tool_calls = []
34
  lines = response.split('\n')
 
35
  for line in lines:
 
36
  if line.startswith('CALL_TOOL:'):
37
- tool_name = line.split(':')[1].strip()
38
- tool_calls.append({'name': tool_name, 'arguments': {}})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  return tool_calls
40
-
41
- async def process_message(self, user_message: str) -> Tuple[str, str]:
42
- messages = [{"role": "system", "content": self.get_system_prompt()}, {"role": "user", "content": user_message}]
43
- response = openai.ChatCompletion.create(model="gpt-3.5-turbo", messages=messages)
44
- ai_response = response.choices[0].message.content
45
-
46
  tool_info = ""
47
- tool_calls = self.extract_tool_calls(ai_response)
48
- if tool_calls:
49
- for tool_call in tool_calls:
50
- result = self.mcp_client.call_tool_sync(tool_call['name'])
51
- tool_info += f"Called {tool_call['name']}: {result}\n"
52
- ai_response += f"\n\nTool results:\n{tool_info}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
- return ai_response, tool_info
 
 
55
 
56
- async def chat_interface(message, history, openai_key, mcp_url):
57
- assistant = AIAssistant(openai_key, MCPClient(mcp_url))
58
- response, tool_info = await assistant.process_message(message)
59
- history.append([message, response])
60
- return history, ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
- with gr.Blocks() as demo:
63
- chatbot = gr.Chatbot()
64
- msg = gr.Textbox()
65
- openai_key = gr.Textbox(label="OpenAI API Key")
66
- mcp_url = gr.Textbox(label="MCP Server URL")
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
- submit_btn = gr.Button("Send")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
- submit_btn.click(chat_interface, [msg, chatbot, openai_key, mcp_url], [chatbot, msg])
71
- msg.submit(chat_interface, [msg, chatbot, openai_key, mcp_url], [chatbot, msg])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
- demo.launch()
 
 
3
  import requests
4
  import json
5
  from typing import Dict, Any, List, Tuple
6
+ from datetime import datetime
7
+ import os
8
 
9
  class MCPClient:
10
+ """MCP Client for communicating with the MCP server"""
11
+
12
  def __init__(self, server_url: str):
13
  self.server_url = server_url.rstrip('/')
14
+
15
  def call_tool_sync(self, tool_name: str, arguments: Dict[str, Any] = None) -> Dict[str, Any]:
16
+ """Synchronous tool call using requests instead of aiohttp"""
17
  if arguments is None:
18
  arguments = {}
19
+
20
+ mcp_request = {
21
+ "jsonrpc": "2.0",
22
+ "id": 1,
23
+ "method": "tools/call",
24
+ "params": {
25
+ "name": tool_name,
26
+ "arguments": arguments
27
+ }
28
+ }
29
+
30
+ try:
31
+ response = requests.post(
32
+ f"{self.server_url}/mcp",
33
+ json=mcp_request,
34
+ headers={
35
+ "Content-Type": "application/json",
36
+ "ngrok-skip-browser-warning": "true"
37
+ },
38
+ timeout=30
39
+ )
40
+
41
+ if response.status_code == 200:
42
+ result = response.json()
43
+ if "result" in result and "content" in result["result"]:
44
+ content = result["result"]["content"][0]["text"]
45
+ return json.loads(content)
46
+ return result
47
+ else:
48
+ return {
49
+ "success": False,
50
+ "error": f"HTTP {response.status_code}: {response.text}"
51
+ }
52
+ except Exception as e:
53
+ return {
54
+ "success": False,
55
+ "error": f"Connection error: {str(e)}"
56
+ }
57
+
58
  def list_tools_sync(self) -> List[Dict[str, Any]]:
59
+ """Synchronous tool listing using requests"""
60
+ mcp_request = {
61
+ "jsonrpc": "2.0",
62
+ "id": 1,
63
+ "method": "tools/list"
64
+ }
65
+
66
+ try:
67
+ response = requests.post(
68
+ f"{self.server_url}/mcp",
69
+ json=mcp_request,
70
+ headers={
71
+ "Content-Type": "application/json",
72
+ "ngrok-skip-browser-warning": "true"
73
+ },
74
+ timeout=30
75
+ )
76
+
77
+ if response.status_code == 200:
78
+ result = response.json()
79
+ return result.get("result", {}).get("tools", [])
80
+ return []
81
+ except Exception as e:
82
+ print(f"Error listing tools: {str(e)}")
83
+ return []
84
 
85
  class AIAssistant:
86
+ """AI Assistant with MCP integration"""
87
+
88
  def __init__(self, openai_api_key: str, mcp_client: MCPClient):
89
+ try:
90
+ self.openai_client = openai.OpenAI(
91
+ api_key=openai_api_key,
92
+ timeout=30.0
93
+ )
94
+ except Exception as e:
95
+ # Fallback for older OpenAI versions
96
+ openai.api_key = openai_api_key
97
+ self.openai_client = openai
98
  self.mcp_client = mcp_client
99
+ self.available_tools = []
100
+
101
+ def initialize(self):
102
+ """Initialize the assistant by fetching available tools"""
103
  self.available_tools = self.mcp_client.list_tools_sync()
104
+
105
  def get_system_prompt(self) -> str:
106
+ """Generate system prompt with available tools"""
107
+ tools_description = "\n".join([
108
+ f"- {tool['name']}: {tool['description']}"
109
+ for tool in self.available_tools
110
+ ])
111
+
112
+ return f"""You are an AI assistant with access to SAP business systems and news data through specialized tools.
113
+
114
+ Available tools:
115
+ {tools_description}
116
+
117
+ When a user asks for information that can be retrieved using these tools, you should:
118
+ 1. Identify which tool(s) would be helpful
119
+ 2. Call the appropriate tool(s) with the right parameters
120
+ 3. Wait for the results before providing your final response
121
 
122
+ For SAP-related queries (purchase orders, requisitions), use the SAP tools.
123
+ For news-related queries, use the news tools.
124
+
125
+ To call a tool, use this exact format:
126
+ CALL_TOOL: tool_name
127
+ or
128
+ CALL_TOOL: tool_name(parameter1=value1, parameter2=value2)
129
+
130
+ Examples:
131
+ - For "show me purchase orders": CALL_TOOL: get_purchase_orders
132
+ - For "get 20 purchase orders": CALL_TOOL: get_purchase_orders(top=20)
133
+ - For "latest tech news": CALL_TOOL: get_news_headlines(category=technology)
134
+
135
+ After calling a tool, I will provide you with the results to interpret for the user.
136
+ """
137
+
138
  def extract_tool_calls(self, response: str) -> List[Dict[str, Any]]:
139
+ """Extract tool calls from AI response"""
140
  tool_calls = []
141
  lines = response.split('\n')
142
+
143
  for line in lines:
144
+ line = line.strip()
145
  if line.startswith('CALL_TOOL:'):
146
+ try:
147
+ # Remove 'CALL_TOOL:' prefix and clean up
148
+ tool_part = line[10:].strip()
149
+
150
+ # Handle cases with or without parentheses
151
+ if '(' in tool_part and ')' in tool_part:
152
+ tool_name = tool_part.split('(')[0].strip()
153
+ params_str = tool_part.split('(')[1].split(')')[0]
154
+
155
+ params = {}
156
+ if params_str.strip():
157
+ for param in params_str.split(','):
158
+ if '=' in param:
159
+ key, value = param.split('=', 1)
160
+ key = key.strip()
161
+ value = value.strip().strip('"\'')
162
+ try:
163
+ if value.isdigit():
164
+ value = int(value)
165
+ elif value.lower() in ['true', 'false']:
166
+ value = value.lower() == 'true'
167
+ except:
168
+ pass
169
+ params[key] = value
170
+
171
+ tool_calls.append({
172
+ 'name': tool_name,
173
+ 'arguments': params
174
+ })
175
+ else:
176
+ # Simple tool call without parameters
177
+ tool_name = tool_part.strip()
178
+ tool_calls.append({
179
+ 'name': tool_name,
180
+ 'arguments': {}
181
+ })
182
+
183
+ except Exception as e:
184
+ print(f"Error parsing tool call '{line}': {e}")
185
+ continue
186
+
187
  return tool_calls
188
+
189
+ def process_message(self, user_message: str) -> Tuple[str, str]:
190
+ """Process user message and handle tool calls"""
 
 
 
191
  tool_info = ""
192
+
193
+ try:
194
+ messages = [
195
+ {"role": "system", "content": self.get_system_prompt()},
196
+ {"role": "user", "content": user_message}
197
+ ]
198
+
199
+ # Check if we have a proper OpenAI client
200
+ if hasattr(self.openai_client, 'chat'):
201
+ response = self.openai_client.chat.completions.create(
202
+ model="gpt-3.5-turbo",
203
+ messages=messages,
204
+ temperature=0.7,
205
+ max_tokens=1000
206
+ )
207
+ ai_response = response.choices[0].message.content
208
+ else:
209
+ # Fallback for older API
210
+ response = self.openai_client.ChatCompletion.create(
211
+ model="gpt-3.5-turbo",
212
+ messages=messages,
213
+ temperature=0.7,
214
+ max_tokens=1000
215
+ )
216
+ ai_response = response.choices[0].message.content
217
+ tool_calls = self.extract_tool_calls(ai_response)
218
+
219
+ # Debug information
220
+ print(f"AI Response: {ai_response}")
221
+ print(f"Extracted tool calls: {tool_calls}")
222
+
223
+ if tool_calls:
224
+ tool_results = []
225
+
226
+ for tool_call in tool_calls:
227
+ tool_info += f"πŸ”§ Calling: {tool_call['name']}\n"
228
+
229
+ result = await self.mcp_client.call_tool(
230
+ tool_call['name'],
231
+ tool_call['arguments']
232
+ )
233
+
234
+ tool_results.append({
235
+ 'tool': tool_call['name'],
236
+ 'result': result
237
+ })
238
+
239
+ if result.get('success'):
240
+ tool_info += f"βœ… {tool_call['name']} completed\n"
241
+ else:
242
+ tool_info += f"❌ {tool_call['name']} failed: {result.get('error', 'Unknown error')}\n"
243
+
244
+ tool_results_text = "\n\n".join([
245
+ f"Tool: {tr['tool']}\nResult: {json.dumps(tr['result'], indent=2)}"
246
+ for tr in tool_results
247
+ ])
248
+
249
+ final_messages = messages + [
250
+ {"role": "assistant", "content": ai_response},
251
+ {"role": "user", "content": f"Here are the tool results:\n\n{tool_results_text}\n\nPlease interpret these results and provide a helpful response to the user."}
252
+ ]
253
+
254
+ # Get final response with tool results
255
+ if hasattr(self.openai_client, 'chat'):
256
+ final_response = self.openai_client.chat.completions.create(
257
+ model="gpt-3.5-turbo",
258
+ messages=final_messages,
259
+ temperature=0.7,
260
+ max_tokens=1000
261
+ )
262
+ return final_response.choices[0].message.content, tool_info
263
+ else:
264
+ final_response = self.openai_client.ChatCompletion.create(
265
+ model="gpt-3.5-turbo",
266
+ messages=final_messages,
267
+ temperature=0.7,
268
+ max_tokens=1000
269
+ )
270
+ return final_response.choices[0].message.content, tool_info
271
+ else:
272
+ return ai_response, ""
273
+
274
+ except Exception as e:
275
+ return f"❌ Error processing your request: {str(e)}", ""
276
 
277
+ # Global variables
278
+ assistant = None
279
+ mcp_client = None
280
 
281
+ def test_connection(mcp_url):
282
+ """Test MCP server connection"""
283
+ if not mcp_url or mcp_url == "https://your-ngrok-url.ngrok.io":
284
+ return "❌ Please enter a valid MCP server URL"
285
+
286
+ try:
287
+ # Test health endpoint
288
+ response = requests.get(f"{mcp_url.rstrip('/')}/health", timeout=10)
289
+ if response.status_code == 200:
290
+ data = response.json()
291
+
292
+ # Test MCP tools list
293
+ mcp_request = {
294
+ "jsonrpc": "2.0",
295
+ "id": 1,
296
+ "method": "tools/list"
297
+ }
298
+
299
+ mcp_response = requests.post(
300
+ f"{mcp_url.rstrip('/')}/mcp",
301
+ json=mcp_request,
302
+ headers={
303
+ "Content-Type": "application/json",
304
+ "ngrok-skip-browser-warning": "true"
305
+ },
306
+ timeout=10
307
+ )
308
+
309
+ if mcp_response.status_code == 200:
310
+ mcp_data = mcp_response.json()
311
+ tools = mcp_data.get("result", {}).get("tools", [])
312
+ tool_names = [tool.get("name", "Unknown") for tool in tools]
313
+
314
+ return f"βœ… Connected successfully!\nHealth Status: {data.get('status', 'Unknown')}\nMCP Tools: {len(tools)}\nAvailable: {', '.join(tool_names)}"
315
+ else:
316
+ return f"βœ… Health OK, but MCP endpoint failed: HTTP {mcp_response.status_code}"
317
+ else:
318
+ return f"❌ Connection failed: HTTP {response.status_code}"
319
+ except Exception as e:
320
+ return f"❌ Connection error: {str(e)}"
321
 
322
+ def initialize_assistant(openai_key, mcp_url):
323
+ """Initialize the AI assistant"""
324
+ global assistant, mcp_client
325
+
326
+ if not openai_key:
327
+ return "❌ Please enter your OpenAI API key"
328
+
329
+ if not mcp_url or mcp_url == "https://your-ngrok-url.ngrok.io":
330
+ return "❌ Please enter a valid MCP server URL"
331
+
332
+ try:
333
+ mcp_client = MCPClient(mcp_url)
334
+ assistant = AIAssistant(openai_key, mcp_client)
335
+ assistant.initialize()
336
+ return f"βœ… AI Assistant initialized with {len(assistant.available_tools)} tools available"
337
+ except Exception as e:
338
+ return f"❌ Failed to initialize: {str(e)}"
339
 
340
+ def chat_interface(message, history, openai_key, mcp_url):
341
+ """Main chat interface"""
342
+ global assistant
343
+
344
+ if not assistant:
345
+ init_result = initialize_assistant(openai_key, mcp_url)
346
+ if "❌" in init_result:
347
+ history.append([message, init_result])
348
+ return history, ""
349
+
350
+ try:
351
+ print(f"Calling process_message with: {message}")
352
+
353
+ # Make sure we call the synchronous method
354
+ result = assistant.process_message(message)
355
+ print(f"process_message returned: {type(result)} - {result}")
356
+
357
+ # Check if result is a tuple (response, tool_info)
358
+ if isinstance(result, tuple) and len(result) == 2:
359
+ response, tool_info = result
360
+ print(f"Unpacked: response={response}, tool_info={tool_info}")
361
+ else:
362
+ response = str(result)
363
+ tool_info = ""
364
+ print(f"Single result: {response}")
365
+
366
+ # Format response with tool info if available
367
+ if tool_info:
368
+ full_response = f"**Tool Execution:**\n{tool_info}\n\n**Response:**\n{response}"
369
+ else:
370
+ full_response = response
371
+
372
+ history.append([message, full_response])
373
+ return history, ""
374
+ except Exception as e:
375
+ import traceback
376
+ error_response = f"❌ Error: {str(e)}\n\nTraceback:\n{traceback.format_exc()}"
377
+ print(f"Error in chat_interface: {error_response}")
378
+ history.append([message, error_response])
379
+ return history, ""
380
 
381
+ # Create Gradio interface
382
+ with gr.Blocks(title="AI Assistant with SAP & News Integration", theme=gr.themes.Soft()) as demo:
383
+ gr.Markdown("# πŸ€– AI Assistant with SAP & News Integration")
384
+ gr.Markdown("Chat with an AI that can access SAP business data and news through natural language queries.")
385
+
386
+ with gr.Row():
387
+ with gr.Column(scale=2):
388
+ chatbot = gr.Chatbot(
389
+ height=500,
390
+ show_label=False,
391
+ container=True,
392
+ bubble_full_width=False
393
+ )
394
+
395
+ msg = gr.Textbox(
396
+ placeholder="Ask me about SAP data, news, or anything else...",
397
+ show_label=False,
398
+ container=False
399
+ )
400
+
401
+ with gr.Row():
402
+ submit_btn = gr.Button("Send", variant="primary")
403
+ clear_btn = gr.Button("Clear", variant="secondary")
404
+
405
+ with gr.Column(scale=1):
406
+ gr.Markdown("### βš™οΈ Configuration")
407
+
408
+ openai_key = gr.Textbox(
409
+ label="OpenAI API Key",
410
+ type="password",
411
+ placeholder="sk-..."
412
+ )
413
+
414
+ mcp_url = gr.Textbox(
415
+ label="MCP Server URL",
416
+ value="https://your-ngrok-url.ngrok.io",
417
+ placeholder="https://abc123.ngrok.io"
418
+ )
419
+
420
+ test_btn = gr.Button("Test Connection", variant="secondary")
421
+ connection_status = gr.Textbox(label="Connection Status", interactive=False)
422
+
423
+ gr.Markdown("### πŸ“‹ Example Queries")
424
+ gr.Markdown("""
425
+ - "Show me recent purchase orders"
426
+ - "Get purchase requisitions"
427
+ - "What's the latest tech news?"
428
+ - "Get news from BBC"
429
+ - "Show me business news from the US"
430
+ """)
431
+
432
+ # Event handlers
433
+ def respond(message, history, openai_key, mcp_url):
434
+ return chat_interface(message, history, openai_key, mcp_url)
435
+
436
+ submit_btn.click(
437
+ respond,
438
+ [msg, chatbot, openai_key, mcp_url],
439
+ [chatbot, msg]
440
+ )
441
+
442
+ msg.submit(
443
+ respond,
444
+ [msg, chatbot, openai_key, mcp_url],
445
+ [chatbot, msg]
446
+ )
447
+
448
+ clear_btn.click(lambda: ([], ""), outputs=[chatbot, msg])
449
+
450
+ test_btn.click(
451
+ test_connection,
452
+ [mcp_url],
453
+ [connection_status]
454
+ )
455
 
456
+ if __name__ == "__main__":
457
+ demo.launch()