om4r932 commited on
Commit
27054c5
·
1 Parent(s): 1208bbb

First version

Browse files
Files changed (3) hide show
  1. app.py +238 -0
  2. index.html +809 -0
  3. server.py +130 -0
app.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # /models
2
+ # /chat
3
+ # /get-tools
4
+
5
+ import asyncio
6
+ import json
7
+ import traceback
8
+ from typing import List, Optional, Literal, Any, Dict
9
+ from contextlib import AsyncExitStack
10
+ import uuid
11
+
12
+ from fastapi import FastAPI, HTTPException
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+ from pydantic import BaseModel
15
+ from fastapi.responses import FileResponse
16
+ from openai import APIConnectionError, OpenAI
17
+
18
+ from mcp import ClientSession, StdioServerParameters
19
+ from mcp.client.stdio import stdio_client
20
+
21
+ sessions = {}
22
+ unique_apikeys = []
23
+
24
+ class MCPClient:
25
+ def __init__(self):
26
+ self.session: Optional[ClientSession] = None
27
+ self.exit_stack = AsyncExitStack()
28
+ self.current_model = None
29
+ self.messages = []
30
+ self.openai: Optional[OpenAI] = None
31
+ self.api_key = None
32
+ self.tool_use: bool = True
33
+ self.models = None
34
+ self.tools = []
35
+
36
+ async def connect(self, api_key: str):
37
+ try:
38
+ self.openai = OpenAI(
39
+ base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
40
+ api_key=api_key
41
+ )
42
+ self.api_key = api_key
43
+ except APIConnectionError as e:
44
+ traceback.print_exception(e)
45
+ return False
46
+ except Exception as e:
47
+ traceback.print_exception(e)
48
+ return False
49
+
50
+ server_params = StdioServerParameters(command="uv", args=["run", "server.py"])
51
+ stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
52
+ self.stdio, self.write = stdio_transport
53
+ self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
54
+
55
+ await self.session.initialize()
56
+
57
+ response = await self.session.list_tools()
58
+ tools = response.tools
59
+ self.tools = [{
60
+ "type": "function",
61
+ "function": {
62
+ "name": tool.name,
63
+ "description": tool.description,
64
+ "parameters": tool.inputSchema
65
+ }
66
+ } for tool in tools]
67
+
68
+ def populate_model(self):
69
+ self.models = sorted([m.id for m in self.openai.models.list().data])
70
+
71
+ async def process_query(self, query: str) -> str:
72
+ """Process a query using Groq and available tools"""
73
+ self.messages.extend([
74
+ {
75
+ "role": "user",
76
+ "content": query
77
+ }
78
+ ])
79
+
80
+ response = self.openai.chat.completions.create(
81
+ model=self.current_model,
82
+ messages=self.messages,
83
+ tools=self.tools,
84
+ temperature=0
85
+ ) if self.tool_use else self.openai.chat.completions.create(
86
+ model=self.current_model,
87
+ messages=self.messages,
88
+ temperature=0.65
89
+ )
90
+
91
+ # Process response and handle tool calls
92
+ final_text = []
93
+
94
+ for choice in response.choices:
95
+ content = choice.message.content
96
+ tool_calls = choice.message.tool_calls
97
+ if content:
98
+ final_text.append(content)
99
+ break
100
+ if tool_calls:
101
+ assistant_message = {
102
+ "role": "assistant",
103
+ "tool_calls": tool_calls
104
+ }
105
+
106
+ if content: # Ajouter content seulement s'il existe
107
+ assistant_message["content"] = content
108
+
109
+ self.messages.append(assistant_message)
110
+
111
+ for tool in tool_calls:
112
+ tool_name = tool.function.name
113
+ tool_args = tool.function.arguments
114
+
115
+ result = await self.session.call_tool(tool_name, json.loads(tool_args))
116
+ print(f"[Calling tool {tool_name} with args {tool_args}]")
117
+
118
+ self.messages.append({
119
+ "role": "tool",
120
+ "tool_call_id": tool.id,
121
+ "content": str(result)
122
+ })
123
+
124
+ response2 = self.openai.chat.completions.create(
125
+ model=self.current_model,
126
+ messages=self.messages,
127
+ temperature=0.7
128
+ )
129
+
130
+ final_text.append(response2.choices[0].message.content)
131
+
132
+ self.messages.append({
133
+ "role": "assistant",
134
+ "content": response2.choices[0].message.content
135
+ })
136
+ return "\n".join(final_text)
137
+
138
+ app = FastAPI()
139
+ app.add_middleware(CORSMiddleware, allow_credentials=True, allow_headers=["*"], allow_methods=["*"], allow_origins=["*"])
140
+ mcp = MCPClient()
141
+
142
+ class InitRequest(BaseModel):
143
+ api_key: str
144
+
145
+ class InitResponse(BaseModel):
146
+ success: bool
147
+ session_id: str
148
+ models: Optional[list] = None
149
+ error: Optional[str] = None
150
+
151
+ class LogoutRequest(BaseModel):
152
+ session_id: str
153
+
154
+ def get_mcp_client(session_id: str) -> MCPClient|None:
155
+ """Get the MCPClient for a given session_id, or raise 404."""
156
+ client = sessions.get(session_id)
157
+ if client is None:
158
+ raise HTTPException(status_code=404, detail="Invalid session_id. Please re-initialize.")
159
+ return client
160
+
161
+ @app.get("/")
162
+ def root():
163
+ return FileResponse("index.html")
164
+
165
+ @app.post("/init", response_model=InitResponse)
166
+ async def init_server(req: InitRequest):
167
+ """
168
+ Initializes a new MCP client session. Returns a session_id.
169
+ """
170
+ api_key = req.api_key
171
+ session_id = str(uuid.uuid4())
172
+ mcp = MCPClient()
173
+
174
+ try:
175
+ ok = await mcp.connect(api_key)
176
+ if ok is False:
177
+ raise RuntimeError("Failed to connect to MCP or OpenAI with API key.")
178
+ mcp.populate_model()
179
+
180
+ sessions[session_id] = mcp
181
+ if api_key not in unique_apikeys:
182
+ unique_apikeys.append(api_key)
183
+ else:
184
+ raise Exception("Session with this API key already exists. We won't re-return you the session ID. Bye-bye Hacker !!")
185
+ return InitResponse(
186
+ session_id=session_id,
187
+ models=mcp.models,
188
+ error=None,
189
+ success=True
190
+ )
191
+ except Exception as e:
192
+ traceback.print_exception(e)
193
+ return InitResponse(
194
+ session_id="",
195
+ models=None,
196
+ error=str(e),
197
+ success=False
198
+ )
199
+
200
+ class ChatRequest(BaseModel):
201
+ session_id: str
202
+ query: str
203
+ tool_use: Optional[bool] = True
204
+ model: Optional[str] = "models/gemini-2.0-flash"
205
+
206
+ class ChatResponse(BaseModel):
207
+ output: str
208
+ error: Optional[str] = None
209
+
210
+ @app.post("/chat", response_model=ChatResponse)
211
+ async def chat(req: ChatRequest):
212
+ """
213
+ Handles chat requests for a given session.
214
+ """
215
+ try:
216
+ mcp = get_mcp_client(req.session_id)
217
+ mcp.tool_use = req.tool_use
218
+ if req.model in mcp.models:
219
+ mcp.current_model = req.model
220
+ else:
221
+ raise ValueError(f"Model not recognized: Not in the model list: {mcp.models}")
222
+ result = await mcp.process_query(req.query)
223
+ return ChatResponse(output=result)
224
+ except Exception as e:
225
+ traceback.print_exception(e)
226
+ return ChatResponse(output="", error=str(e))
227
+
228
+ @app.post("/logout")
229
+ async def logout(logout_req: LogoutRequest):
230
+ """Clean up session resources."""
231
+ mcp = sessions.pop(logout_req.session_id, None)
232
+ unique_apikeys.remove(mcp.api_key)
233
+ if mcp and hasattr(mcp.exit_stack, "aclose"):
234
+ try:
235
+ await mcp.exit_stack.aclose()
236
+ except RuntimeError:
237
+ pass
238
+ return {"success": True}
index.html ADDED
@@ -0,0 +1,809 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ChatBot OpenAI</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'Google Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ }
22
+
23
+ .container {
24
+ width: 100%;
25
+ max-width: 1200px;
26
+ height: 100vh;
27
+ background: white;
28
+ border-radius: 16px;
29
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
30
+ overflow: hidden;
31
+ display: flex;
32
+ flex-direction: column;
33
+ }
34
+
35
+ /* Login Page Styles */
36
+ .login-container {
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: center;
40
+ height: 100vh;
41
+ background: white;
42
+ }
43
+
44
+ .login-form {
45
+ background: white;
46
+ padding: 48px;
47
+ border-radius: 16px;
48
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1);
49
+ width: 100%;
50
+ max-width: 400px;
51
+ text-align: center;
52
+ }
53
+
54
+ .login-form h1 {
55
+ color: #1a73e8;
56
+ font-size: 32px;
57
+ font-weight: 400;
58
+ margin-bottom: 8px;
59
+ }
60
+
61
+ .login-form p {
62
+ color: #5f6368;
63
+ font-size: 16px;
64
+ margin-bottom: 32px;
65
+ }
66
+
67
+ .input-group {
68
+ margin-bottom: 24px;
69
+ text-align: left;
70
+ }
71
+
72
+ .input-group label {
73
+ display: block;
74
+ color: #202124;
75
+ font-size: 14px;
76
+ font-weight: 500;
77
+ margin-bottom: 8px;
78
+ }
79
+
80
+ .input-group input {
81
+ width: 100%;
82
+ padding: 16px;
83
+ border: 2px solid #dadce0;
84
+ border-radius: 8px;
85
+ font-size: 16px;
86
+ transition: all 0.2s ease;
87
+ background: #fafafa;
88
+ }
89
+
90
+ .input-group input:focus {
91
+ outline: none;
92
+ border-color: #1a73e8;
93
+ background: white;
94
+ box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.1);
95
+ }
96
+
97
+ .btn-primary {
98
+ background: #1a73e8;
99
+ color: white;
100
+ border: none;
101
+ padding: 16px 32px;
102
+ border-radius: 8px;
103
+ font-size: 16px;
104
+ font-weight: 500;
105
+ cursor: pointer;
106
+ transition: all 0.2s ease;
107
+ width: 100%;
108
+ }
109
+
110
+ .btn-primary:hover {
111
+ background: #1557b0;
112
+ box-shadow: 0 2px 8px rgba(26, 115, 232, 0.3);
113
+ }
114
+
115
+ /* Chat Interface Styles */
116
+ .chat-container {
117
+ display: none;
118
+ height: 100vh;
119
+ flex-direction: column;
120
+ }
121
+
122
+ .chat-header {
123
+ background: #1a73e8;
124
+ color: white;
125
+ padding: 16px 24px;
126
+ display: flex;
127
+ align-items: center;
128
+ justify-content: space-between;
129
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
130
+ }
131
+
132
+ .header-left {
133
+ display: flex;
134
+ align-items: center;
135
+ gap: 16px;
136
+ }
137
+
138
+ .header-title {
139
+ font-size: 20px;
140
+ font-weight: 500;
141
+ }
142
+
143
+ .header-controls {
144
+ display: flex;
145
+ align-items: center;
146
+ gap: 16px;
147
+ }
148
+
149
+ .model-select {
150
+ background: rgba(255, 255, 255, 0.1);
151
+ border: 1px solid rgba(255, 255, 255, 0.2);
152
+ color: white;
153
+ padding: 8px 12px;
154
+ border-radius: 8px;
155
+ font-size: 14px;
156
+ }
157
+
158
+ .model-select option {
159
+ background: #1a73e8;
160
+ color: white;
161
+ }
162
+
163
+ .toggle-container {
164
+ display: flex;
165
+ align-items: center;
166
+ gap: 8px;
167
+ }
168
+
169
+ .toggle {
170
+ position: relative;
171
+ width: 48px;
172
+ height: 24px;
173
+ background: rgba(255, 255, 255, 0.2);
174
+ border-radius: 12px;
175
+ cursor: pointer;
176
+ transition: all 0.3s ease;
177
+ }
178
+
179
+ .toggle.active {
180
+ background: #34a853;
181
+ }
182
+
183
+ .toggle-slider {
184
+ position: absolute;
185
+ top: 2px;
186
+ left: 2px;
187
+ width: 20px;
188
+ height: 20px;
189
+ background: white;
190
+ border-radius: 50%;
191
+ transition: all 0.3s ease;
192
+ }
193
+
194
+ .toggle.active .toggle-slider {
195
+ transform: translateX(24px);
196
+ }
197
+
198
+ .logout-btn {
199
+ background: rgba(255, 255, 255, 0.1);
200
+ border: 1px solid rgba(255, 255, 255, 0.2);
201
+ color: white;
202
+ padding: 8px 16px;
203
+ border-radius: 8px;
204
+ cursor: pointer;
205
+ font-size: 14px;
206
+ transition: all 0.2s ease;
207
+ }
208
+
209
+ .logout-btn:hover {
210
+ background: rgba(255, 255, 255, 0.2);
211
+ }
212
+
213
+ .chat-messages {
214
+ flex: 1;
215
+ padding: 24px;
216
+ overflow-y: auto;
217
+ background: #f8f9fa;
218
+ display: flex;
219
+ flex-direction: column;
220
+ gap: 16px;
221
+ }
222
+
223
+ .message {
224
+ display: flex;
225
+ gap: 12px;
226
+ max-width: 80%;
227
+ animation: slideIn 0.3s ease;
228
+ }
229
+
230
+ .message.user {
231
+ align-self: flex-end;
232
+ flex-direction: row-reverse;
233
+ }
234
+
235
+ .message-avatar {
236
+ width: 40px;
237
+ height: 40px;
238
+ border-radius: 50%;
239
+ display: flex;
240
+ align-items: center;
241
+ justify-content: center;
242
+ font-weight: 500;
243
+ font-size: 14px;
244
+ flex-shrink: 0;
245
+ }
246
+
247
+ .message.user .message-avatar {
248
+ background: #1a73e8;
249
+ color: white;
250
+ }
251
+
252
+ .message.assistant .message-avatar {
253
+ background: #34a853;
254
+ color: white;
255
+ }
256
+
257
+ .message-content {
258
+ background: white;
259
+ padding: 16px;
260
+ border-radius: 16px;
261
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
262
+ line-height: 1.5;
263
+ word-wrap: break-word;
264
+ }
265
+
266
+ .message.user .message-content {
267
+ background: #1a73e8;
268
+ color: white;
269
+ }
270
+
271
+ .chat-input-container {
272
+ padding: 24px;
273
+ background: white;
274
+ border-top: 1px solid #e0e0e0;
275
+ }
276
+
277
+ .chat-input-wrapper {
278
+ display: flex;
279
+ gap: 12px;
280
+ align-items: flex-end;
281
+ }
282
+
283
+ .chat-input {
284
+ flex: 1;
285
+ padding: 16px;
286
+ border: 2px solid #dadce0;
287
+ border-radius: 24px;
288
+ font-size: 16px;
289
+ resize: none;
290
+ min-height: 24px;
291
+ max-height: 120px;
292
+ transition: all 0.2s ease;
293
+ }
294
+
295
+ .chat-input:focus {
296
+ outline: none;
297
+ border-color: #1a73e8;
298
+ box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.1);
299
+ }
300
+
301
+ .send-btn {
302
+ background: #1a73e8;
303
+ color: white;
304
+ border: none;
305
+ width: 48px;
306
+ height: 48px;
307
+ border-radius: 50%;
308
+ cursor: pointer;
309
+ display: flex;
310
+ align-items: center;
311
+ justify-content: center;
312
+ transition: all 0.2s ease;
313
+ flex-shrink: 0;
314
+ }
315
+
316
+ .send-btn:hover {
317
+ background: #1557b0;
318
+ transform: scale(1.05);
319
+ }
320
+
321
+ .send-btn:disabled {
322
+ background: #dadce0;
323
+ cursor: not-allowed;
324
+ transform: none;
325
+ }
326
+
327
+ .typing-indicator {
328
+ display: none;
329
+ align-items: center;
330
+ gap: 12px;
331
+ max-width: 80%;
332
+ animation: slideIn 0.3s ease;
333
+ }
334
+
335
+ .typing-dots {
336
+ background: white;
337
+ padding: 16px;
338
+ border-radius: 16px;
339
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
340
+ display: flex;
341
+ gap: 4px;
342
+ }
343
+
344
+ .typing-dot {
345
+ width: 8px;
346
+ height: 8px;
347
+ border-radius: 50%;
348
+ background: #dadce0;
349
+ animation: typing 1.4s infinite ease-in-out;
350
+ }
351
+
352
+ .typing-dot:nth-child(2) {
353
+ animation-delay: 0.2s;
354
+ }
355
+
356
+ .typing-dot:nth-child(3) {
357
+ animation-delay: 0.4s;
358
+ }
359
+
360
+ @keyframes slideIn {
361
+ from {
362
+ opacity: 0;
363
+ transform: translateY(20px);
364
+ }
365
+ to {
366
+ opacity: 1;
367
+ transform: translateY(0);
368
+ }
369
+ }
370
+
371
+ @keyframes typing {
372
+ 0%, 60%, 100% {
373
+ transform: scale(1);
374
+ background: #dadce0;
375
+ }
376
+ 30% {
377
+ transform: scale(1.2);
378
+ background: #1a73e8;
379
+ }
380
+ }
381
+
382
+ .error-message {
383
+ background: #fce8e6;
384
+ color: #d93025;
385
+ padding: 12px 16px;
386
+ border-radius: 8px;
387
+ margin-bottom: 16px;
388
+ border-left: 4px solid #d93025;
389
+ }
390
+
391
+ @media (max-width: 768px) {
392
+ .container {
393
+ border-radius: 0;
394
+ height: 100vh;
395
+ }
396
+
397
+ .login-form {
398
+ padding: 32px 24px;
399
+ margin: 16px;
400
+ }
401
+
402
+ .header-controls {
403
+ flex-direction: column;
404
+ gap: 8px;
405
+ align-items: flex-end;
406
+ }
407
+
408
+ .message {
409
+ max-width: 90%;
410
+ }
411
+
412
+ .chat-input-container {
413
+ padding: 16px;
414
+ }
415
+ }
416
+ </style>
417
+ </head>
418
+ <body>
419
+ <div class="container">
420
+ <!-- Login Page -->
421
+ <div id="loginPage" class="login-container">
422
+ <div class="login-form">
423
+ <h1>ChatBot OpenAI</h1>
424
+ <p>Connectez-vous avec votre clé API</p>
425
+ <div id="loginError"></div>
426
+ <form id="loginForm">
427
+ <div class="input-group">
428
+ <label for="apiKey">Clé API</label>
429
+ <input type="password" id="apiKey" placeholder="Votre clé API..." required>
430
+ </div>
431
+ <button type="submit" class="btn-primary">Se connecter</button>
432
+ </form>
433
+ </div>
434
+ </div>
435
+
436
+ <!-- Chat Interface -->
437
+ <div id="chatPage" class="chat-container">
438
+ <div class="chat-header">
439
+ <div class="header-left">
440
+ <div class="header-title">ChatBot OpenAI</div>
441
+ </div>
442
+ <div class="header-controls">
443
+ <select id="modelSelect" class="model-select">
444
+ <!-- Models will be populated dynamically -->
445
+ </select>
446
+ <div class="toggle-container">
447
+ <span style="font-size: 14px;">Tools</span>
448
+ <div id="toolToggle" class="toggle active">
449
+ <div class="toggle-slider"></div>
450
+ </div>
451
+ </div>
452
+ <button id="logoutBtn" class="logout-btn">Déconnexion</button>
453
+ </div>
454
+ </div>
455
+
456
+ <div id="chatMessages" class="chat-messages">
457
+ <div class="message assistant">
458
+ <div class="message-avatar">AI</div>
459
+ <div class="message-content">
460
+ Bonjour ! Je suis votre assistant IA. Comment puis-je vous aider aujourd'hui ?
461
+ </div>
462
+ </div>
463
+ </div>
464
+
465
+ <div class="typing-indicator" id="typingIndicator">
466
+ <div class="message-avatar" style="background: #34a853; color: white;">AI</div>
467
+ <div class="typing-dots">
468
+ <div class="typing-dot"></div>
469
+ <div class="typing-dot"></div>
470
+ <div class="typing-dot"></div>
471
+ </div>
472
+ </div>
473
+
474
+ <div class="chat-input-container">
475
+ <div class="chat-input-wrapper">
476
+ <textarea id="chatInput" class="chat-input" placeholder="Tapez votre message..." rows="1"></textarea>
477
+ <button id="sendBtn" class="send-btn">
478
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
479
+ <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
480
+ </svg>
481
+ </button>
482
+ </div>
483
+ </div>
484
+ </div>
485
+ </div>
486
+
487
+ <script>
488
+ class ChatBot {
489
+ constructor() {
490
+ this.sessionId = '';
491
+ this.currentModel = 'llama-3.3-70b-versatile';
492
+ this.toolsEnabled = true;
493
+ this.isTyping = false;
494
+ this.availableModels = [];
495
+ this.baseUrl = window.location.origin; // Utilise l'URL de votre serveur FastAPI
496
+
497
+ this.initializeElements();
498
+ this.bindEvents();
499
+ this.loadFromStorage();
500
+ }
501
+
502
+ initializeElements() {
503
+ // Login elements
504
+ this.loginPage = document.getElementById('loginPage');
505
+ this.loginForm = document.getElementById('loginForm');
506
+ this.apiKeyInput = document.getElementById('apiKey');
507
+ this.loginError = document.getElementById('loginError');
508
+
509
+ // Chat elements
510
+ this.chatPage = document.getElementById('chatPage');
511
+ this.chatMessages = document.getElementById('chatMessages');
512
+ this.chatInput = document.getElementById('chatInput');
513
+ this.sendBtn = document.getElementById('sendBtn');
514
+ this.modelSelect = document.getElementById('modelSelect');
515
+ this.toolToggle = document.getElementById('toolToggle');
516
+ this.logoutBtn = document.getElementById('logoutBtn');
517
+ this.typingIndicator = document.getElementById('typingIndicator');
518
+ }
519
+
520
+ bindEvents() {
521
+ // Login events
522
+ this.loginForm.addEventListener('submit', (e) => this.handleLogin(e));
523
+
524
+ // Chat events
525
+ this.sendBtn.addEventListener('click', () => this.sendMessage());
526
+ this.chatInput.addEventListener('keypress', (e) => {
527
+ if (e.key === 'Enter' && !e.shiftKey) {
528
+ e.preventDefault();
529
+ this.sendMessage();
530
+ }
531
+ });
532
+
533
+ // Auto-resize textarea
534
+ this.chatInput.addEventListener('input', () => {
535
+ this.chatInput.style.height = 'auto';
536
+ this.chatInput.style.height = Math.min(this.chatInput.scrollHeight, 120) + 'px';
537
+ });
538
+
539
+ // Header controls
540
+ this.modelSelect.addEventListener('change', (e) => {
541
+ this.currentModel = e.target.value;
542
+ this.saveToStorage();
543
+ });
544
+
545
+ this.toolToggle.addEventListener('click', () => {
546
+ this.toolsEnabled = !this.toolsEnabled;
547
+ this.toolToggle.classList.toggle('active', this.toolsEnabled);
548
+ this.saveToStorage();
549
+ });
550
+
551
+ this.logoutBtn.addEventListener('click', () => this.logout());
552
+ }
553
+
554
+ async handleLogin(e) {
555
+ e.preventDefault();
556
+ const apiKey = this.apiKeyInput.value.trim();
557
+
558
+ if (!apiKey) {
559
+ this.showLoginError('Veuillez entrer une clé API valide');
560
+ return;
561
+ }
562
+
563
+ try {
564
+ const response = await fetch(`${this.baseUrl}/init`, {
565
+ method: 'POST',
566
+ headers: {
567
+ 'Content-Type': 'application/json'
568
+ },
569
+ body: JSON.stringify({
570
+ api_key: apiKey
571
+ })
572
+ });
573
+
574
+ const data = await response.json();
575
+
576
+ if (data.success) {
577
+ this.sessionId = data.session_id;
578
+ this.availableModels = data.models || [];
579
+ this.populateModelSelect();
580
+ this.saveToStorage();
581
+ this.showChatInterface();
582
+ this.loginError.innerHTML = '';
583
+ } else {
584
+ this.showLoginError(data.error || 'Erreur de connexion');
585
+ }
586
+ } catch (error) {
587
+ this.showLoginError('Erreur de connexion au serveur. Veuillez réessayer.');
588
+ console.error('Login error:', error);
589
+ }
590
+ }
591
+
592
+ populateModelSelect() {
593
+ this.modelSelect.innerHTML = '';
594
+ this.availableModels.forEach(model => {
595
+ const option = document.createElement('option');
596
+ option.value = model;
597
+ option.textContent = model;
598
+ this.modelSelect.appendChild(option);
599
+ });
600
+
601
+ // Set default model if available
602
+ if (this.availableModels.includes(this.currentModel)) {
603
+ this.modelSelect.value = this.currentModel;
604
+ } else if (this.availableModels.length > 0) {
605
+ this.currentModel = this.availableModels[0];
606
+ this.modelSelect.value = this.currentModel;
607
+ }
608
+ }
609
+
610
+ showLoginError(message) {
611
+ this.loginError.innerHTML = `<div class="error-message">${message}</div>`;
612
+ }
613
+
614
+ showChatInterface() {
615
+ this.loginPage.style.display = 'none';
616
+ this.chatPage.style.display = 'flex';
617
+ this.chatInput.focus();
618
+ }
619
+
620
+ async logout() {
621
+ try {
622
+ if (this.sessionId) {
623
+ await fetch(`${this.baseUrl}/logout`, {
624
+ method: 'POST',
625
+ headers: {
626
+ 'Content-Type': 'application/json'
627
+ },
628
+ body: JSON.stringify({
629
+ session_id: this.sessionId
630
+ })
631
+ });
632
+ }
633
+ } catch (error) {
634
+ console.error('Logout error:', error);
635
+ }
636
+
637
+ this.sessionId = '';
638
+ this.availableModels = [];
639
+ localStorage.removeItem('chatbot_data');
640
+ this.chatPage.style.display = 'none';
641
+ this.loginPage.style.display = 'flex';
642
+ this.apiKeyInput.value = '';
643
+ this.clearMessages();
644
+ }
645
+
646
+ async sendMessage() {
647
+ const message = this.chatInput.value.trim();
648
+ if (!message || this.isTyping || !this.sessionId) return;
649
+
650
+ // Add user message
651
+ this.addMessage('user', message);
652
+ this.chatInput.value = '';
653
+ this.chatInput.style.height = 'auto';
654
+
655
+ // Show typing indicator
656
+ this.showTyping(true);
657
+
658
+ try {
659
+ const response = await fetch(`${this.baseUrl}/chat`, {
660
+ method: 'POST',
661
+ headers: {
662
+ 'Content-Type': 'application/json'
663
+ },
664
+ body: JSON.stringify({
665
+ session_id: this.sessionId,
666
+ query: message,
667
+ tool_use: this.toolsEnabled,
668
+ model: this.currentModel
669
+ })
670
+ });
671
+
672
+ const data = await response.json();
673
+ this.showTyping(false);
674
+
675
+ if (data.error) {
676
+ this.addMessage('assistant', `Erreur: ${data.error}`);
677
+ } else {
678
+ this.addMessage('assistant', data.output);
679
+ }
680
+ } catch (error) {
681
+ this.showTyping(false);
682
+ this.addMessage('assistant', `Erreur de connexion: ${error.message}`);
683
+ console.error('Chat error:', error);
684
+ }
685
+
686
+ this.saveToStorage();
687
+ }
688
+
689
+ addMessage(role, content) {
690
+ const messageDiv = document.createElement('div');
691
+ messageDiv.className = `message ${role}`;
692
+
693
+ const avatar = document.createElement('div');
694
+ avatar.className = 'message-avatar';
695
+ avatar.textContent = role === 'user' ? 'U' : 'AI';
696
+
697
+ const messageContent = document.createElement('div');
698
+ messageContent.className = 'message-content';
699
+ messageContent.textContent = content;
700
+
701
+ messageDiv.appendChild(avatar);
702
+ messageDiv.appendChild(messageContent);
703
+
704
+ // Vérifier si typingIndicator est bien un enfant de chatMessages
705
+ if (this.typingIndicator && this.typingIndicator.parentNode === this.chatMessages) {
706
+ this.chatMessages.insertBefore(messageDiv, this.typingIndicator);
707
+ } else {
708
+ // Si typingIndicator n'est pas présent ou pas un enfant, ajouter à la fin
709
+ this.chatMessages.appendChild(messageDiv);
710
+ }
711
+
712
+ this.scrollToBottom();
713
+ }
714
+
715
+ showTyping(show) {
716
+ this.isTyping = show;
717
+ this.typingIndicator.style.display = show ? 'flex' : 'none';
718
+ this.sendBtn.disabled = show;
719
+ this.scrollToBottom();
720
+ }
721
+
722
+ scrollToBottom() {
723
+ setTimeout(() => {
724
+ this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
725
+ }, 100);
726
+ }
727
+
728
+ clearMessages() {
729
+ const messages = this.chatMessages.querySelectorAll('.message');
730
+ messages.forEach((msg, index) => {
731
+ if (index > 0) { // Keep welcome message
732
+ msg.remove();
733
+ }
734
+ });
735
+ }
736
+
737
+ saveToStorage() {
738
+ const data = {
739
+ sessionId: this.sessionId,
740
+ currentModel: this.currentModel,
741
+ toolsEnabled: this.toolsEnabled,
742
+ availableModels: this.availableModels
743
+ };
744
+ localStorage.setItem('chatbot_data', JSON.stringify(data));
745
+ }
746
+
747
+ loadFromStorage() {
748
+ const saved = localStorage.getItem('chatbot_data');
749
+ if (saved) {
750
+ try {
751
+ const data = JSON.parse(saved);
752
+ if (data.sessionId) {
753
+ this.sessionId = data.sessionId;
754
+ this.currentModel = data.currentModel || 'llama-3.3-70b-versatile';
755
+ this.toolsEnabled = data.toolsEnabled !== undefined ? data.toolsEnabled : true;
756
+ this.availableModels = data.availableModels || [];
757
+
758
+ // Update UI
759
+ this.populateModelSelect();
760
+ this.toolToggle.classList.toggle('active', this.toolsEnabled);
761
+
762
+ // Verify session is still valid
763
+ this.verifySession();
764
+ }
765
+ } catch (error) {
766
+ console.error('Error loading from storage:', error);
767
+ localStorage.removeItem('chatbot_data');
768
+ }
769
+ }
770
+ }
771
+
772
+ async verifySession() {
773
+ try {
774
+ // Test if session is still valid by making a simple request
775
+ const response = await fetch(`${this.baseUrl}/chat`, {
776
+ method: 'POST',
777
+ headers: {
778
+ 'Content-Type': 'application/json'
779
+ },
780
+ body: JSON.stringify({
781
+ session_id: this.sessionId,
782
+ query: 'test',
783
+ tool_use: false,
784
+ model: this.currentModel
785
+ })
786
+ });
787
+
788
+ if (response.ok) {
789
+ this.showChatInterface();
790
+ } else {
791
+ // Session invalid, clear storage and show login
792
+ localStorage.removeItem('chatbot_data');
793
+ this.sessionId = '';
794
+ }
795
+ } catch (error) {
796
+ // Connection error, clear session
797
+ localStorage.removeItem('chatbot_data');
798
+ this.sessionId = '';
799
+ }
800
+ }
801
+ }
802
+
803
+ // Initialize the chatbot when the page loads
804
+ document.addEventListener('DOMContentLoaded', () => {
805
+ new ChatBot();
806
+ });
807
+ </script>
808
+ </body>
809
+ </html>
server.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional
2
+ import requests
3
+ import json
4
+ from mcp.server.fastmcp import FastMCP
5
+
6
+ mcp = FastMCP("organizedprogrammers-mcp-server")
7
+
8
+ @mcp.tool()
9
+ def search_arxiv_papers(keyword: str, limit: int = 5) -> str:
10
+ """
11
+ Search papers from arXiv database with specified keywords [optional: a limit of papers the user wants]
12
+ Args: keyword: string, [optional: limit: integer, set limit to 5 if not specified]
13
+ """
14
+ response = requests.post("https://om4r932-arxiv.hf.space/search", headers={
15
+ "Content-Type": "application/json"
16
+ }, data=json.dumps({
17
+ "keyword": keyword,
18
+ "limit": limit
19
+ }), verify=False)
20
+
21
+ if response.status_code != 200:
22
+ return "Unable to find papers: error on post()"
23
+
24
+ responseJson = response.json()
25
+
26
+ if responseJson.get("error") or not isinstance(responseJson['message'], dict):
27
+ return f"Unable to find papers: error on API -> {responseJson['message']}"
28
+
29
+ if len(responseJson["message"].keys()) == 0:
30
+ return "No papers has been found"
31
+
32
+ return "\n".join([f"arXiv n°{paper_id} - {paper_meta['title']} by {paper_meta['authors']} : {paper_meta['abstract']}" for paper_id, paper_meta in responseJson['message'].items()])
33
+
34
+ @mcp.tool()
35
+ def locate_3gpp_document(doc_id: str) -> str:
36
+ """
37
+ Find 3GPP document location with the document's ID
38
+ Args: doc_id: string
39
+ """
40
+ response = requests.post("https://organizedprogrammers-3gppdocfinder.hf.space/find", headers={
41
+ "Content-Type": "application/json"
42
+ }, data=json.dumps({
43
+ "doc_id": doc_id
44
+ }), verify=False)
45
+
46
+ if response.status_code != 200:
47
+ return f"Unable to find document: {response.status_code} - {response.content}"
48
+
49
+ responseJson = response.json()
50
+
51
+ if responseJson.get("detail"):
52
+ return responseJson['detail']
53
+
54
+ return f"Document ID {responseJson['doc_id']} version {responseJson['version']} is downloadable via this link: {responseJson['url']}.\n{responseJson['scope']}"
55
+
56
+ @mcp.tool()
57
+ def locate_multiple_3gpp_documents(doc_ids: List[str]) -> str:
58
+ """
59
+ Find 3GPP document location with the document's ID
60
+ Args: doc_id: string
61
+ """
62
+ response = requests.post("https://organizedprogrammers-3gppdocfinder.hf.space/batch", headers={
63
+ "Content-Type": "application/json"
64
+ }, data=json.dumps({
65
+ "doc_ids": doc_ids
66
+ }), verify=False)
67
+
68
+ if response.status_code != 200:
69
+ return f"Unable to find document: {response.status_code} - {response.content}"
70
+
71
+ responseJson = response.json()
72
+
73
+ if responseJson.get("detail"):
74
+ return responseJson['detail']
75
+
76
+ return "\n".join([f"The document {doc_id} is downloadable via this link: {url}" for doc_id, url in responseJson['results']] + [f"We can't find document {doc_id}" for doc_id in responseJson['missing']])
77
+
78
+ @mcp.tool()
79
+ def locate_etsi_specifications(doc_id: str) -> str:
80
+ """
81
+ Find ETSI document location with the document's ID (starts with SET and SCP)
82
+ Args: doc_id: string
83
+ """
84
+ response = requests.post("https://organizedprogrammers-etsidocfinder.hf.space/find", headers={
85
+ "Content-Type": "application/json"
86
+ }, data=json.dumps({
87
+ "doc_id": doc_id
88
+ }), verify=False)
89
+
90
+ if response.status_code != 200:
91
+ return f"Unable to find document: {response.status_code} - {response.content}"
92
+
93
+ responseJson = response.json()
94
+
95
+ if responseJson.get("detail"):
96
+ return responseJson['detail']
97
+
98
+ return f"Document ID {responseJson['doc_id']} is downloadable via this link: {responseJson['url']}"
99
+
100
+ @mcp.tool()
101
+ def search_3gpp_specification(keywords: str, threshold: int, release: Optional[str] = "", working_group: Optional[str] = "", spec_type: Optional[str] = "") -> str:
102
+ """
103
+ Search 3GPP specifications with specified keywords and filters using BM25
104
+ Args: keywords: string, threshold: integer 0-100 [default 60], release: optional filter, string [only the number Rel-19 -> '19'], working_group: optional filter, string [options: C1,C2,...,C6,CP or S1,S2,...,S6,SP], spec_type: optional filter, string [either TS (Technical Specification) or TR (Technical Report)]
105
+ For each non-used optional filters, leave a empty string
106
+ """
107
+ body = {"keywords": keywords, "threshold": threshold}
108
+ if release:
109
+ body['release'] = release
110
+ if working_group:
111
+ body['working_group'] = working_group
112
+ if spec_type:
113
+ body['spec_type'] = spec_type
114
+
115
+ response = requests.post("https://organizedprogrammers-3gppdocfinder.hf.space/search-spec/experimental", headers={
116
+ "Content-Type": "application/json"
117
+ }, data=json.dumps(body), verify=False)
118
+
119
+ if response.status_code != 200:
120
+ return f"Unable to find document: {response.status_code} - {response.content}"
121
+
122
+ responseJson = response.json()
123
+
124
+ if responseJson.get("detail"):
125
+ return responseJson['detail']
126
+
127
+ return "\n--\n".join([f"3GPP {spec['type']} {spec['id']} version {spec['version']} - {spec['title']} is downloadable via this link: {spec['url']}\n{spec['scope']}" for spec in responseJson['results']])
128
+
129
+ if __name__ == "__main__":
130
+ mcp.run(transport="stdio")