Spaces:
Sleeping
Sleeping
Omar ID EL MOUMEN
commited on
Commit
·
8227e25
1
Parent(s):
93c72cb
Final version
Browse files- Dockerfile +13 -0
- README.md +4 -4
- app.py +195 -0
- index.html +361 -0
- mcp_client.py +37 -0
- requirements.txt +6 -0
- server.py +143 -0
- static/proxy_llm.js +262 -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"]
|
README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
| 1 |
---
|
| 2 |
title: MCPSynapseChat
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: gpl-3.0
|
| 9 |
-
short_description: A MCP
|
| 10 |
---
|
| 11 |
|
| 12 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
title: MCPSynapseChat
|
| 3 |
+
emoji: 🗿
|
| 4 |
+
colorFrom: gray
|
| 5 |
+
colorTo: red
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: gpl-3.0
|
| 9 |
+
short_description: A MCP chabot that communicates with Synapse LLM
|
| 10 |
---
|
| 11 |
|
| 12 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
app.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import traceback
|
| 2 |
+
from fastapi import FastAPI, WebSocket
|
| 3 |
+
from fastapi.responses import FileResponse
|
| 4 |
+
import asyncio
|
| 5 |
+
from fastapi.staticfiles import StaticFiles
|
| 6 |
+
from contextlib import asynccontextmanager
|
| 7 |
+
import json
|
| 8 |
+
from fastapi import HTTPException
|
| 9 |
+
from pydantic import BaseModel
|
| 10 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 11 |
+
from typing import List, Optional, Any, Dict
|
| 12 |
+
from mcp_client import MCPClient
|
| 13 |
+
|
| 14 |
+
mcp = MCPClient()
|
| 15 |
+
|
| 16 |
+
class ChatMessage(BaseModel):
|
| 17 |
+
role: str
|
| 18 |
+
content: str
|
| 19 |
+
|
| 20 |
+
class ChatCompletionRequest(BaseModel):
|
| 21 |
+
model: str = "gemini-2.5-pro-exp-03-25"
|
| 22 |
+
messages: List[ChatMessage]
|
| 23 |
+
tools: Optional[list] = []
|
| 24 |
+
max_tokens: Optional[int] = None
|
| 25 |
+
|
| 26 |
+
class ChatCompletionResponseChoice(BaseModel):
|
| 27 |
+
index: int = 0
|
| 28 |
+
message: ChatMessage
|
| 29 |
+
finish_reason: str = "stop"
|
| 30 |
+
|
| 31 |
+
class ChatCompletionResponse(BaseModel):
|
| 32 |
+
id: str
|
| 33 |
+
object: str = "chat.completion"
|
| 34 |
+
created: int
|
| 35 |
+
model: str
|
| 36 |
+
choices: List[ChatCompletionResponseChoice]
|
| 37 |
+
|
| 38 |
+
@asynccontextmanager
|
| 39 |
+
async def lifespan(app: FastAPI):
|
| 40 |
+
try:
|
| 41 |
+
await mcp.connect()
|
| 42 |
+
print("Connexion au MCP réussi !")
|
| 43 |
+
except Exception as e:
|
| 44 |
+
print("Warning ! : Connexion au MCP impossible\n", str(e))
|
| 45 |
+
|
| 46 |
+
yield
|
| 47 |
+
|
| 48 |
+
if mcp.session:
|
| 49 |
+
try:
|
| 50 |
+
await mcp.exit_stack.aclose()
|
| 51 |
+
print("MCP déconnecté !")
|
| 52 |
+
except Exception as e:
|
| 53 |
+
print("Erreur à la fermeture du MCP\n", str(e))
|
| 54 |
+
|
| 55 |
+
app = FastAPI(lifespan=lifespan)
|
| 56 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 57 |
+
app.add_middleware(
|
| 58 |
+
CORSMiddleware,
|
| 59 |
+
allow_credentials=True,
|
| 60 |
+
allow_headers=["*"],
|
| 61 |
+
allow_methods=["*"],
|
| 62 |
+
allow_origins=["*"]
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
class ConnectionManager:
|
| 66 |
+
def __init__(self):
|
| 67 |
+
self.active_connections = {}
|
| 68 |
+
self.response_queues = {}
|
| 69 |
+
|
| 70 |
+
async def connect(self, websocket: WebSocket):
|
| 71 |
+
await websocket.accept()
|
| 72 |
+
self.active_connections[websocket] = None
|
| 73 |
+
|
| 74 |
+
def set_source(self, websocket: WebSocket, source: str):
|
| 75 |
+
if websocket in self.active_connections:
|
| 76 |
+
self.active_connections[websocket] = source
|
| 77 |
+
|
| 78 |
+
async def send_to_dest(self, destination: str, message: str):
|
| 79 |
+
for ws, src in self.active_connections.items():
|
| 80 |
+
if src == destination:
|
| 81 |
+
await ws.send_text(message)
|
| 82 |
+
|
| 83 |
+
def remove(self, websocket: WebSocket):
|
| 84 |
+
if websocket in self.active_connections:
|
| 85 |
+
del self.active_connections[websocket]
|
| 86 |
+
|
| 87 |
+
async def wait_for_response(self, request_id: str, timeout: int = 30):
|
| 88 |
+
queue = asyncio.Queue(maxsize=1)
|
| 89 |
+
self.response_queues[request_id] = queue
|
| 90 |
+
try:
|
| 91 |
+
return await asyncio.wait_for(queue.get(), timeout=timeout)
|
| 92 |
+
finally:
|
| 93 |
+
self.response_queues.pop(request_id, None)
|
| 94 |
+
|
| 95 |
+
manager = ConnectionManager()
|
| 96 |
+
|
| 97 |
+
@app.get("/")
|
| 98 |
+
async def index_page():
|
| 99 |
+
return FileResponse("index.html")
|
| 100 |
+
|
| 101 |
+
# @app.post("/v1/chat/completions", response_model=ChatCompletionResponse)
|
| 102 |
+
# async def chat_completions(request: ChatCompletionRequest):
|
| 103 |
+
# request_id = str(uuid.uuid4())
|
| 104 |
+
# proxy_ws = next((ws for ws, src in manager.active_connections.items() if src == "proxy"), None)
|
| 105 |
+
# if not proxy_ws:
|
| 106 |
+
# raise HTTPException(503, "Proxy client not connected !")
|
| 107 |
+
# user_msg = next((m for m in request.messages if m.role == "user"), None)
|
| 108 |
+
# if not user_msg:
|
| 109 |
+
# raise HTTPException(400, "No user message found !")
|
| 110 |
+
|
| 111 |
+
# proxy_msg = {
|
| 112 |
+
# "request_id": request_id,
|
| 113 |
+
# "content": user_msg.content,
|
| 114 |
+
# "source": "api",
|
| 115 |
+
# "destination": "proxy",
|
| 116 |
+
# "model": request.model,
|
| 117 |
+
# "tools": request.tools,
|
| 118 |
+
# "max_tokens": request.max_tokens
|
| 119 |
+
# }
|
| 120 |
+
|
| 121 |
+
# await proxy_ws.send_text(json.dumps(proxy_msg))
|
| 122 |
+
|
| 123 |
+
# try:
|
| 124 |
+
# response_content = await manager.wait_for_response(request_id)
|
| 125 |
+
# except asyncio.TimeoutError:
|
| 126 |
+
# raise HTTPException(504, "Proxy response timeout")
|
| 127 |
+
# return ChatCompletionResponse(
|
| 128 |
+
# id=request_id,
|
| 129 |
+
# created=int(time.time()),
|
| 130 |
+
# model=request.model,
|
| 131 |
+
# choices=[ChatCompletionResponseChoice(
|
| 132 |
+
# message=ChatMessage(role="assistant", content=response_content)
|
| 133 |
+
# )]
|
| 134 |
+
# )
|
| 135 |
+
|
| 136 |
+
class ToolCallRequest(BaseModel):
|
| 137 |
+
tool_calls: List[Dict[str, Any]]
|
| 138 |
+
|
| 139 |
+
@app.get("/list-tools", response_model=List[Dict[str, Any]])
|
| 140 |
+
async def list_tools():
|
| 141 |
+
if not mcp.session:
|
| 142 |
+
try:
|
| 143 |
+
await mcp.connect()
|
| 144 |
+
except Exception as e:
|
| 145 |
+
raise HTTPException(status_code=503, detail=f"Connexion au MCP impossible !\n{str(e)}")
|
| 146 |
+
try:
|
| 147 |
+
tools = await mcp.list_tools()
|
| 148 |
+
return tools
|
| 149 |
+
except Exception as e:
|
| 150 |
+
raise HTTPException(status_code=500, detail=f"Erreur lors de la récupération des outils: {str(e)}")
|
| 151 |
+
|
| 152 |
+
@app.post("/call-tools")
|
| 153 |
+
async def call_tools(request: ToolCallRequest):
|
| 154 |
+
if not mcp.session:
|
| 155 |
+
try:
|
| 156 |
+
await mcp.connect()
|
| 157 |
+
except Exception as e:
|
| 158 |
+
raise HTTPException(status_code=503, detail=f"Erreur lors de la récupération des outils: {str(e)}")
|
| 159 |
+
try:
|
| 160 |
+
result_tools = []
|
| 161 |
+
for tool_call in request.tool_calls:
|
| 162 |
+
print(tool_call)
|
| 163 |
+
tool = tool_call["function"]
|
| 164 |
+
tool_name = tool["name"]
|
| 165 |
+
tool_args = tool["arguments"]
|
| 166 |
+
result = await mcp.session.call_tool(tool_name, json.loads(tool_args))
|
| 167 |
+
result_tools.append({
|
| 168 |
+
"role": "user",
|
| 169 |
+
"content": result.content[0].text
|
| 170 |
+
})
|
| 171 |
+
print("Finished !")
|
| 172 |
+
return result_tools
|
| 173 |
+
except Exception as e:
|
| 174 |
+
raise HTTPException(status_code=500, detail=f"Erreur lors de l'appel des outils: {str(e)}")
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
@app.websocket("/ws")
|
| 180 |
+
async def websocket_endpoint(websocket: WebSocket):
|
| 181 |
+
await manager.connect(websocket)
|
| 182 |
+
try:
|
| 183 |
+
data = await websocket.receive_text()
|
| 184 |
+
init_msg = json.loads(data)
|
| 185 |
+
if 'source' in init_msg:
|
| 186 |
+
manager.set_source(websocket, init_msg['source'])
|
| 187 |
+
print(init_msg['source'])
|
| 188 |
+
|
| 189 |
+
while True:
|
| 190 |
+
message = await websocket.receive_text()
|
| 191 |
+
msg_data = json.loads(message)
|
| 192 |
+
await manager.send_to_dest(msg_data["destination"], message)
|
| 193 |
+
except Exception as e:
|
| 194 |
+
manager.remove(websocket)
|
| 195 |
+
await websocket.close()
|
index.html
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html>
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<title>Chat System with LLM Proxy</title>
|
| 6 |
+
<style>
|
| 7 |
+
body {
|
| 8 |
+
font-family: Arial, sans-serif;
|
| 9 |
+
margin: 0;
|
| 10 |
+
padding: 20px;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
.container {
|
| 14 |
+
display: flex;
|
| 15 |
+
gap: 20px;
|
| 16 |
+
height: 90vh;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
.panel {
|
| 20 |
+
flex: 1;
|
| 21 |
+
border: 1px solid #ddd;
|
| 22 |
+
border-radius: 8px;
|
| 23 |
+
padding: 15px;
|
| 24 |
+
display: flex;
|
| 25 |
+
flex-direction: column;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
h2 {
|
| 29 |
+
margin-top: 0;
|
| 30 |
+
border-bottom: 1px solid #eee;
|
| 31 |
+
padding-bottom: 10px;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
.chat-container {
|
| 35 |
+
height: 300px;
|
| 36 |
+
overflow-y: scroll;
|
| 37 |
+
border: 1px solid #eee;
|
| 38 |
+
padding: 10px;
|
| 39 |
+
margin-bottom: 10px;
|
| 40 |
+
flex: 1;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
.input-container {
|
| 44 |
+
display: flex;
|
| 45 |
+
gap: 10px;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
input[type="text"],
|
| 49 |
+
input[type="password"] {
|
| 50 |
+
flex: 1;
|
| 51 |
+
padding: 8px;
|
| 52 |
+
border: 1px solid #ddd;
|
| 53 |
+
border-radius: 4px;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
button {
|
| 57 |
+
padding: 8px 15px;
|
| 58 |
+
background-color: #4CAF50;
|
| 59 |
+
color: white;
|
| 60 |
+
border: none;
|
| 61 |
+
border-radius: 4px;
|
| 62 |
+
cursor: pointer;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
button:hover {
|
| 66 |
+
background-color: #45a049;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.message {
|
| 70 |
+
margin-bottom: 10px;
|
| 71 |
+
padding: 8px;
|
| 72 |
+
border-radius: 8px;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.user-message {
|
| 76 |
+
background-color: #e1f5fe;
|
| 77 |
+
align-self: flex-end;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.assistant-message {
|
| 81 |
+
background-color: #f1f1f1;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.connection-status {
|
| 85 |
+
color: #666;
|
| 86 |
+
font-size: 0.9em;
|
| 87 |
+
margin-top: 10px;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.message-entry {
|
| 91 |
+
margin: 5px 0;
|
| 92 |
+
padding: 8px;
|
| 93 |
+
border-radius: 8px;
|
| 94 |
+
background: white;
|
| 95 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
| 96 |
+
font-family: monospace;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.incoming {
|
| 100 |
+
border-left: 4px solid #4CAF50;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.outgoing {
|
| 104 |
+
border-left: 4px solid #2196F3;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.system {
|
| 108 |
+
border-left: 4px solid #9C27B0;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.error {
|
| 112 |
+
border-left: 4px solid #F44336;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.message-header {
|
| 116 |
+
display: flex;
|
| 117 |
+
justify-content: space-between;
|
| 118 |
+
font-size: 0.8em;
|
| 119 |
+
color: #666;
|
| 120 |
+
margin-bottom: 4px;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.tabs {
|
| 124 |
+
display: flex;
|
| 125 |
+
margin-bottom: 15px;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.tab {
|
| 129 |
+
padding: 10px 20px;
|
| 130 |
+
cursor: pointer;
|
| 131 |
+
border: 1px solid #ddd;
|
| 132 |
+
border-radius: 4px 4px 0 0;
|
| 133 |
+
margin-right: 5px;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.tab.active {
|
| 137 |
+
background-color: #f1f1f1;
|
| 138 |
+
border-bottom: none;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.tab-content {
|
| 142 |
+
display: none;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.tab-content.active {
|
| 146 |
+
display: block;
|
| 147 |
+
flex: 1;
|
| 148 |
+
display: flex;
|
| 149 |
+
flex-direction: column;
|
| 150 |
+
}
|
| 151 |
+
</style>
|
| 152 |
+
</head>
|
| 153 |
+
|
| 154 |
+
<body>
|
| 155 |
+
<div class="tabs">
|
| 156 |
+
<div class="tab active" onclick="switchTab('chat')">Chat Client</div>
|
| 157 |
+
<div class="tab" onclick="switchTab('proxy')">Proxy Configuration</div>
|
| 158 |
+
</div>
|
| 159 |
+
|
| 160 |
+
<div class="container">
|
| 161 |
+
<!-- Chat Client Panel -->
|
| 162 |
+
<div id="chat-tab" class="tab-content active panel">
|
| 163 |
+
<h2>Chat Client</h2>
|
| 164 |
+
<div id="chat" class="chat-container"></div>
|
| 165 |
+
<div class="input-container">
|
| 166 |
+
<input id="msg" type="text" placeholder="Type your message here...">
|
| 167 |
+
<button onclick="sendMessage()">Send</button>
|
| 168 |
+
</div>
|
| 169 |
+
<div id="client-status" class="connection-status">Connecting...</div>
|
| 170 |
+
</div>
|
| 171 |
+
|
| 172 |
+
<!-- Proxy Configuration Panel -->
|
| 173 |
+
<div id="proxy-tab" class="tab-content panel">
|
| 174 |
+
<h2>LLM Proxy Configuration</h2>
|
| 175 |
+
<div style="margin-bottom: 20px;">
|
| 176 |
+
<input type="password" id="apiKey" placeholder="Enter API Key" style="width: 100%;">
|
| 177 |
+
<button onclick="initializeClient()" style="margin-top: 10px;">Fetch Models</button>
|
| 178 |
+
</div>
|
| 179 |
+
<select id="modelSelect" style="width: 100%; margin-bottom: 20px;"></select>
|
| 180 |
+
<div id="systemStatus" class="connection-status"></div>
|
| 181 |
+
|
| 182 |
+
<h3>Message Flow</h3>
|
| 183 |
+
<div id="messageFlow"
|
| 184 |
+
style="flex: 1; border: 1px solid #eee; padding: 10px; overflow-y: auto; background: #f9f9f9;">
|
| 185 |
+
<div style="text-align: center; color: #999; margin-bottom: 10px;">Message Flow</div>
|
| 186 |
+
</div>
|
| 187 |
+
<div id="detailedStatus" class="connection-status"></div>
|
| 188 |
+
</div>
|
| 189 |
+
</div>
|
| 190 |
+
|
| 191 |
+
<script>
|
| 192 |
+
function showStatus(message, type = 'info') {
|
| 193 |
+
const statusDiv = document.getElementById('systemStatus');
|
| 194 |
+
statusDiv.innerHTML = `<div style="color: ${type === 'error' ? '#F44336' : '#4CAF50'}">${message}</div>`;
|
| 195 |
+
addMessageEntry('system', 'system', 'proxy', message);
|
| 196 |
+
}
|
| 197 |
+
// Tab switching functionality
|
| 198 |
+
function switchTab(tabName) {
|
| 199 |
+
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
|
| 200 |
+
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
| 201 |
+
|
| 202 |
+
document.querySelector(`.tab[onclick="switchTab('${tabName}')"]`).classList.add('active');
|
| 203 |
+
document.getElementById(`${tabName}-tab`).classList.add('active');
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
// Client WebSocket
|
| 207 |
+
const clientWs = new WebSocket('wss://' + window.location.host + '/ws');
|
| 208 |
+
clientWs.onopen = () => {
|
| 209 |
+
clientWs.send(JSON.stringify({
|
| 210 |
+
source: 'user'
|
| 211 |
+
}));
|
| 212 |
+
document.getElementById('client-status').textContent = 'Connected';
|
| 213 |
+
};
|
| 214 |
+
clientWs.onclose = () => {
|
| 215 |
+
document.getElementById('client-status').textContent = 'Disconnected';
|
| 216 |
+
};
|
| 217 |
+
clientWs.onmessage = e => {
|
| 218 |
+
const msg = JSON.parse(e.data);
|
| 219 |
+
const chatDiv = document.getElementById('chat');
|
| 220 |
+
chatDiv.innerHTML += `<div class="message assistant-message">${msg.content}</div>`;
|
| 221 |
+
chatDiv.scrollTop = chatDiv.scrollHeight;
|
| 222 |
+
};
|
| 223 |
+
|
| 224 |
+
function sendMessage() {
|
| 225 |
+
const input = document.getElementById('msg');
|
| 226 |
+
const content = input.value.trim();
|
| 227 |
+
if (content) {
|
| 228 |
+
const message = {
|
| 229 |
+
content: content,
|
| 230 |
+
source: 'user',
|
| 231 |
+
destination: 'proxy',
|
| 232 |
+
request_id: generateUUID()
|
| 233 |
+
};
|
| 234 |
+
clientWs.send(JSON.stringify(message));
|
| 235 |
+
|
| 236 |
+
const chatDiv = document.getElementById('chat');
|
| 237 |
+
chatDiv.innerHTML += `<div class="message user-message">${content}</div>`;
|
| 238 |
+
chatDiv.scrollTop = chatDiv.scrollHeight;
|
| 239 |
+
|
| 240 |
+
input.value = '';
|
| 241 |
+
}
|
| 242 |
+
}
|
| 243 |
+
document.getElementById('msg').addEventListener('keypress', function (e) {
|
| 244 |
+
if (e.key === 'Enter') {
|
| 245 |
+
sendMessage();
|
| 246 |
+
}
|
| 247 |
+
});
|
| 248 |
+
|
| 249 |
+
// Proxy WebSocket
|
| 250 |
+
let proxyWs = new WebSocket('wss://' + window.location.host + '/ws');
|
| 251 |
+
proxyWs.onopen = () => {
|
| 252 |
+
proxyWs.send(JSON.stringify({
|
| 253 |
+
source: 'proxy'
|
| 254 |
+
}));
|
| 255 |
+
showStatus('Connected to server');
|
| 256 |
+
};
|
| 257 |
+
proxyWs.onclose = () => {
|
| 258 |
+
showStatus('Disconnected from server', 'error');
|
| 259 |
+
};
|
| 260 |
+
proxyWs.onmessage = async e => {
|
| 261 |
+
const msg = JSON.parse(e.data);
|
| 262 |
+
|
| 263 |
+
// Display incoming messages
|
| 264 |
+
if (msg.destination === 'proxy') {
|
| 265 |
+
let tools = null
|
| 266 |
+
addMessageEntry('incoming', msg.source, 'proxy', msg.content);
|
| 267 |
+
document.getElementById('detailedStatus').textContent = `Processing ${msg.source} request...`;
|
| 268 |
+
|
| 269 |
+
try {
|
| 270 |
+
const response = await fetch("/list-tools");
|
| 271 |
+
tools = await response.json();
|
| 272 |
+
} catch (error) {
|
| 273 |
+
console.log(`Failed to fetch tools : ${error}`);
|
| 274 |
+
tools = null;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
try {
|
| 278 |
+
if (!agentClient) {
|
| 279 |
+
throw new Error(
|
| 280 |
+
"LLM client not initialized. Please enter API key and fetch models first.");
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
if (!currentModel) {
|
| 284 |
+
throw new Error("No model selected. Please select a model first.");
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
let llmResponse = await agentClient.call(
|
| 288 |
+
currentModel,
|
| 289 |
+
msg.content,
|
| 290 |
+
conversationHistory,
|
| 291 |
+
tools
|
| 292 |
+
);
|
| 293 |
+
|
| 294 |
+
conversationHistory = llmResponse.history
|
| 295 |
+
|
| 296 |
+
// Display outgoing response
|
| 297 |
+
addMessageEntry('outgoing', 'proxy', msg.source, llmResponse.response);
|
| 298 |
+
|
| 299 |
+
if(llmResponse.response.tool_calls != null){
|
| 300 |
+
try {
|
| 301 |
+
console.log("Calling ....")
|
| 302 |
+
const toolCalls = await fetch("/call-tools", {
|
| 303 |
+
method: "POST",
|
| 304 |
+
headers: {
|
| 305 |
+
"Content-Type": "application/json"
|
| 306 |
+
},
|
| 307 |
+
body: JSON.stringify({tool_calls: llmResponse.response.tool_calls})
|
| 308 |
+
})
|
| 309 |
+
const toolCallsJson = await toolCalls.json()
|
| 310 |
+
console.log("Succeed !")
|
| 311 |
+
for(const toolCall of toolCallsJson){
|
| 312 |
+
conversationHistory.push(toolCall)
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
llmResponse = await agentClient.call(
|
| 316 |
+
currentModel,
|
| 317 |
+
null,
|
| 318 |
+
conversationHistory,
|
| 319 |
+
null
|
| 320 |
+
)
|
| 321 |
+
|
| 322 |
+
conversationHistory = llmResponse.history
|
| 323 |
+
} catch (error) {
|
| 324 |
+
throw new Error("Error on calling tools " + error)
|
| 325 |
+
}
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
const responseMsg = {
|
| 329 |
+
request_id: msg.request_id,
|
| 330 |
+
content: llmResponse.response.content,
|
| 331 |
+
source: 'proxy',
|
| 332 |
+
destination: msg.source
|
| 333 |
+
};
|
| 334 |
+
proxyWs.send(JSON.stringify(responseMsg));
|
| 335 |
+
|
| 336 |
+
document.getElementById('detailedStatus').textContent = `Response sent to ${msg.source}`;
|
| 337 |
+
} catch (error) {
|
| 338 |
+
addMessageEntry('error', 'system', 'proxy', `Error: ${error.message}`);
|
| 339 |
+
const errorResponse = {
|
| 340 |
+
request_id: msg.request_id,
|
| 341 |
+
content: `Error: ${error.message}`,
|
| 342 |
+
source: 'proxy',
|
| 343 |
+
destination: msg.source
|
| 344 |
+
};
|
| 345 |
+
proxyWs.send(JSON.stringify(errorResponse));
|
| 346 |
+
}
|
| 347 |
+
}
|
| 348 |
+
};
|
| 349 |
+
|
| 350 |
+
function generateUUID() {
|
| 351 |
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
| 352 |
+
var r = Math.random() * 16 | 0,
|
| 353 |
+
v = c == 'x' ? r : (r & 0x3 | 0x8);
|
| 354 |
+
return v.toString(16);
|
| 355 |
+
});
|
| 356 |
+
}
|
| 357 |
+
</script>
|
| 358 |
+
<script src="/static/proxy_llm.js"></script>
|
| 359 |
+
</body>
|
| 360 |
+
|
| 361 |
+
</html>
|
mcp_client.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional
|
| 2 |
+
from contextlib import AsyncExitStack
|
| 3 |
+
|
| 4 |
+
from mcp import ClientSession, StdioServerParameters
|
| 5 |
+
from mcp.client.stdio import stdio_client
|
| 6 |
+
from websockets import ClientConnection
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class MCPClient:
|
| 10 |
+
def __init__(self):
|
| 11 |
+
self.session: Optional[ClientSession] = None
|
| 12 |
+
self.exit_stack = AsyncExitStack()
|
| 13 |
+
self.stdio = None
|
| 14 |
+
self.write = None
|
| 15 |
+
self.ws: Optional[ClientConnection] = None
|
| 16 |
+
|
| 17 |
+
async def connect(self):
|
| 18 |
+
server_params = StdioServerParameters(
|
| 19 |
+
command="uv",
|
| 20 |
+
args=["--directory", "/app", "run", "server.py"],
|
| 21 |
+
env=None
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
|
| 25 |
+
self.stdio, self.write = stdio_transport
|
| 26 |
+
self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
|
| 27 |
+
|
| 28 |
+
await self.session.initialize()
|
| 29 |
+
|
| 30 |
+
async def list_tools(self):
|
| 31 |
+
tools = await self.session.list_tools()
|
| 32 |
+
tools_openai = [{
|
| 33 |
+
"name": tool.name,
|
| 34 |
+
"description": tool.description,
|
| 35 |
+
"parameters": tool.inputSchema
|
| 36 |
+
} for tool in tools.tools]
|
| 37 |
+
return tools_openai
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
uvicorn[standard]
|
| 2 |
+
fastapi
|
| 3 |
+
mcp[cli]
|
| 4 |
+
httpx
|
| 5 |
+
websockets
|
| 6 |
+
uv
|
server.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, Literal
|
| 2 |
+
import httpx
|
| 3 |
+
import traceback
|
| 4 |
+
from mcp.server.fastmcp import FastMCP
|
| 5 |
+
|
| 6 |
+
# Initialize FastMCP server
|
| 7 |
+
mcp = FastMCP("arxiv-omar")
|
| 8 |
+
|
| 9 |
+
# Constants
|
| 10 |
+
CUSTOM_ARXIV_API_BASE = "https://om4r932-arxiv.hf.space"
|
| 11 |
+
DDG_API_BASE = "https://ychkhan-ptt-endpoints.hf.space"
|
| 12 |
+
API_3GPP_BASE = "https://organizedprogrammers-3gppdocfinder.hf.space"
|
| 13 |
+
|
| 14 |
+
# Helpers
|
| 15 |
+
async def make_request(url: str, data: dict = None) -> dict[str, Any] | None:
|
| 16 |
+
if data is None:
|
| 17 |
+
return None
|
| 18 |
+
headers = {
|
| 19 |
+
"Accept": "application/json"
|
| 20 |
+
}
|
| 21 |
+
async with httpx.AsyncClient(verify=False) 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
|
| 30 |
+
|
| 31 |
+
def format_search(pub_id: str, content: dict) -> str:
|
| 32 |
+
return f"""
|
| 33 |
+
arXiv publication ID : {pub_id}
|
| 34 |
+
Title : {content["title"]}
|
| 35 |
+
Authors : {content["authors"]}
|
| 36 |
+
Release Date : {content["date"]}
|
| 37 |
+
Abstract : {content["abstract"]}
|
| 38 |
+
PDF link : {content["pdf"]}
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
def format_extract(message: dict) -> str:
|
| 42 |
+
return f"""
|
| 43 |
+
Title of PDF : {message.get("title", "No title has been found")}
|
| 44 |
+
Text : {message.get("text", "No text !")}
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
def format_result_search(page: dict) -> str:
|
| 48 |
+
return f"""
|
| 49 |
+
Title : {page.get("title", "No titles found !")}
|
| 50 |
+
Little description : {page.get("body", "No description")}
|
| 51 |
+
PDF url : {page.get("url", None)}
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
def format_3gpp_doc_result(result: dict, release: int = None) -> str:
|
| 55 |
+
return f"""
|
| 56 |
+
Document ID : {result.get("doc_id")}
|
| 57 |
+
Release version : {release if release is not None else "Not specified"}
|
| 58 |
+
URL : {result.get("url", "No URL found !")}
|
| 59 |
+
"""
|
| 60 |
+
|
| 61 |
+
# Tools
|
| 62 |
+
@mcp.tool()
|
| 63 |
+
async def get_publications(keyword: str, limit: int = 15) -> str:
|
| 64 |
+
"""
|
| 65 |
+
Get arXiv publications based on keywords and limit of documents
|
| 66 |
+
|
| 67 |
+
Args:
|
| 68 |
+
keyword: Keywords separated by spaces
|
| 69 |
+
limit: Numbers of maximum publications returned (by default, 15)
|
| 70 |
+
"""
|
| 71 |
+
url = f"{CUSTOM_ARXIV_API_BASE}/search"
|
| 72 |
+
data = await make_request(url, data={'keyword': keyword, 'limit': limit})
|
| 73 |
+
if data["error"]:
|
| 74 |
+
return data["message"]
|
| 75 |
+
if not data:
|
| 76 |
+
return "Unable to fetch publications"
|
| 77 |
+
if len(data["message"].keys()) == 0:
|
| 78 |
+
return "No publications found"
|
| 79 |
+
|
| 80 |
+
publications = [format_search(pub_id, content) for (pub_id, content) in data["message"].items()]
|
| 81 |
+
return "\n--\n".join(publications)
|
| 82 |
+
|
| 83 |
+
@mcp.tool()
|
| 84 |
+
async def web_pdf_search(query: str) -> str:
|
| 85 |
+
"""
|
| 86 |
+
Search on the Web (with DuckDuckGo search engine) to get PDF documents based on the keywords
|
| 87 |
+
|
| 88 |
+
Args:
|
| 89 |
+
query: Keywords to search documents on the Web
|
| 90 |
+
"""
|
| 91 |
+
|
| 92 |
+
url = f"{DDG_API_BASE}/search"
|
| 93 |
+
data = await make_request(url, data={"query": query})
|
| 94 |
+
if not data:
|
| 95 |
+
return "Unable to fetch results"
|
| 96 |
+
if len(data["results"]) == 0:
|
| 97 |
+
return "No results found"
|
| 98 |
+
|
| 99 |
+
results = [format_result_search(result) for result in data["results"]]
|
| 100 |
+
return "\n--\n".join(results)
|
| 101 |
+
|
| 102 |
+
@mcp.tool()
|
| 103 |
+
async def get_3gpp_doc_url_byID(doc_id: str, release: int = None):
|
| 104 |
+
"""
|
| 105 |
+
Get 3GPP Technical Document URL by their document ID.
|
| 106 |
+
|
| 107 |
+
Args:
|
| 108 |
+
doc_id: Document ID (i.e. C4-125411, SP-551242, 31.101)
|
| 109 |
+
release : The release version of the document (by default, None)
|
| 110 |
+
"""
|
| 111 |
+
url = f"{API_3GPP_BASE}/find"
|
| 112 |
+
data = await make_request(url, data={"doc_id": doc_id, "release": release})
|
| 113 |
+
if not data:
|
| 114 |
+
return "Unable to search document in 3GPP"
|
| 115 |
+
|
| 116 |
+
return format_3gpp_doc_result(data, release)
|
| 117 |
+
|
| 118 |
+
@mcp.tool()
|
| 119 |
+
async def get_pdf_text(pdf_url: str, limit_page: int = -1) -> str:
|
| 120 |
+
"""
|
| 121 |
+
Extract the text from the URL pointing to a PDF file
|
| 122 |
+
|
| 123 |
+
Args:
|
| 124 |
+
pdf_url: URL to a PDF document
|
| 125 |
+
limit_page: How many pages the user wants to extract the content (default: -1 for all pages)
|
| 126 |
+
"""
|
| 127 |
+
|
| 128 |
+
url = f"{CUSTOM_ARXIV_API_BASE}/extract_pdf/url"
|
| 129 |
+
data = {"url": pdf_url}
|
| 130 |
+
if limit_page != -1:
|
| 131 |
+
data["page_num"] = limit_page
|
| 132 |
+
data = await make_request(url, data=data)
|
| 133 |
+
if data["error"]:
|
| 134 |
+
return data["message"]
|
| 135 |
+
if not data:
|
| 136 |
+
return "Unable to extract PDF text"
|
| 137 |
+
if len(data["message"].keys()) == 0:
|
| 138 |
+
return "No text can be extracted from this PDF"
|
| 139 |
+
|
| 140 |
+
return format_extract(data["message"])
|
| 141 |
+
|
| 142 |
+
if __name__ == "__main__":
|
| 143 |
+
mcp.run(transport="stdio")
|
static/proxy_llm.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// LLM Client Implementation
|
| 2 |
+
let agentClient = null;
|
| 3 |
+
let currentModel = null;
|
| 4 |
+
let conversationHistory = [];
|
| 5 |
+
|
| 6 |
+
function initializeClient() {
|
| 7 |
+
const apiKey = document.getElementById('apiKey').value;
|
| 8 |
+
if (!apiKey) {
|
| 9 |
+
showStatus("Please enter an API key", 'error');
|
| 10 |
+
return;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
agentClient = new ConversationalAgentClient(apiKey);
|
| 14 |
+
agentClient.populateLLMModels()
|
| 15 |
+
.then(models => {
|
| 16 |
+
agentClient.updateModelSelect('modelSelect', models.find(m => m.includes("gemini-2.5")));
|
| 17 |
+
currentModel = document.getElementById('modelSelect').value;
|
| 18 |
+
showStatus(`Loaded ${models.length} models. Default: ${currentModel}`);
|
| 19 |
+
})
|
| 20 |
+
.catch(error => {
|
| 21 |
+
showStatus(`Error fetching models: ${error.message}`, 'error');
|
| 22 |
+
});
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
function addMessageEntry(direction, source, destination, content) {
|
| 26 |
+
const flowDiv = document.getElementById('messageFlow');
|
| 27 |
+
const timestamp = new Date().toLocaleTimeString();
|
| 28 |
+
|
| 29 |
+
const entry = document.createElement('div');
|
| 30 |
+
entry.className = `message-entry ${direction}`;
|
| 31 |
+
entry.innerHTML = `
|
| 32 |
+
<div class="message-header">
|
| 33 |
+
<span>${source} → ${destination}</span>
|
| 34 |
+
<span>${timestamp}</span>
|
| 35 |
+
</div>
|
| 36 |
+
<div style="white-space: pre-wrap;">${content}</div>
|
| 37 |
+
`;
|
| 38 |
+
|
| 39 |
+
flowDiv.appendChild(entry);
|
| 40 |
+
flowDiv.scrollTop = flowDiv.scrollHeight;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
// LLM Client Classes
|
| 44 |
+
class BaseAgentClient {
|
| 45 |
+
constructor(apiKey, apiUrl = 'https://llm.synapse.thalescloud.io/v1/') {
|
| 46 |
+
this.apiKey = apiKey;
|
| 47 |
+
this.apiUrl = apiUrl;
|
| 48 |
+
this.models = [];
|
| 49 |
+
this.tools = [];
|
| 50 |
+
this.maxCallsPerMinute = 4;
|
| 51 |
+
this.callTimestamps = [];
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
setTools(tools) {
|
| 55 |
+
this.tools = tools;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
async fetchLLMModels() {
|
| 59 |
+
if (!this.apiKey) throw new Error("API Key is not set.");
|
| 60 |
+
console.log("Fetching models from:", this.apiUrl + 'models');
|
| 61 |
+
|
| 62 |
+
try {
|
| 63 |
+
const response = await fetch(this.apiUrl + 'models', {
|
| 64 |
+
method: 'GET',
|
| 65 |
+
headers: {
|
| 66 |
+
'Authorization': `Bearer ${this.apiKey}`
|
| 67 |
+
}
|
| 68 |
+
});
|
| 69 |
+
|
| 70 |
+
if (!response.ok) {
|
| 71 |
+
const errorText = await response.text();
|
| 72 |
+
console.error("Fetch models error response:", errorText);
|
| 73 |
+
throw new Error(`HTTP error! Status: ${response.status} - ${errorText}`);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
const data = await response.json();
|
| 77 |
+
console.log("Models fetched:", data.data);
|
| 78 |
+
|
| 79 |
+
const filteredModels = data.data
|
| 80 |
+
.map(model => model.id)
|
| 81 |
+
.filter(id => !id.toLowerCase().includes('embed') && !id.toLowerCase().includes('image'));
|
| 82 |
+
|
| 83 |
+
return filteredModels;
|
| 84 |
+
} catch (error) {
|
| 85 |
+
console.error('Error fetching LLM models:', error);
|
| 86 |
+
throw new Error(`Failed to fetch models: ${error.message}`);
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
async populateLLMModels(defaultModel = "gemini-2.5-pro-exp-03-25") {
|
| 91 |
+
try {
|
| 92 |
+
const modelList = await this.fetchLLMModels();
|
| 93 |
+
|
| 94 |
+
const sortedModels = modelList.sort((a, b) => {
|
| 95 |
+
if (a === defaultModel) return -1;
|
| 96 |
+
if (b === defaultModel) return 1;
|
| 97 |
+
return a.localeCompare(b);
|
| 98 |
+
});
|
| 99 |
+
|
| 100 |
+
const finalModels = [];
|
| 101 |
+
|
| 102 |
+
if (sortedModels.includes(defaultModel)) {
|
| 103 |
+
finalModels.push(defaultModel);
|
| 104 |
+
sortedModels.forEach(model => {
|
| 105 |
+
if (model !== defaultModel) finalModels.push(model);
|
| 106 |
+
});
|
| 107 |
+
} else {
|
| 108 |
+
finalModels.push(defaultModel);
|
| 109 |
+
finalModels.push(...sortedModels);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
this.models = finalModels;
|
| 113 |
+
console.log("Populated models:", this.models);
|
| 114 |
+
return this.models;
|
| 115 |
+
} catch (error) {
|
| 116 |
+
console.error("Error populating models:", error);
|
| 117 |
+
this.models = [defaultModel];
|
| 118 |
+
throw error;
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
updateModelSelect(elementId = 'modelSelect', selectedModel = null) {
|
| 123 |
+
const select = document.getElementById(elementId);
|
| 124 |
+
if (!select) {
|
| 125 |
+
console.warn(`Element ID ${elementId} not found.`);
|
| 126 |
+
return;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
const currentSelection = selectedModel || select.value || this.models[0];
|
| 130 |
+
select.innerHTML = '';
|
| 131 |
+
|
| 132 |
+
if (this.models.length === 0 || (this.models.length === 1 && this.models[0] === "gemini-2.5-pro-exp-03-25" && !this.apiKey)) {
|
| 133 |
+
const option = document.createElement('option');
|
| 134 |
+
option.value = "";
|
| 135 |
+
option.textContent = "-- Fetch models first --";
|
| 136 |
+
option.disabled = true;
|
| 137 |
+
select.appendChild(option);
|
| 138 |
+
return;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
this.models.forEach(model => {
|
| 142 |
+
const option = document.createElement('option');
|
| 143 |
+
option.value = model;
|
| 144 |
+
option.textContent = model;
|
| 145 |
+
if (model === currentSelection) option.selected = true;
|
| 146 |
+
select.appendChild(option);
|
| 147 |
+
});
|
| 148 |
+
|
| 149 |
+
if (!select.value && this.models.length > 0) select.value = this.models[0];
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
async rateLimitWait() {
|
| 153 |
+
const currentTime = Date.now();
|
| 154 |
+
this.callTimestamps = this.callTimestamps.filter(ts => currentTime - ts <= 60000);
|
| 155 |
+
|
| 156 |
+
if (this.callTimestamps.length >= this.maxCallsPerMinute) {
|
| 157 |
+
const waitTime = 60000 - (currentTime - this.callTimestamps[0]);
|
| 158 |
+
const waitSeconds = Math.ceil(waitTime / 1000);
|
| 159 |
+
const waitMessage = `Rate limit (${this.maxCallsPerMinute}/min) reached. Waiting ${waitSeconds}s...`;
|
| 160 |
+
|
| 161 |
+
console.log(waitMessage);
|
| 162 |
+
showStatus(waitMessage, 'warn');
|
| 163 |
+
|
| 164 |
+
await new Promise(resolve => setTimeout(resolve, waitTime + 100));
|
| 165 |
+
|
| 166 |
+
showStatus('Resuming after rate limit wait...', 'info');
|
| 167 |
+
this.callTimestamps = this.callTimestamps.filter(ts => Date.now() - ts <= 60000);
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
async callAgent(model, messages, tools = null) {
|
| 172 |
+
await this.rateLimitWait();
|
| 173 |
+
const startTime = Date.now();
|
| 174 |
+
console.log("Calling Agent:", model);
|
| 175 |
+
|
| 176 |
+
let body = {
|
| 177 |
+
model: model,
|
| 178 |
+
messages: messages
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
body.tools = tools;
|
| 182 |
+
|
| 183 |
+
try {
|
| 184 |
+
const response = await fetch(this.apiUrl + 'chat/completions', {
|
| 185 |
+
method: 'POST',
|
| 186 |
+
headers: {
|
| 187 |
+
'Content-Type': 'application/json',
|
| 188 |
+
'Authorization': `Bearer ${this.apiKey}`
|
| 189 |
+
},
|
| 190 |
+
body: JSON.stringify(body)
|
| 191 |
+
});
|
| 192 |
+
|
| 193 |
+
const endTime = Date.now();
|
| 194 |
+
this.callTimestamps.push(endTime);
|
| 195 |
+
console.log(`API call took ${endTime - startTime} ms`);
|
| 196 |
+
|
| 197 |
+
if (!response.ok) {
|
| 198 |
+
const errorData = await response.json().catch(() => ({ error: { message: response.statusText } }));
|
| 199 |
+
console.error("API Error:", errorData);
|
| 200 |
+
throw new Error(errorData.error?.message || `API failed: ${response.status}`);
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
const data = await response.json();
|
| 204 |
+
if (!data.choices || !data.choices[0]?.message) throw new Error("Invalid API response structure");
|
| 205 |
+
|
| 206 |
+
console.log("API Response received.");
|
| 207 |
+
return data.choices[0].message;
|
| 208 |
+
} catch (error) {
|
| 209 |
+
this.callTimestamps.push(Date.now());
|
| 210 |
+
console.error('Error calling agent:', error);
|
| 211 |
+
throw error;
|
| 212 |
+
}
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
setMaxCallsPerMinute(value) {
|
| 216 |
+
const parsedValue = parseInt(value, 10);
|
| 217 |
+
if (!isNaN(parsedValue) && parsedValue > 0) {
|
| 218 |
+
console.log(`Max calls/min set to: ${parsedValue}`);
|
| 219 |
+
this.maxCallsPerMinute = parsedValue;
|
| 220 |
+
return true;
|
| 221 |
+
}
|
| 222 |
+
console.warn(`Invalid max calls/min: ${value}`);
|
| 223 |
+
return false;
|
| 224 |
+
}
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
class ConversationalAgentClient extends BaseAgentClient {
|
| 228 |
+
constructor(apiKey, apiUrl = 'https://llm.synapse.thalescloud.io/v1/') {
|
| 229 |
+
super(apiKey, apiUrl);
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
async call(model, userPrompt, conversationHistory = [], tools) {
|
| 233 |
+
const messages = userPrompt ? [
|
| 234 |
+
...conversationHistory,
|
| 235 |
+
{ role: 'user', content: userPrompt }
|
| 236 |
+
] : [
|
| 237 |
+
...conversationHistory
|
| 238 |
+
];
|
| 239 |
+
|
| 240 |
+
const assistantResponse = await super.callAgent(model, messages, tools);
|
| 241 |
+
|
| 242 |
+
const updatedHistory = userPrompt ? [
|
| 243 |
+
...conversationHistory,
|
| 244 |
+
{ role: 'user', content: userPrompt },
|
| 245 |
+
{ role: assistantResponse.role, content: assistantResponse.content }
|
| 246 |
+
] : [
|
| 247 |
+
...conversationHistory,
|
| 248 |
+
{ role: assistantResponse.role, content: assistantResponse.content }
|
| 249 |
+
];
|
| 250 |
+
|
| 251 |
+
return {
|
| 252 |
+
response: assistantResponse,
|
| 253 |
+
history: updatedHistory
|
| 254 |
+
};
|
| 255 |
+
}
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
// Model selection change handler
|
| 259 |
+
document.getElementById('modelSelect').addEventListener('change', function() {
|
| 260 |
+
currentModel = this.value;
|
| 261 |
+
showStatus(`Model changed to: ${currentModel}`);
|
| 262 |
+
});
|