mohms commited on
Commit
2d5ee92
·
1 Parent(s): 27c59de
Files changed (1) hide show
  1. main.py +453 -0
main.py ADDED
@@ -0,0 +1,453 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import uuid
3
+ import time
4
+ import asyncio
5
+ import threading
6
+ import json
7
+ import re
8
+ from typing import Optional
9
+ from fastapi import FastAPI, Header, HTTPException, Request
10
+ from fastapi.responses import JSONResponse
11
+
12
+
13
+ API_SECRET_KEY = os.getenv("API_SECRET_KEY", "change-secret-key-2026")
14
+
15
+
16
+ class AsyncBrowserThread(threading.Thread):
17
+ def __init__(self):
18
+ super().__init__(daemon=True)
19
+ self.loop = asyncio.new_event_loop()
20
+ self.ready_event = threading.Event()
21
+ self.browser = None
22
+ self.playwright = None
23
+
24
+ def run(self):
25
+ asyncio.set_event_loop(self.loop)
26
+ self.loop.run_until_complete(self._start_browser())
27
+ self.ready_event.set()
28
+ print("[LITE-SERVER].....")
29
+ self.loop.run_forever()
30
+
31
+ async def _start_browser(self):
32
+ from playwright.async_api import async_playwright
33
+ print("[LITE-SERVER].....")
34
+ self.playwright = await async_playwright().start()
35
+ self.browser = await self.playwright.chromium.launch(
36
+ headless=True,
37
+ channel="chrome",
38
+ args=[
39
+ '--disable-blink-features=AutomationControlled',
40
+ '--no-sandbox',
41
+ '--disable-gpu',
42
+ '--disable-dev-shm-usage-for-fast-performance',
43
+ '--disable-setuid-sandbox',
44
+ ]
45
+ )
46
+
47
+ async def _talk_to_chatgpt(self, prompt: str):
48
+ context = await self.browser.new_context(
49
+ user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
50
+ viewport={'width': 1920, 'height': 1080}
51
+ )
52
+
53
+ await context.add_init_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")
54
+
55
+ page = await context.new_page()
56
+
57
+ try:
58
+ page.set_default_timeout(120000)
59
+ await page.goto("https://chatgpt.com/", wait_until="domcontentloaded")
60
+
61
+ await page.wait_for_selector('#prompt-textarea', timeout=60000)
62
+ await page.fill('#prompt-textarea', prompt)
63
+ await asyncio.sleep(0.5)
64
+ await page.press('#prompt-textarea', 'Enter')
65
+
66
+ await page.wait_for_selector('[data-message-author-role="assistant"]', timeout=120000)
67
+
68
+ last_text = ""
69
+ unchanged_count = 0
70
+ while unchanged_count < 4:
71
+ messages = await page.query_selector_all('[data-message-author-role="assistant"]')
72
+ if messages:
73
+ current_text = await messages[-1].inner_text()
74
+ if current_text == last_text and current_text.strip() != "":
75
+ unchanged_count += 1
76
+ else:
77
+ last_text = current_text
78
+ unchanged_count = 0
79
+ await asyncio.sleep(0.5)
80
+
81
+ return last_text.strip()
82
+
83
+ except Exception as e:
84
+ print(f"[LITE-SERVER] Error: {e}")
85
+ raise e
86
+ finally:
87
+ await page.close()
88
+ await context.close()
89
+
90
+ def process_request(self, prompt: str):
91
+ if not self.ready_event.wait(timeout=30):
92
+ raise Exception("Error From Browser")
93
+
94
+ future = asyncio.run_coroutine_threadsafe(self._talk_to_chatgpt(prompt), self.loop)
95
+ return future.result(timeout=120)
96
+
97
+ browser_engine = AsyncBrowserThread()
98
+ browser_engine.start()
99
+
100
+ # ====================================================================
101
+ # Smart Prompt Builder
102
+ # ====================================================================
103
+ def format_prompt(messages, tools=None):
104
+ parts = []
105
+ system_parts = []
106
+ has_tool_results = False
107
+ user_question = ""
108
+
109
+ for msg in messages:
110
+ role = msg.get("role", "")
111
+ msg_type = msg.get("type", "")
112
+ content = msg.get("content", "")
113
+
114
+ if isinstance(content, list):
115
+ text_parts = []
116
+ for item in content:
117
+ if isinstance(item, dict):
118
+ text_parts.append(item.get("text", item.get("content", str(item))))
119
+ else:
120
+ text_parts.append(str(item))
121
+ content = "\n".join(text_parts)
122
+
123
+ if role == "system":
124
+ system_parts.append(content)
125
+ elif role == "tool":
126
+ has_tool_results = True
127
+ tool_name = msg.get("name", "tool")
128
+ parts.append(f"[TOOL RESULT from '{tool_name}']:\n{content}")
129
+ elif msg_type == "function_call_output":
130
+ has_tool_results = True
131
+ call_id = msg.get("call_id", "")
132
+ output_content = msg.get("output", content)
133
+ parts.append(f"[TOOL RESULT (call_id: {call_id})]:\n{output_content}")
134
+ elif msg_type == "function_call":
135
+ func_name = msg.get("name", "?")
136
+ func_args = msg.get("arguments", "{}")
137
+ parts.append(f"[PREVIOUS TOOL CALL: Called '{func_name}' with arguments: {func_args}]")
138
+ elif role == "assistant":
139
+ assistant_content = content if content else ""
140
+ tool_calls_in_msg = msg.get("tool_calls", [])
141
+ if tool_calls_in_msg:
142
+ tc_descriptions = []
143
+ for tc in tool_calls_in_msg:
144
+ func = tc.get("function", {})
145
+ tc_descriptions.append(f"Called '{func.get('name', '?')}' with: {func.get('arguments', '{}')}")
146
+ assistant_content += "\n[Previous tool calls: " + "; ".join(tc_descriptions) + "]"
147
+ if assistant_content.strip():
148
+ parts.append(f"[Assistant]: {assistant_content}")
149
+ elif role == "user" or (msg_type == "message" and role != "system"):
150
+ user_question = content
151
+ parts.append(content)
152
+ has_tool_results = False
153
+ elif content:
154
+ parts.append(content)
155
+
156
+ final = ""
157
+
158
+ if system_parts:
159
+ if tools and not has_tool_results:
160
+ final += "=== YOUR ROLE ===\n"
161
+ final += "\n\n".join(system_parts)
162
+ final += "\n=== END OF ROLE ===\n\n"
163
+ else:
164
+ final += "=== SYSTEM INSTRUCTIONS (FOLLOW STRICTLY) ===\n"
165
+ final += "\n\n".join(system_parts)
166
+ final += "\n=== END OF INSTRUCTIONS ===\n\n"
167
+
168
+ if tools and not has_tool_results:
169
+ final += format_tools_instruction(tools, user_question)
170
+
171
+ if has_tool_results:
172
+ final += "=== CONTEXT FROM TOOLS ===\n"
173
+ final += "The following information was retrieved by the tools you requested.\n"
174
+ final += "Use ONLY this information to answer the user's question.\n\n"
175
+
176
+ if parts:
177
+ final += "\n".join(parts)
178
+
179
+ if has_tool_results:
180
+ final += "\n\n=== INSTRUCTION ===\n"
181
+ final += "Now answer the user's question based ONLY on the tool results above.\n"
182
+
183
+ return final
184
+
185
+ def format_tools_instruction(tools, user_question=""):
186
+ instruction = "\n=== MANDATORY TOOL USAGE ===\n"
187
+ instruction += "You MUST use one of the tools below to answer this question.\n"
188
+ instruction += "Do NOT answer directly. Do NOT say you don't have information.\n"
189
+ instruction += "You MUST respond with ONLY a JSON object to call the tool.\n\n"
190
+
191
+ instruction += "RESPONSE FORMAT - respond with ONLY this JSON, nothing else:\n"
192
+ instruction += '{"tool_calls": [{"name": "TOOL_NAME", "arguments": {"param": "value"}}]}\n\n'
193
+
194
+ instruction += "RULES:\n"
195
+ instruction += "- Your ENTIRE response must be valid JSON only\n"
196
+ instruction += "- No markdown, no code blocks, no explanation\n"
197
+ instruction += "- No text before or after the JSON\n\n"
198
+
199
+ instruction += "Available tools:\n\n"
200
+
201
+ for tool in tools:
202
+ func = tool.get("function", tool)
203
+ name = func.get("name", "unknown")
204
+ desc = func.get("description", "No description")
205
+ params = func.get("parameters", {})
206
+
207
+ instruction += f"Tool: {name}\n"
208
+ instruction += f"Description: {desc}\n"
209
+
210
+ if params.get("properties"):
211
+ instruction += "Parameters:\n"
212
+ required_params = params.get("required", [])
213
+ for param_name, param_info in params["properties"].items():
214
+ param_type = param_info.get("type", "string")
215
+ param_desc = param_info.get("description", "")
216
+ is_required = "required" if param_name in required_params else "optional"
217
+ instruction += f" - {param_name} ({param_type}, {is_required}): {param_desc}\n"
218
+ instruction += "\n"
219
+
220
+ instruction += "=== END OF TOOLS ===\n\n"
221
+
222
+ first_tool = tools[0] if tools else {}
223
+ first_func = first_tool.get("function", first_tool)
224
+ first_name = first_func.get("name", "tool")
225
+
226
+ instruction += f'EXAMPLE: If the user asks a question, respond with:\n'
227
+ instruction += '{"tool_calls": [{"name": "' + first_name + '", "arguments": {"input": "the user question here"}}]}\n\n'
228
+
229
+ instruction += "Now respond with the JSON to call the appropriate tool:\n\n"
230
+ return instruction
231
+
232
+ def parse_tool_calls(response_text):
233
+ cleaned = response_text.strip()
234
+ if "```" in cleaned:
235
+ code_block_match = re.search(r'```(?:json)?\s*\n?(.*?)\n?\s*```', cleaned, re.DOTALL)
236
+ if code_block_match:
237
+ cleaned = code_block_match.group(1).strip()
238
+
239
+ json_candidates = [cleaned]
240
+ json_match = re.search(r'\{[\s\S]*"tool_calls"[\s\S]*\}', cleaned)
241
+ if json_match:
242
+ json_candidates.append(json_match.group(0))
243
+
244
+ for candidate in json_candidates:
245
+ try:
246
+ parsed = json.loads(candidate)
247
+ if isinstance(parsed, dict) and "tool_calls" in parsed:
248
+ raw_calls = parsed["tool_calls"]
249
+ if isinstance(raw_calls, list) and len(raw_calls) > 0:
250
+ formatted_calls = []
251
+ for call in raw_calls:
252
+ tool_name = call.get("name", "")
253
+ arguments = call.get("arguments", {})
254
+ if isinstance(arguments, dict):
255
+ arguments_str = json.dumps(arguments, ensure_ascii=False)
256
+ else:
257
+ arguments_str = str(arguments)
258
+
259
+ formatted_calls.append({
260
+ "id": f"call_{uuid.uuid4().hex[:24]}",
261
+ "type": "function",
262
+ "function": {
263
+ "name": tool_name,
264
+ "arguments": arguments_str
265
+ }
266
+ })
267
+ return formatted_calls
268
+ except (json.JSONDecodeError, TypeError, KeyError):
269
+ continue
270
+ return None
271
+
272
+ # ====================================================================
273
+ # FastAPI App
274
+ # ====================================================================
275
+ app = FastAPI(title="mse_ai_api for n8n")
276
+
277
+ @app.post("/v1/chat/completions")
278
+ async def chat_completions(request: Request):
279
+
280
+ try:
281
+ data = await request.json()
282
+ except Exception:
283
+ return JSONResponse(status_code=400, content={"error": {"message": "Invalid JSON payload"}})
284
+
285
+ authorization = request.headers.get("authorization", "")
286
+
287
+ if not authorization or authorization.replace("Bearer ", "").strip() != API_SECRET_KEY:
288
+ return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
289
+
290
+ messages = data.get("messages", [])
291
+ if not messages:
292
+ return JSONResponse(status_code=400, content={"error": {"message": "messages field is required"}})
293
+
294
+ try:
295
+ tools = data.get("tools", None)
296
+ prompt = format_prompt(messages, tools=tools)
297
+
298
+ start_time = time.time()
299
+ print(f"[LITE-SERVER]..... ({len(prompt)} len)")
300
+
301
+ response_text = browser_engine.process_request(prompt)
302
+
303
+ p_tokens = len(prompt.split())
304
+ c_tokens = len(response_text.split())
305
+
306
+ tool_calls = None
307
+ if tools:
308
+ tool_calls = parse_tool_calls(response_text)
309
+
310
+ if tool_calls:
311
+ return {
312
+ "id": f"chatcmpl-{uuid.uuid4().hex[:29]}",
313
+ "object": "chat.completion",
314
+ "created": int(start_time),
315
+ "model": data.get("model", "gpt-4o-mini"),
316
+ "choices": [{
317
+ "index": 0,
318
+ "message": {
319
+ "role": "assistant",
320
+ "content": None,
321
+ "tool_calls": tool_calls
322
+ },
323
+ "finish_reason": "tool_calls"
324
+ }],
325
+ "usage": {
326
+ "prompt_tokens": p_tokens,
327
+ "completion_tokens": c_tokens,
328
+ "total_tokens": p_tokens + c_tokens
329
+ }
330
+ }
331
+ else:
332
+ return {
333
+ "id": f"chatcmpl-{uuid.uuid4().hex[:29]}",
334
+ "object": "chat.completion",
335
+ "created": int(start_time),
336
+ "model": data.get("model", "gpt-4o-mini"),
337
+ "choices": [{
338
+ "index": 0,
339
+ "message": {"role": "assistant", "content": response_text},
340
+ "finish_reason": "stop"
341
+ }],
342
+ "usage": {
343
+ "prompt_tokens": p_tokens,
344
+ "completion_tokens": c_tokens,
345
+ "total_tokens": p_tokens + c_tokens
346
+ }
347
+ }
348
+ except Exception as e:
349
+ return JSONResponse(status_code=500, content={"error": str(e)})
350
+
351
+ @app.post("/v1/responses")
352
+ async def responses(request: Request):
353
+
354
+ try:
355
+ data = await request.json()
356
+ except Exception:
357
+ return JSONResponse(status_code=400, content={"error": {"message": "Invalid JSON payload"}})
358
+
359
+ authorization = request.headers.get("authorization", "")
360
+ if not authorization or authorization.replace("Bearer ", "").strip() != API_SECRET_KEY:
361
+ return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
362
+
363
+ input_data = data.get("input", "")
364
+ if isinstance(input_data, str):
365
+ messages = [{"role": "user", "content": input_data}]
366
+ elif isinstance(input_data, list):
367
+ messages = input_data
368
+ else:
369
+ messages = data.get("messages", [])
370
+
371
+ if not messages:
372
+ return JSONResponse(status_code=400, content={"error": {"message": "input field is required"}})
373
+
374
+ try:
375
+ tools = data.get("tools", None)
376
+ instructions = data.get("instructions", "")
377
+ if instructions:
378
+ messages.insert(0, {"role": "system", "content": instructions})
379
+
380
+ prompt = format_prompt(messages, tools=tools)
381
+ start_time = time.time()
382
+
383
+ response_text = browser_engine.process_request(prompt)
384
+ p_tokens = len(prompt.split())
385
+ c_tokens = len(response_text.split())
386
+
387
+ tool_calls = None
388
+ if tools:
389
+ tool_calls = parse_tool_calls(response_text)
390
+
391
+ if tool_calls:
392
+ output_items = []
393
+ for tc in tool_calls:
394
+ output_items.append({
395
+ "type": "function_call",
396
+ "id": tc["id"],
397
+ "call_id": tc["id"],
398
+ "name": tc["function"]["name"],
399
+ "arguments": tc["function"]["arguments"],
400
+ "status": "completed"
401
+ })
402
+
403
+ return {
404
+ "id": f"resp-{uuid.uuid4().hex[:29]}",
405
+ "object": "response",
406
+ "created_at": int(start_time),
407
+ "model": data.get("model", "gpt-4o-mini"),
408
+ "status": "completed",
409
+ "output": output_items,
410
+ "usage": {
411
+ "input_tokens": p_tokens,
412
+ "output_tokens": c_tokens,
413
+ "total_tokens": p_tokens + c_tokens
414
+ }
415
+ }
416
+ else:
417
+ return {
418
+ "id": f"resp-{uuid.uuid4().hex[:29]}",
419
+ "object": "response",
420
+ "created_at": int(start_time),
421
+ "model": data.get("model", "gpt-4o-mini"),
422
+ "status": "completed",
423
+ "output": [
424
+ {
425
+ "type": "message",
426
+ "role": "assistant",
427
+ "content": [{"type": "output_text", "text": response_text}]
428
+ }
429
+ ],
430
+ "usage": {
431
+ "input_tokens": p_tokens,
432
+ "output_tokens": c_tokens,
433
+ "total_tokens": p_tokens + c_tokens
434
+ }
435
+ }
436
+ except Exception as e:
437
+ return JSONResponse(status_code=500, content={"error": str(e)})
438
+
439
+ @app.get("/v1/models")
440
+ async def list_models():
441
+
442
+ return {
443
+ "object": "list",
444
+ "data": [{"id": "gpt-4o-mini", "object": "model", "owned_by": "mse_ai_api"}]
445
+ }
446
+
447
+ @app.get("/")
448
+ async def health_check():
449
+ return {"status": "running", "message": "mse_ai_api Server is active!"}
450
+
451
+ if __name__ == "__main__":
452
+ import uvicorn
453
+ uvicorn.run(app, host="0.0.0.0", port=7777)