Hivra commited on
Commit
483a113
·
verified ·
1 Parent(s): 7eba146

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +195 -165
app.py CHANGED
@@ -1,36 +1,30 @@
1
  from fastapi import FastAPI, HTTPException, Request
2
  from fastapi.responses import JSONResponse, StreamingResponse
3
  from fastapi.middleware.cors import CORSMiddleware
4
- import httpx
5
  import json
6
  import os
7
  import time
8
  import asyncio
9
- import logging
10
- from typing import AsyncGenerator
11
-
12
- # Configure logging
13
- logging.basicConfig(level=logging.INFO)
14
- logger = logging.getLogger(__name__)
15
 
16
  app = FastAPI()
17
 
18
- # CORS Configuration
19
  app.add_middleware(
20
  CORSMiddleware,
21
- allow_origins=["*"],
22
  allow_credentials=True,
23
  allow_methods=["*"],
24
  allow_headers=["*"],
25
  )
26
 
27
- # Environment Configuration
28
  STATUS_URL = os.environ.get("STATUS_URL", "https://duckduckgo.com/duckchat/v1/status")
29
  CHAT_URL = os.environ.get("CHAT_URL", "https://duckduckgo.com/duckchat/v1/chat")
30
  REFERER = os.environ.get("REFERER", "https://duckduckgo.com/")
31
  ORIGIN = os.environ.get("ORIGIN", "https://duckduckgo.com")
