Omar ID EL MOUMEN
commited on
Commit
·
2df36ec
1
Parent(s):
824bf0c
First version
Browse files- Dockerfile +13 -0
- app.py +232 -0
- index.html +39 -0
- requirements.txt +6 -0
- server.py +153 -0
- static/script.js +178 -0
- static/style.css +170 -0
Dockerfile
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.10.6
|
2 |
+
|
3 |
+
RUN useradd -m -u 1000 user
|
4 |
+
USER user
|
5 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
6 |
+
|
7 |
+
WORKDIR /app
|
8 |
+
|
9 |
+
COPY --chown=user ./requirements.txt requirements.txt
|
10 |
+
RUN pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org --no-cache-dir --upgrade -r requirements.txt
|
11 |
+
|
12 |
+
COPY --chown=user . /app
|
13 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
app.py
ADDED
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# GET /models
|
2 |
+
# GET /tools
|
3 |
+
# POST /chat -> Groq -> response
|
4 |
+
|
5 |
+
import asyncio
|
6 |
+
import json
|
7 |
+
import traceback
|
8 |
+
from typing import List, Optional
|
9 |
+
from contextlib import AsyncExitStack
|
10 |
+
import uuid
|
11 |
+
|
12 |
+
from fastapi.staticfiles import StaticFiles
|
13 |
+
from pydantic import BaseModel
|
14 |
+
from fastapi import FastAPI, Request, HTTPException, Depends
|
15 |
+
from fastapi.middleware.cors import CORSMiddleware
|
16 |
+
from fastapi.responses import FileResponse, JSONResponse
|
17 |
+
|
18 |
+
|
19 |
+
from mcp import ClientSession, StdioServerParameters
|
20 |
+
from mcp.client.stdio import stdio_client
|
21 |
+
|
22 |
+
from groq import Groq, APIConnectionError
|
23 |
+
from dotenv import load_dotenv
|
24 |
+
import os
|
25 |
+
import httpx
|
26 |
+
|
27 |
+
sessions = {}
|
28 |
+
unique_apikeys = []
|
29 |
+
|
30 |
+
class MCPClient:
|
31 |
+
def __init__(self):
|
32 |
+
self.session: Optional[ClientSession] = None
|
33 |
+
self.exit_stack = AsyncExitStack()
|
34 |
+
self.current_model = None
|
35 |
+
self.groq = None
|
36 |
+
self.api_key = None
|
37 |
+
self.messages = [{
|
38 |
+
"role": "system",
|
39 |
+
"content": "You are a helpful assistant that have access to different tools via MCP. Make complete answers."
|
40 |
+
}]
|
41 |
+
self.tool_use = True
|
42 |
+
self.models = None
|
43 |
+
self.tools = []
|
44 |
+
|
45 |
+
async def connect(self, api_key: str):
|
46 |
+
try:
|
47 |
+
self.groq = Groq(api_key=api_key, http_client=httpx.Client(verify=False, timeout=30))
|
48 |
+
self.api_key = api_key
|
49 |
+
except APIConnectionError as e:
|
50 |
+
traceback.print_exception(e)
|
51 |
+
return False
|
52 |
+
except Exception as e:
|
53 |
+
traceback.print_exception(e)
|
54 |
+
return False
|
55 |
+
server_params = StdioServerParameters(command="uv", args=["run", "server.py"])
|
56 |
+
|
57 |
+
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
|
58 |
+
self.stdio, self.write = stdio_transport
|
59 |
+
self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
|
60 |
+
|
61 |
+
await self.session.initialize()
|
62 |
+
|
63 |
+
response = await self.session.list_tools()
|
64 |
+
tools = response.tools
|
65 |
+
print("\nConnected to server with tools:", [tool.name for tool in tools])
|
66 |
+
self.tools = [{"type": "function", "function": {
|
67 |
+
"name": tool.name,
|
68 |
+
"description": tool.description,
|
69 |
+
"parameters": tool.inputSchema
|
70 |
+
}} for tool in tools]
|
71 |
+
|
72 |
+
def populate_model(self):
|
73 |
+
self.models = sorted([m.id for m in self.groq.models.list().data])
|
74 |
+
|
75 |
+
async def process_query(self, query: str) -> str:
|
76 |
+
"""Process a query using Groq and available tools"""
|
77 |
+
self.messages.extend([
|
78 |
+
{
|
79 |
+
"role": "user",
|
80 |
+
"content": query
|
81 |
+
}
|
82 |
+
])
|
83 |
+
|
84 |
+
response = self.groq.chat.completions.create(
|
85 |
+
model=self.current_model,
|
86 |
+
messages=self.messages,
|
87 |
+
tools=self.tools,
|
88 |
+
temperature=0
|
89 |
+
) if self.tool_use else self.groq.chat.completions.create(
|
90 |
+
model=self.current_model,
|
91 |
+
messages=self.messages,
|
92 |
+
temperature=0.7
|
93 |
+
)
|
94 |
+
|
95 |
+
# Process response and handle tool calls
|
96 |
+
final_text = []
|
97 |
+
|
98 |
+
for choice in response.choices:
|
99 |
+
content = choice.message.content
|
100 |
+
tool_calls = choice.message.tool_calls
|
101 |
+
if content:
|
102 |
+
final_text.append(content)
|
103 |
+
if tool_calls:
|
104 |
+
print(tool_calls)
|
105 |
+
for tool in tool_calls:
|
106 |
+
tool_name = tool.function.name
|
107 |
+
tool_args = tool.function.arguments
|
108 |
+
|
109 |
+
result = await self.session.call_tool(tool_name, json.loads(tool_args))
|
110 |
+
print(f"[Calling tool {tool_name} with args {tool_args}]")
|
111 |
+
|
112 |
+
if content is not None:
|
113 |
+
self.messages.append({
|
114 |
+
"role": "assistant",
|
115 |
+
"content": content
|
116 |
+
})
|
117 |
+
self.messages.append({
|
118 |
+
"role": "tool",
|
119 |
+
"tool_call_id": tool.id,
|
120 |
+
"content": str(result.content)
|
121 |
+
})
|
122 |
+
|
123 |
+
print(result.content[0].text)
|
124 |
+
|
125 |
+
response = self.groq.chat.completions.create(
|
126 |
+
model=self.current_model,
|
127 |
+
messages=self.messages,
|
128 |
+
temperature=0.7
|
129 |
+
)
|
130 |
+
|
131 |
+
final_text.append(response.choices[0].message.content)
|
132 |
+
return "\n".join(final_text)
|
133 |
+
|
134 |
+
app = FastAPI()
|
135 |
+
app.add_middleware(CORSMiddleware, allow_credentials=True, allow_headers=["*"], allow_methods=["*"], allow_origins=["*"])
|
136 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
137 |
+
mcp = MCPClient()
|
138 |
+
|
139 |
+
class InitRequest(BaseModel):
|
140 |
+
api_key: str
|
141 |
+
|
142 |
+
class InitResponse(BaseModel):
|
143 |
+
success: bool
|
144 |
+
session_id: str
|
145 |
+
models: Optional[list] = None
|
146 |
+
error: Optional[str] = None
|
147 |
+
|
148 |
+
class LogoutRequest(BaseModel):
|
149 |
+
session_id: str
|
150 |
+
|
151 |
+
def get_mcp_client(session_id: str) -> MCPClient|None:
|
152 |
+
"""Get the MCPClient for a given session_id, or raise 404."""
|
153 |
+
client = sessions.get(session_id)
|
154 |
+
if client is None:
|
155 |
+
raise HTTPException(status_code=404, detail="Invalid session_id. Please re-initialize.")
|
156 |
+
return client
|
157 |
+
|
158 |
+
@app.get("/")
|
159 |
+
def root():
|
160 |
+
return FileResponse("index.html")
|
161 |
+
|
162 |
+
@app.post("/init", response_model=InitResponse)
|
163 |
+
async def init_server(req: InitRequest):
|
164 |
+
"""
|
165 |
+
Initializes a new MCP client session. Returns a session_id.
|
166 |
+
"""
|
167 |
+
api_key = req.api_key
|
168 |
+
session_id = str(uuid.uuid4())
|
169 |
+
mcp = MCPClient()
|
170 |
+
|
171 |
+
try:
|
172 |
+
ok = await mcp.connect(api_key)
|
173 |
+
if ok is False:
|
174 |
+
raise RuntimeError("Failed to connect to MCP or Groq with API key.")
|
175 |
+
mcp.populate_model()
|
176 |
+
|
177 |
+
sessions[session_id] = mcp
|
178 |
+
if api_key not in unique_apikeys:
|
179 |
+
unique_apikeys.append(api_key)
|
180 |
+
else:
|
181 |
+
raise Exception("Session with this API key already exists. We won't re-return you the session ID. Bye-bye Hacker !!")
|
182 |
+
return InitResponse(
|
183 |
+
session_id=session_id,
|
184 |
+
models=mcp.models,
|
185 |
+
error=None,
|
186 |
+
success=True
|
187 |
+
)
|
188 |
+
except Exception as e:
|
189 |
+
traceback.print_exception(e)
|
190 |
+
return InitResponse(
|
191 |
+
session_id="",
|
192 |
+
models=None,
|
193 |
+
error=str(e),
|
194 |
+
success=False
|
195 |
+
)
|
196 |
+
|
197 |
+
class ChatRequest(BaseModel):
|
198 |
+
session_id: str
|
199 |
+
query: str
|
200 |
+
tool_use: Optional[bool] = True
|
201 |
+
model: Optional[str] = "llama-3.3-70b-versatile"
|
202 |
+
|
203 |
+
class ChatResponse(BaseModel):
|
204 |
+
output: str
|
205 |
+
error: Optional[str] = None
|
206 |
+
|
207 |
+
@app.post("/chat", response_model=ChatResponse)
|
208 |
+
async def chat(req: ChatRequest):
|
209 |
+
"""
|
210 |
+
Handles chat requests for a given session.
|
211 |
+
"""
|
212 |
+
try:
|
213 |
+
mcp = get_mcp_client(req.session_id)
|
214 |
+
mcp.tool_use = req.tool_use
|
215 |
+
if req.model in mcp.models:
|
216 |
+
mcp.current_model = req.model
|
217 |
+
else:
|
218 |
+
raise ValueError(f"Model not recognized: Not in the model list: {mcp.models}")
|
219 |
+
result = await mcp.process_query(req.query)
|
220 |
+
return ChatResponse(output=result)
|
221 |
+
except Exception as e:
|
222 |
+
traceback.print_exception(e)
|
223 |
+
return ChatResponse(output="", error=str(e))
|
224 |
+
|
225 |
+
@app.post("/logout")
|
226 |
+
async def logout(logout_req: LogoutRequest):
|
227 |
+
"""Clean up session resources."""
|
228 |
+
mcp = sessions.pop(logout_req.session_id, None)
|
229 |
+
unique_apikeys.remove(mcp.api_key)
|
230 |
+
if mcp and hasattr(mcp.exit_stack, "aclose"):
|
231 |
+
await mcp.exit_stack.aclose()
|
232 |
+
return {"success": True}
|
index.html
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<title>Groq Chatbot SPA</title>
|
6 |
+
<link rel="stylesheet" href="static/style.css">
|
7 |
+
</head>
|
8 |
+
<body>
|
9 |
+
<!-- API Key Modal -->
|
10 |
+
<div id="modal" class="modal">
|
11 |
+
<div class="modal-content">
|
12 |
+
<h2>Enter Groq API Key</h2>
|
13 |
+
<input type="password" id="apiKeyInput" placeholder="Groq API Key">
|
14 |
+
<button id="loginBtn">Login</button>
|
15 |
+
<div id="loginError" class="error"></div>
|
16 |
+
</div>
|
17 |
+
</div>
|
18 |
+
|
19 |
+
<!-- Chat App -->
|
20 |
+
<div id="app" class="hidden">
|
21 |
+
<header>
|
22 |
+
<span>Groq Chatbot</span>
|
23 |
+
<select id="modelSelect"></select>
|
24 |
+
<label class="toggle-label">
|
25 |
+
<input type="checkbox" id="toolToggle" checked>
|
26 |
+
Tool Calls
|
27 |
+
</label>
|
28 |
+
<button id="logoutBtn">Logout</button>
|
29 |
+
</header>
|
30 |
+
<div id="chatContainer"></div>
|
31 |
+
<div id="typingIndicator" class="typing hidden">Bot is typing...</div>
|
32 |
+
<form id="inputForm" autocomplete="off">
|
33 |
+
<input id="userInput" type="text" placeholder="Type your message..." autocomplete="off">
|
34 |
+
<button type="submit">Send</button>
|
35 |
+
</form>
|
36 |
+
</div>
|
37 |
+
<script src="static/script.js"></script>
|
38 |
+
</body>
|
39 |
+
</html>
|
requirements.txt
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
uvicorn[standard]
|
2 |
+
fastapi
|
3 |
+
mcp[cli]
|
4 |
+
httpx
|
5 |
+
uv
|
6 |
+
groq
|
server.py
ADDED
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Any, List, Literal, Dict, Optional
|
2 |
+
import httpx
|
3 |
+
import traceback
|
4 |
+
from mcp.server.fastmcp import FastMCP
|
5 |
+
|
6 |
+
# Initialize FastMCP server
|
7 |
+
mcp = FastMCP("patent-problematic-generator-helper")
|
8 |
+
|
9 |
+
# API used
|
10 |
+
ARXIV_BASE = "https://om4r932-arxiv.hf.space"
|
11 |
+
DUCKDUCKGO_BASE = "https://ychkhan-ptt-endpoints.hf.space"
|
12 |
+
DOC3GPPFINDER_BASE = "https://organizedprogrammers-3gppdocfinder.hf.space"
|
13 |
+
|
14 |
+
# Request function
|
15 |
+
|
16 |
+
async def post_data_to_api(url, **kwargs):
|
17 |
+
data = dict(kwargs)
|
18 |
+
if data is None or data == {}:
|
19 |
+
return (None, "")
|
20 |
+
headers = {"Accept": "application/json"}
|
21 |
+
async with httpx.AsyncClient(verify=False, timeout=180) as client:
|
22 |
+
try:
|
23 |
+
response = await client.post(url, headers=headers, json=data)
|
24 |
+
print(response)
|
25 |
+
response.raise_for_status()
|
26 |
+
return response.json()
|
27 |
+
except Exception as e:
|
28 |
+
traceback.print_exception(e)
|
29 |
+
return (None, e)
|
30 |
+
|
31 |
+
async def fake_post_data_to_api(url, **kwargs):
|
32 |
+
params = dict(kwargs)
|
33 |
+
if params is None or params == {}:
|
34 |
+
return (None, "")
|
35 |
+
headers = {"Accept": "application/json"}
|
36 |
+
async with httpx.AsyncClient(verify=False, timeout=180) as client:
|
37 |
+
try:
|
38 |
+
response = await client.post(url, headers=headers, params=params)
|
39 |
+
print(response)
|
40 |
+
response.raise_for_status()
|
41 |
+
return response.json()
|
42 |
+
except Exception as e:
|
43 |
+
traceback.print_exception(e)
|
44 |
+
return (None, e)
|
45 |
+
|
46 |
+
async def get_data_from_api(url):
|
47 |
+
headers = {"Accept": "application/json"}
|
48 |
+
async with httpx.AsyncClient(verify=False, timeout=180) as client:
|
49 |
+
try:
|
50 |
+
response = await client.get(url, headers=headers)
|
51 |
+
print(response)
|
52 |
+
response.raise_for_status()
|
53 |
+
return response.json()
|
54 |
+
except Exception as e:
|
55 |
+
traceback.print_exception(e)
|
56 |
+
return (None, e)
|
57 |
+
|
58 |
+
# Tools
|
59 |
+
|
60 |
+
# arXiv
|
61 |
+
|
62 |
+
@mcp.tool()
|
63 |
+
async def get_arxiv_publications(keywords: str, limit: int):
|
64 |
+
"""
|
65 |
+
Search arXiv publications based on keywords and a limit of documents printed
|
66 |
+
Arguments available: keywords: string [mandatory], limit: integer [mandatory, default = 5]
|
67 |
+
"""
|
68 |
+
endpoint = ARXIV_BASE + "/search"
|
69 |
+
data = await post_data_to_api(endpoint, keyword=keywords, limit=limit)
|
70 |
+
if isinstance(data, tuple) and data[0] is None:
|
71 |
+
return f"An error has occured while getting publications: {data[1]}"
|
72 |
+
if data["error"]:
|
73 |
+
return data["message"]
|
74 |
+
if len(data) < 1:
|
75 |
+
return "No publications has been found"
|
76 |
+
|
77 |
+
results = data["message"]
|
78 |
+
output = []
|
79 |
+
for pub, metadata in results.items():
|
80 |
+
output.append(f"arXiv pub ID: {pub}\nTitle: {metadata['title']}\nAuthors: {metadata['authors']}\nPublished on: {metadata['date']}\nAbstract: {metadata['abstract']}\nPDF URL: {metadata['pdf']}\n")
|
81 |
+
|
82 |
+
return "-\n".join(output)
|
83 |
+
|
84 |
+
# 3GPP Doc Finder
|
85 |
+
|
86 |
+
@mcp.tool()
|
87 |
+
async def get_document_url(doc_id: str, release: int = None):
|
88 |
+
"""
|
89 |
+
Find 3GPP document (TSG docs, specifications or workshop files) only by their ID [note that it will only work with keywords] (and release if it's a specification only) and return their position via a URL (and a scope if it's a specification)
|
90 |
+
Arguments available: doc_id: string [mandatory], release: integer [optional for every case]
|
91 |
+
"""
|
92 |
+
endpoint = DOC3GPPFINDER_BASE + "/find"
|
93 |
+
data = await post_data_to_api(endpoint, doc_id=doc_id, release=release)
|
94 |
+
if isinstance(data, tuple) and data[0] is None:
|
95 |
+
return f"An error while searching publications: {data[1]}"
|
96 |
+
output = f'Document ID: {doc_id}\nURL: {data.get("url", "Not found !")}'
|
97 |
+
output += f'\nScope: {data["scope"]}' if data.get("scope", None) is not None else ""
|
98 |
+
return output
|
99 |
+
|
100 |
+
@mcp.tool()
|
101 |
+
async def search_specs(keywords: str, limit: int):
|
102 |
+
"""
|
103 |
+
Search 3GPP specifications only by their keywords [note that it will only work with keywords](and some filters [see kwargs field])
|
104 |
+
Arguments available: keywords: string [mandatory, separated by space], limit [mandatory, default = 5]
|
105 |
+
Kwargs available [optional]: (release: integer as string or 'Rel-xxx', wg: string = working group (S1, C4, SP, ...), spec_type: string (either TS or TR), mode: string (either 'and' or 'or') = search mode)
|
106 |
+
"""
|
107 |
+
endpoint = DOC3GPPFINDER_BASE + "/search-spec"
|
108 |
+
data = await post_data_to_api(endpoint, keywords=keywords)
|
109 |
+
if isinstance(data, tuple) and data[0] is None:
|
110 |
+
return f"An error has occured while searching specifications"
|
111 |
+
results = data['results'][:min(len(data['results'])-1, limit)]
|
112 |
+
output = []
|
113 |
+
for spec in results:
|
114 |
+
output.append(f"Specification ID: {spec['id']}\nTitle: {spec['title']}\nType: {spec['type']}\nRelease: {spec['release']}\nVersion: {spec['version']}\nWorking Group: {spec['working_group']}\nURL of spec: {spec['url']}\n")
|
115 |
+
|
116 |
+
return "-\n".join(output)
|
117 |
+
|
118 |
+
@mcp.tool()
|
119 |
+
async def get_multiple_documents_url(doc_ids: List[str], release: int = None):
|
120 |
+
"""
|
121 |
+
[BATCH] Search multiple 3GPP documents (TSG docs, specifications or workshop files) [note that it will only work with document ID] (and release if it's a specification only) and return only their position via a URL
|
122 |
+
Arguments available: doc_ids: list of string [mandatory], release: integer [optional for every case]
|
123 |
+
"""
|
124 |
+
endpoint = DOC3GPPFINDER_BASE + "/batch"
|
125 |
+
data = await post_data_to_api(endpoint, doc_ids=doc_ids, release=release)
|
126 |
+
if isinstance(data, tuple) and data[0] is None:
|
127 |
+
return f"An error while searching publications: {data[1]}"
|
128 |
+
results = data["results"]
|
129 |
+
output = []
|
130 |
+
for doc_id, url in results.items():
|
131 |
+
output.append(f'Document ID: {doc_id}\nURL: {url}\n')
|
132 |
+
return "-\n".join(output)
|
133 |
+
|
134 |
+
# PTT Endpoints
|
135 |
+
|
136 |
+
@mcp.tool()
|
137 |
+
async def search_documents_web(query: str, data_type: str = None, limit: int = 5):
|
138 |
+
"""
|
139 |
+
Search on the Web (thanks to DuckDuckGo) documents based on the user's query
|
140 |
+
Arguments available: query: string [mandatory], data_type: string [optional, either 'pdf', 'patent' or None (classic web search)], limit: integer [optional, default = 5]
|
141 |
+
"""
|
142 |
+
endpoint = DUCKDUCKGO_BASE + "/search"
|
143 |
+
data = await fake_post_data_to_api(endpoint, query=query, data_type=data_type, max_references=limit)
|
144 |
+
if isinstance(data, tuple) and data[0] is None:
|
145 |
+
return f"An error while searching publications: {data[1]}"
|
146 |
+
results = data["results"]
|
147 |
+
output = []
|
148 |
+
for ref in results:
|
149 |
+
output.append(f"Title: {ref['title']}\nBody: {ref['body']}\nURL: {ref['url']}")
|
150 |
+
return "-\n".join(output)
|
151 |
+
|
152 |
+
if __name__ == "__main__":
|
153 |
+
mcp.run(transport="stdio")
|
static/script.js
ADDED
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// --- State ---
|
2 |
+
let sessionId = null;
|
3 |
+
let models = [];
|
4 |
+
let currentModel = null;
|
5 |
+
|
6 |
+
const REQUEST_LIMIT = 4;
|
7 |
+
const REQUEST_WINDOW_MS = 60 * 1000; // 1 minute
|
8 |
+
let requestTimestamps = [];
|
9 |
+
|
10 |
+
// --- DOM Elements ---
|
11 |
+
const modal = document.getElementById('modal');
|
12 |
+
const apiKeyInput = document.getElementById('apiKeyInput');
|
13 |
+
const loginBtn = document.getElementById('loginBtn');
|
14 |
+
const loginError = document.getElementById('loginError');
|
15 |
+
const app = document.getElementById('app');
|
16 |
+
const chatContainer = document.getElementById('chatContainer');
|
17 |
+
const toolToggle = document.getElementById('toolToggle');
|
18 |
+
const inputForm = document.getElementById('inputForm');
|
19 |
+
const userInput = document.getElementById('userInput');
|
20 |
+
const modelSelect = document.getElementById('modelSelect');
|
21 |
+
const logoutBtn = document.getElementById('logoutBtn');
|
22 |
+
const typingIndicator = document.getElementById('typingIndicator');
|
23 |
+
|
24 |
+
let toolUse = true;
|
25 |
+
|
26 |
+
// --- Helpers ---
|
27 |
+
function canSendRequest() {
|
28 |
+
const now = Date.now();
|
29 |
+
// Remove timestamps older than 1 minute
|
30 |
+
requestTimestamps = requestTimestamps.filter(ts => now - ts < REQUEST_WINDOW_MS);
|
31 |
+
return requestTimestamps.length < REQUEST_LIMIT;
|
32 |
+
}
|
33 |
+
|
34 |
+
function recordRequest() {
|
35 |
+
requestTimestamps.push(Date.now());
|
36 |
+
}
|
37 |
+
|
38 |
+
function scrollToBottom() {
|
39 |
+
chatContainer.scrollTop = chatContainer.scrollHeight;
|
40 |
+
}
|
41 |
+
|
42 |
+
function addMessage(text, sender) {
|
43 |
+
const msgDiv = document.createElement('div');
|
44 |
+
msgDiv.className = 'message ' + sender;
|
45 |
+
const bubble = document.createElement('div');
|
46 |
+
bubble.className = 'bubble';
|
47 |
+
bubble.textContent = text;
|
48 |
+
msgDiv.appendChild(bubble);
|
49 |
+
chatContainer.appendChild(msgDiv);
|
50 |
+
scrollToBottom();
|
51 |
+
}
|
52 |
+
|
53 |
+
function setTyping(isTyping) {
|
54 |
+
typingIndicator.classList.toggle('hidden', !isTyping);
|
55 |
+
scrollToBottom();
|
56 |
+
}
|
57 |
+
|
58 |
+
function showError(msg) {
|
59 |
+
loginError.textContent = msg || '';
|
60 |
+
}
|
61 |
+
|
62 |
+
toolToggle.addEventListener('change', function() {
|
63 |
+
toolUse = toolToggle.checked;
|
64 |
+
});
|
65 |
+
|
66 |
+
// --- Auth & Model Selection ---
|
67 |
+
loginBtn.onclick = async function () {
|
68 |
+
const apiKey = apiKeyInput.value.trim();
|
69 |
+
if (!apiKey) { showError('API key required.'); return; }
|
70 |
+
loginBtn.disabled = true;
|
71 |
+
showError('');
|
72 |
+
try {
|
73 |
+
const res = await fetch('/init', {
|
74 |
+
method: 'POST',
|
75 |
+
headers: {'Content-Type': 'application/json'},
|
76 |
+
body: JSON.stringify({api_key: apiKey})
|
77 |
+
});
|
78 |
+
const data = await res.json();
|
79 |
+
if (!data.success) { showError(data.error || 'Login failed.'); loginBtn.disabled = false; return; }
|
80 |
+
sessionId = data.session_id;
|
81 |
+
models = data.models || [];
|
82 |
+
// Populate model dropdown
|
83 |
+
modelSelect.innerHTML = '';
|
84 |
+
models.forEach(m => {
|
85 |
+
const opt = document.createElement('option');
|
86 |
+
opt.value = m;
|
87 |
+
opt.textContent = m;
|
88 |
+
modelSelect.appendChild(opt);
|
89 |
+
});
|
90 |
+
currentModel = models[0];
|
91 |
+
modelSelect.value = currentModel;
|
92 |
+
// Show app, hide modal
|
93 |
+
modal.classList.add('hidden');
|
94 |
+
app.classList.remove('hidden');
|
95 |
+
chatContainer.innerHTML = '';
|
96 |
+
userInput.focus();
|
97 |
+
} catch (e) {
|
98 |
+
showError('Network error.');
|
99 |
+
loginBtn.disabled = false;
|
100 |
+
}
|
101 |
+
};
|
102 |
+
|
103 |
+
modelSelect.onchange = function () {
|
104 |
+
currentModel = modelSelect.value;
|
105 |
+
};
|
106 |
+
|
107 |
+
// --- Chat Logic ---
|
108 |
+
inputForm.onsubmit = async function (e) {
|
109 |
+
e.preventDefault();
|
110 |
+
const text = userInput.value.trim();
|
111 |
+
if (!text) return;
|
112 |
+
|
113 |
+
if (!canSendRequest()) {
|
114 |
+
addMessage('[Rate limit] Please wait: only 4 requests per minute allowed.', 'bot');
|
115 |
+
return;
|
116 |
+
}
|
117 |
+
recordRequest();
|
118 |
+
|
119 |
+
addMessage(text, 'user');
|
120 |
+
userInput.value = '';
|
121 |
+
setTyping(true);
|
122 |
+
try {
|
123 |
+
const res = await fetch('/chat', {
|
124 |
+
method: 'POST',
|
125 |
+
headers: {'Content-Type': 'application/json'},
|
126 |
+
body: JSON.stringify({
|
127 |
+
session_id: sessionId,
|
128 |
+
query: text,
|
129 |
+
tool_use: toolUse, // <-- Use the toggle value here
|
130 |
+
model: currentModel
|
131 |
+
})
|
132 |
+
});
|
133 |
+
const data = await res.json();
|
134 |
+
setTyping(false);
|
135 |
+
if (data.error) {
|
136 |
+
addMessage('[Error] ' + data.error, 'bot');
|
137 |
+
return;
|
138 |
+
}
|
139 |
+
addMessage(data.output, 'bot');
|
140 |
+
} catch (e) {
|
141 |
+
setTyping(false);
|
142 |
+
addMessage('[Network error]', 'bot');
|
143 |
+
}
|
144 |
+
};
|
145 |
+
|
146 |
+
// --- Logout Logic ---
|
147 |
+
logoutBtn.onclick = async function () {
|
148 |
+
await logoutAndReset();
|
149 |
+
};
|
150 |
+
|
151 |
+
async function logoutAndReset() {
|
152 |
+
if (sessionId) {
|
153 |
+
try {
|
154 |
+
await fetch('/logout', {
|
155 |
+
method: 'POST',
|
156 |
+
headers: {'Content-Type': 'application/json'},
|
157 |
+
body: JSON.stringify({session_id: sessionId})
|
158 |
+
});
|
159 |
+
} catch {}
|
160 |
+
sessionId = null;
|
161 |
+
models = [];
|
162 |
+
currentModel = null;
|
163 |
+
}
|
164 |
+
app.classList.add('hidden');
|
165 |
+
modal.classList.remove('hidden');
|
166 |
+
apiKeyInput.value = '';
|
167 |
+
chatContainer.innerHTML = '';
|
168 |
+
showError('');
|
169 |
+
loginBtn.disabled = false;
|
170 |
+
}
|
171 |
+
|
172 |
+
// --- Auto logout on exit ---
|
173 |
+
window.addEventListener('beforeunload', logoutAndReset);
|
174 |
+
|
175 |
+
// --- UX: Enter key in modal input triggers login ---
|
176 |
+
apiKeyInput.addEventListener('keyup', function(e) {
|
177 |
+
if (e.key === 'Enter') loginBtn.click();
|
178 |
+
});
|
static/style.css
ADDED
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
body {
|
2 |
+
font-family: system-ui, sans-serif;
|
3 |
+
margin: 0;
|
4 |
+
background: #f4f6fb;
|
5 |
+
}
|
6 |
+
|
7 |
+
header {
|
8 |
+
background: #232946;
|
9 |
+
color: #fff;
|
10 |
+
padding: 12px 20px;
|
11 |
+
display: flex;
|
12 |
+
align-items: center;
|
13 |
+
gap: 16px;
|
14 |
+
justify-content: space-between;
|
15 |
+
}
|
16 |
+
|
17 |
+
header select, header button {
|
18 |
+
font-size: 1em;
|
19 |
+
padding: 4px 8px;
|
20 |
+
border-radius: 4px;
|
21 |
+
border: none;
|
22 |
+
}
|
23 |
+
|
24 |
+
#chatContainer {
|
25 |
+
max-width: 600px;
|
26 |
+
margin: 30px auto 0 auto;
|
27 |
+
background: #fff;
|
28 |
+
border-radius: 10px;
|
29 |
+
box-shadow: 0 2px 16px rgba(0,0,0,0.06);
|
30 |
+
min-height: 400px;
|
31 |
+
padding: 18px 10px 70px 10px;
|
32 |
+
overflow-y: auto;
|
33 |
+
height: 60vh;
|
34 |
+
display: flex;
|
35 |
+
flex-direction: column;
|
36 |
+
}
|
37 |
+
|
38 |
+
.message {
|
39 |
+
display: flex;
|
40 |
+
margin-bottom: 10px;
|
41 |
+
align-items: flex-end;
|
42 |
+
}
|
43 |
+
|
44 |
+
.message.user {
|
45 |
+
justify-content: flex-end;
|
46 |
+
}
|
47 |
+
|
48 |
+
.message.bot {
|
49 |
+
justify-content: flex-start;
|
50 |
+
}
|
51 |
+
|
52 |
+
.bubble {
|
53 |
+
max-width: 70%;
|
54 |
+
padding: 10px 14px;
|
55 |
+
border-radius: 20px;
|
56 |
+
font-size: 1em;
|
57 |
+
line-height: 1.5;
|
58 |
+
word-break: break-word;
|
59 |
+
}
|
60 |
+
|
61 |
+
.user .bubble {
|
62 |
+
background: #232946;
|
63 |
+
color: #fff;
|
64 |
+
border-bottom-right-radius: 4px;
|
65 |
+
}
|
66 |
+
|
67 |
+
.bot .bubble {
|
68 |
+
background: #e7eaf6;
|
69 |
+
color: #232946;
|
70 |
+
border-bottom-left-radius: 4px;
|
71 |
+
}
|
72 |
+
|
73 |
+
#inputForm {
|
74 |
+
position: fixed;
|
75 |
+
left: 0; right: 0; bottom: 0;
|
76 |
+
max-width: 600px;
|
77 |
+
margin: 0 auto;
|
78 |
+
display: flex;
|
79 |
+
gap: 10px;
|
80 |
+
background: #fff;
|
81 |
+
padding: 12px 10px;
|
82 |
+
border-top: 1px solid #e7eaf6;
|
83 |
+
}
|
84 |
+
|
85 |
+
#userInput {
|
86 |
+
flex: 1;
|
87 |
+
padding: 10px;
|
88 |
+
border-radius: 6px;
|
89 |
+
border: 1px solid #ccc;
|
90 |
+
font-size: 1em;
|
91 |
+
}
|
92 |
+
|
93 |
+
#inputForm button {
|
94 |
+
padding: 0 18px;
|
95 |
+
border-radius: 6px;
|
96 |
+
background: #232946;
|
97 |
+
color: #fff;
|
98 |
+
border: none;
|
99 |
+
font-size: 1em;
|
100 |
+
cursor: pointer;
|
101 |
+
}
|
102 |
+
|
103 |
+
.typing {
|
104 |
+
font-size: 0.95em;
|
105 |
+
color: #888;
|
106 |
+
margin-left: 18px;
|
107 |
+
margin-bottom: 8px;
|
108 |
+
}
|
109 |
+
|
110 |
+
.hidden {
|
111 |
+
display: none !important;
|
112 |
+
}
|
113 |
+
|
114 |
+
/* Modal */
|
115 |
+
.modal {
|
116 |
+
position: fixed;
|
117 |
+
top: 0; left: 0; right: 0; bottom: 0;
|
118 |
+
background: rgba(0,0,0,0.18);
|
119 |
+
display: flex;
|
120 |
+
align-items: center;
|
121 |
+
justify-content: center;
|
122 |
+
z-index: 10;
|
123 |
+
}
|
124 |
+
|
125 |
+
.modal-content {
|
126 |
+
background: #fff;
|
127 |
+
padding: 32px 28px 24px 28px;
|
128 |
+
border-radius: 12px;
|
129 |
+
box-shadow: 0 2px 16px rgba(0,0,0,0.12);
|
130 |
+
min-width: 320px;
|
131 |
+
display: flex;
|
132 |
+
flex-direction: column;
|
133 |
+
gap: 16px;
|
134 |
+
}
|
135 |
+
|
136 |
+
.modal-content input {
|
137 |
+
padding: 10px;
|
138 |
+
border-radius: 6px;
|
139 |
+
border: 1px solid #ccc;
|
140 |
+
font-size: 1em;
|
141 |
+
}
|
142 |
+
|
143 |
+
.toggle-label {
|
144 |
+
display: flex;
|
145 |
+
align-items: center;
|
146 |
+
gap: 5px;
|
147 |
+
font-size: 1em;
|
148 |
+
color: #fff;
|
149 |
+
}
|
150 |
+
.toggle-label input[type="checkbox"] {
|
151 |
+
accent-color: #232946;
|
152 |
+
width: 16px;
|
153 |
+
height: 16px;
|
154 |
+
}
|
155 |
+
.modal-content button {
|
156 |
+
padding: 8px 0;
|
157 |
+
border-radius: 6px;
|
158 |
+
background: #232946;
|
159 |
+
color: #fff;
|
160 |
+
border: none;
|
161 |
+
font-size: 1em;
|
162 |
+
cursor: pointer;
|
163 |
+
}
|
164 |
+
|
165 |
+
.error {
|
166 |
+
color: #c00;
|
167 |
+
font-size: 0.95em;
|
168 |
+
min-height: 18px;
|
169 |
+
}
|
170 |
+
|