32
  USER_AGENT = os.environ.get("USER_AGENT", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36")
33
- COOKIE = os.environ.get("COOKIE", "dcm=3; s=l; bf=1")
34
 
35
  DEFAULT_HEADERS = {
36
  "User-Agent": USER_AGENT,
@@ -48,184 +42,220 @@ DEFAULT_HEADERS = {
48
  "TE": "trailers",
49
  }
50
 
51
- SUPPORTED_MODELS = ["o3-mini", "gpt-4o-mini", "claude-3-haiku-20240307", "meta-llama/Llama-3.3-70B-Instruct-Turbo"]
52
- TIMEOUT = 30.0 # Seconds
 
 
 
 
53
 
54
- class StreamClosedError(Exception):
55
- pass
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
- async def get_vqd() -> str:
58
- """Fetch and validate DuckDuckGo authentication token."""
59
  try:
60
- async with httpx.AsyncClient() as client:
61
- response = await client.get(
62
- STATUS_URL,
63
- headers={**DEFAULT_HEADERS, "x-vqd-accept": "1"},
64
- timeout=10.0
65
- )
66
- response.raise_for_status()
67
-
68
- if "x-vqd-4" not in response.headers:
69
- logger.error("Missing x-vqd-4 header in response")
70
- raise ValueError("Authentication failed: Missing VQD token")
71
-
72
- return response.headers["x-vqd-4"]
73
-
74
- except httpx.HTTPStatusError as e:
75
- logger.error(f"VQD fetch failed: {e.response.text}")
76
- raise HTTPException(status_code=502, detail="Upstream authentication service unavailable")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  except Exception as e:
78
- logger.exception("Critical VQD error")
79
- raise HTTPException(status_code=500, detail="Internal authentication error")
80
 
81
- async def stream_generator(response: httpx.Response) -> AsyncGenerator[str, None]:
82
- """Handle streaming response with proper buffer management."""
83
- buffer = ""
84
  try:
85
- async for chunk in response.aiter_text():
86
- buffer += chunk
87
-
88
- while "\n\n" in buffer:
89
- event, buffer = buffer.split("\n\n", 1)
90
- for line in event.strip().split("\n"):
91
- if not line.startswith("data: "):
92
- continue
93
-
 
 
 
 
 
 
 
 
 
 
 
94
  try:
95
- data = json.loads(line[5:])
96
- if error := data.get("error"):
97
- logger.error(f"Upstream error: {error}")
98
- yield format_error_chunk(error)
99
- return
100
-
101
- if message := data.get("message", ""):
102
- yield format_openai_chunk(message, "gpt-4o-mini")
103
- await asyncio.sleep(0.001)
104
-
105
- except json.JSONDecodeError:
106
- logger.warning(f"Invalid JSON line: {line[:100]}")
107
- yield format_error_chunk("Invalid response format")
108
- return
109
- except Exception as e:
110
- logger.error(f"Stream processing error: {str(e)}")
111
- yield format_error_chunk("Stream processing failed")
112
- return
113
-
114
- except httpx.RemoteProtocolError:
115
- logger.info("Connection closed by server")
116
- except Exception as e:
117
- logger.error(f"Unexpected stream error: {str(e)}")
118
- yield format_error_chunk("Stream connection failed")
119
- finally:
120
- yield "data: [DONE]\n\n"
121
 
122
- async def duckduckgo_chat_stream(model: str, messages: list) -> StreamingResponse:
123
- """Robust streaming handler with connection monitoring."""
124
- try:
125
- logger.info(f"Initiating stream for model: {model}")
126
- vqd_token = await get_vqd()
127
-
128
- async with httpx.AsyncClient() as client:
129
- response = await client.post(
130
- CHAT_URL,
131
- headers={
132
- **DEFAULT_HEADERS,
133
- "x-vqd-4": vqd_token,
134
- "Accept": "text/event-stream",
135
- },
136
- json={"model": model, "messages": messages},
137
- timeout=TIMEOUT
138
- )
139
- response.raise_for_status()
140
-
141
- return StreamingResponse(
142
- stream_generator(response),
143
- media_type="text/event-stream",
144
- headers={
145
- "Cache-Control": "no-cache",
146
- "X-Stream-ID": f"stream_{int(time.time())}",
147
- }
148
- )
149
-
150
- except httpx.HTTPStatusError as e:
151
- logger.error(f"Upstream API error: {e.response.text}")
152
- raise HTTPException(status_code=502, detail="Upstream service unavailable")
153
  except Exception as e:
154
- logger.exception("Stream setup failed")
155
- raise HTTPException(status_code=500, detail="Stream initialization failed")
156
-
157
- def format_openai_chunk(content: str, model: str) -> str:
158
- """Generate OpenAI-compatible SSE chunk."""
159
- return json.dumps({
160
- "id": f"chatcmpl-{int(time.time()*1000)}",
161
- "object": "chat.completion.chunk",
162
- "created": int(time.time()),
163
- "model": model,
164
- "choices": [{
165
- "delta": {"content": content},
166
- "index": 0,
167
- "finish_reason": None
168
- }]
169
- }) + "\n\n"
170
-
171
- def format_error_chunk(message: str) -> str:
172
- """Format error messages for SSE stream."""
173
- return json.dumps({
174
- "error": message,
175
- "code": "STREAM_ERROR"
176
- }) + "\n\n"
177
 
178
  @app.post("/v1/chat/completions")
179
  async def chat_completions(request: Request):
180
  try:
181
- data = await request.json()
182
- model = data.get("model", "o3-mini")
183
- messages = data.get("messages", [])
184
- stream = data.get("stream", False)
185
 
186
- # Validate input
187
  if model not in SUPPORTED_MODELS:
188
- raise HTTPException(400, f"Unsupported model: {model}")
189
- if not messages or not isinstance(messages, list):
190
- raise HTTPException(400, "Invalid messages format")
191
-
192
- # Process messages
193
- last_message = messages[-1]
194
- if last_message.get("role") != "user":
195
- raise HTTPException(400, "Last message must be from user")
196
-
197
- # Build payload (simplified for example)
198
- payload = [{
199
- "role": "user",
200
- "content": "\n".join(
201
- f"{m['role']}: {m['content']}"
202
- for m in messages
203
  )
204
- }]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
 
206
  if stream:
207
- return await duckduckgo_chat_stream(model, payload)
208
  else:
209
- # Non-streaming implementation
210
- raise HTTPException(501, "Non-streaming mode temporarily disabled")
211
 
212
- except HTTPException:
213
- raise
214
- except Exception as e:
215
- logger.exception("API handler error")
216
- raise HTTPException(500, "Internal server error")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
 
218
- @app.get("/health")
219
- async def health_check():
220
- return {"status": "ok", "timestamp": int(time.time())}
 
 
221
 
222
  @app.exception_handler(HTTPException)
223
- async def http_error_handler(request: Request, exc: HTTPException):
224
  return JSONResponse(
225
  status_code=exc.status_code,
226
- content={"error": exc.detail},
227
  )
228
 
229
- if __name__ == "__main__":
230
- import uvicorn
231
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
  from fastapi import FastAPI, HTTPException, Request
2
  from fastapi.responses import JSONResponse, StreamingResponse
3
  from fastapi.middleware.cors import CORSMiddleware
4
+ import requests
5
  import json
6
  import os
7
  import time
8
  import asyncio
 
 
 
 
 
 
9
 
10
  app = FastAPI()
11
 
12
+ # CORS settings
13
  app.add_middleware(
14
  CORSMiddleware,
15
+ allow_origins=["*"], # Allow all origins. In production, change this to specific domains.
16
  allow_credentials=True,
17
  allow_methods=["*"],
18
  allow_headers=["*"],
19
  )
20
 
21
+ # Environment variable configuration
22
  STATUS_URL = os.environ.get("STATUS_URL", "https://duckduckgo.com/duckchat/v1/status")
23
  CHAT_URL = os.environ.get("CHAT_URL", "https://duckduckgo.com/duckchat/v1/chat")
24
  REFERER = os.environ.get("REFERER", "https://duckduckgo.com/")
25
  ORIGIN = os.environ.get("ORIGIN", "https://duckduckgo.com")
26
  USER_AGENT = os.environ.get("USER_AGENT", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36")
27
+ COOKIE = os.environ.get("COOKIE", "dcm=3; s=l; bf=1") # Get cookie from environment variable
28
 
29
  DEFAULT_HEADERS = {
30
  "User-Agent": USER_AGENT,
 
42
  "TE": "trailers",
43
  }
44
 
45
+ SUPPORTED_MODELS = [
46
+ "o3-mini",
47
+ "gpt-4o-mini",
48
+ "claude-3-haiku-20240307",
49
+ "meta-llama/Llama-3.3-70B-Instruct-Turbo",
50
+ ]
51
 
52
+ async def get_vqd():
53
+ """Get the VQD value for DuckDuckGo Chat."""
54
+ headers = {**DEFAULT_HEADERS, "x-vqd-accept": "1"}
55
+ try:
56
+ response = requests.get(STATUS_URL, headers=headers)
57
+ response.raise_for_status() # Raise HTTPError if status code is not 200
58
+ vqd = response.headers.get("x-vqd-4")
59
+ if not vqd:
60
+ raise ValueError("x-vqd-4 header not found in the response.")
61
+ return vqd
62
+ except requests.exceptions.RequestException as e:
63
+ raise HTTPException(status_code=500, detail=f"HTTP request failed: {e}")
64
+ except ValueError as e:
65
+ raise HTTPException(status_code=500, detail=str(e))
66
 
67
+ async def duckduckgo_chat_stream(model, messages):
68
+ """Interact with DuckDuckGo Chat with streaming output."""
69
  try:
70
+ x_vqd_4 = await get_vqd()
71
+
72
+ chat_headers = {
73
+ **DEFAULT_HEADERS,
74
+ "x-vqd-4": x_vqd_4,
75
+ "Accept": "text/event-stream", # Ensure we accept SSE
76
+ }
77
+
78
+ body = json.dumps({
79
+ "model": model,
80
+ "messages": messages,
81
+ })
82
+
83
+ response = requests.post(CHAT_URL, headers=chat_headers, data=body, stream=True)
84
+ response.raise_for_status()
85
+
86
+ async def event_stream():
87
+ try:
88
+ for line in response.iter_lines():
89
+ if line:
90
+ decoded_line = line.decode('utf-8')
91
+ if decoded_line.startswith("data: "):
92
+ content = decoded_line[5:].strip()
93
+ # Check if this is the final marker
94
+ if content == "[DONE]":
95
+ yield f"data: [DONE]\n\n"
96
+ break
97
+ try:
98
+ json_data = json.loads(content)
99
+ message_content = json_data.get("message", "")
100
+ if message_content:
101
+ # Build OpenAI style streaming response
102
+ openai_stream_response = {
103
+ "id": f"chatcmpl-{int(time.time() * 1000)}",
104
+ "object": "chat.completion.chunk",
105
+ "created": int(time.time()),
106
+ "model": model,
107
+ "choices": [
108
+ {
109
+ "delta": {"content": message_content},
110
+ "index": 0,
111
+ "finish_reason": None,
112
+ }
113
+ ],
114
+ }
115
+ yield f"data: {json.dumps(openai_stream_response)}\n\n"
116
+ await asyncio.sleep(0.01) # Prevent high CPU usage
117
+ except json.JSONDecodeError as e:
118
+ print(f"JSON decode error: {e}, line: {decoded_line}")
119
+ yield f"data: {json.dumps({'error': 'JSON decode error'})}\n\n"
120
+ break # Stop the stream
121
+ except requests.exceptions.RequestException as e:
122
+ print(f"Request error: {e}")
123
+ yield f"data: {json.dumps({'error': 'Request error'})}\n\n"
124
+ except Exception as e:
125
+ print(f"An error occurred: {e}")
126
+ yield f"data: {json.dumps({'error': 'An error occurred'})}\n\n"
127
+ finally:
128
+ yield "data: [DONE]\n\n" # End SSE stream
129
+
130
+ return StreamingResponse(event_stream(), media_type="text/event-stream")
131
+
132
+ except requests.exceptions.RequestException as e:
133
+ raise HTTPException(status_code=500, detail=f"HTTP request failed: {e}")
134
  except Exception as e:
135
+ raise HTTPException(status_code=500, detail=f"Error during chat: {e}")
 
136
 
137
+ async def duckduckgo_chat_non_stream(model, messages):
138
+ """Interact with DuckDuckGo Chat without streaming output."""
 
139
  try:
140
+ x_vqd_4 = await get_vqd()
141
+
142
+ chat_headers = {
143
+ **DEFAULT_HEADERS,
144
+ "x-vqd-4": x_vqd_4,
145
+ }
146
+
147
+ body = json.dumps({
148
+ "model": model,
149
+ "messages": messages,
150
+ })
151
+
152
+ response = requests.post(CHAT_URL, headers=chat_headers, data=body)
153
+ response.raise_for_status()
154
+
155
+ full_message = ""
156
+ for line in response.iter_lines():
157
+ if line:
158
+ decoded_line = line.decode('utf-8')
159
+ if decoded_line.startswith("data: "):
160
  try:
161
+ json_data = json.loads(decoded_line[5:])
162
+ full_message += json_data.get("message", "")
163
+ except json.JSONDecodeError as e:
164
+ print(f"JSON decode error: {e}, line: {decoded_line}")
165
+ pass # Ignore decoding errors
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
+ return full_message
168
+
169
+ except requests.exceptions.RequestException as e:
170
+ raise HTTPException(status_code=500, detail=f"HTTP request failed: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  except Exception as e:
172
+ raise HTTPException(status_code=500, detail=f"Error during chat: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
  @app.post("/v1/chat/completions")
175
  async def chat_completions(request: Request):
176
  try:
177
+ body = await request.json()
178
+ if not body:
179
+ raise HTTPException(status_code=400, detail="Invalid request body")
 
180
 
181
+ model = body.get("model", "o3-mini")
182
  if model not in SUPPORTED_MODELS:
183
+ raise HTTPException(
184
+ status_code=400,
185
+ detail=f"Model \"{model}\" is not supported. Supported models are: {', '.join(SUPPORTED_MODELS)}."
 
 
 
 
 
 
 
 
 
 
 
 
186
  )
187
+
188
+ messages = body.get("messages")
189
+ if not messages:
190
+ raise HTTPException(status_code=400, detail="No message content provided")
191
+
192
+ stream = body.get("stream", False) # Get the stream parameter, default is False
193
+
194
+ # Process system message
195
+ system_message = next((msg for msg in messages if msg.get("role") == "system"), None)
196
+ system_prompt = f"You will play the role of a {system_message['content']}.\n" if system_message else ""
197
+
198
+ # Extract and format the conversation history
199
+ history_messages = "\n".join(
200
+ f"{msg['role']}: {msg['content']}"
201
+ for msg in messages
202
+ if msg.get("role") != "system" and msg != messages[-1]
203
+ )
204
+
205
+ # Get the last user message
206
+ last_user_message = messages[-1]
207
+ current_question = last_user_message["content"] if last_user_message.get("role") == "user" else ""
208
+
209
+ # Build the combined message
210
+ combined_message_content = (
211
+ f"{system_prompt}Below is the conversation history:\n{history_messages}\n"
212
+ f"User's current question: {current_question}"
213
+ )
214
+ combined_message = {"role": "user", "content": combined_message_content}
215
 
216
  if stream:
217
+ return await duckduckgo_chat_stream(model, [combined_message])
218
  else:
219
+ response_text = await duckduckgo_chat_non_stream(model, [combined_message])
 
220
 
221
+ # Build OpenAI style response
222
+ openai_response = {
223
+ "id": f"chatcmpl-{int(time.time() * 1000)}", # Unique ID
224
+ "object": "chat.completion",
225
+ "created": int(time.time()),
226
+ "model": model,
227
+ "choices": [
228
+ {
229
+ "message": {
230
+ "role": "assistant",
231
+ "content": response_text,
232
+ },
233
+ "finish_reason": "stop",
234
+ "index": 0,
235
+ },
236
+ ],
237
+ "usage": {
238
+ "prompt_tokens": 0,
239
+ "completion_tokens": 0,
240
+ "total_tokens": 0
241
+ },
242
+ }
243
+
244
+ return JSONResponse(content=openai_response)
245
 
246
+ except HTTPException as e:
247
+ raise e # Re-raise HTTPException so FastAPI can handle it
248
+ except Exception as e:
249
+ print(f"API error: {e}")
250
+ raise HTTPException(status_code=500, detail=f"Internal server error: {e}")
251
 
252
  @app.exception_handler(HTTPException)
253
+ async def http_exception_handler(request: Request, exc: HTTPException):
254
  return JSONResponse(
255
  status_code=exc.status_code,
256
+ content={"detail": exc.detail},
257
  )
258
 
259
+ @app.get("/")
260
+ async def greet_json():
261
+ return {"Hello": "World!"}