adowu commited on
Commit
b7b44c4
·
verified ·
1 Parent(s): 7d211df

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +358 -0
main.py CHANGED
@@ -1,5 +1,363 @@
1
  from __future__ import annotations
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  import os
4
  import json
5
  import time
 
1
  from __future__ import annotations
2
 
3
+ import os, json, time, uuid, asyncio, logging
4
+ from typing import Any, AsyncGenerator
5
+ from contextlib import asynccontextmanager
6
+
7
+ from dotenv import load_dotenv
8
+ from fastapi import FastAPI, HTTPException, Request, Depends
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from fastapi.responses import StreamingResponse, JSONResponse
11
+ from pydantic import BaseModel
12
+ from gradio_client import Client
13
+
14
+ load_dotenv()
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # Config
18
+ # ---------------------------------------------------------------------------
19
+ API_KEY = os.getenv("API_KEY", "")
20
+ HF_SPACE_URL = os.getenv("HF_SPACE_URL", "")
21
+ MODEL_ID = os.getenv("MODEL_ID", "")
22
+ DEFAULT_TEMP = float(os.getenv("DEFAULT_TEMPERATURE", "0.6"))
23
+ DEFAULT_TOP_P = float(os.getenv("DEFAULT_TOP_P", "0.95"))
24
+ DEFAULT_TOKENS = int(os.getenv("DEFAULT_MAX_TOKENS", "1024"))
25
+
26
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
27
+ log = logging.getLogger(__name__)
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Gradio client (singleton)
31
+ # ---------------------------------------------------------------------------
32
+ _client: Client | None = None
33
+
34
+ async def get_client() -> Client:
35
+ global _client
36
+ if _client is None:
37
+ log.info("Connecting to %s", HF_SPACE_URL)
38
+ _client = await asyncio.to_thread(Client, HF_SPACE_URL)
39
+ log.info("Connected.")
40
+ return _client
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # Pydantic schemas
44
+ # ---------------------------------------------------------------------------
45
+
46
+ class Message(BaseModel):
47
+ role: str
48
+ content: str | list[dict] = ""
49
+ name: str | None = None
50
+
51
+ class ChatCompletionRequest(BaseModel):
52
+ model: str = MODEL_ID
53
+ messages: list[Message]
54
+ temperature: float = DEFAULT_TEMP
55
+ top_p: float = DEFAULT_TOP_P
56
+ max_tokens: int = DEFAULT_TOKENS
57
+ stream: bool = False
58
+ frequency_penalty: float = 0
59
+ presence_penalty: float = 0
60
+ stop: str | list[str] | None = None
61
+ seed: int | None = None
62
+ user: str | None = None
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # Auth
66
+ # ---------------------------------------------------------------------------
67
+
68
+ async def verify_key(request: Request) -> None:
69
+ if not API_KEY:
70
+ return
71
+ auth = request.headers.get("Authorization", "")
72
+ if not auth.startswith("Bearer ") or auth[7:] != API_KEY:
73
+ raise HTTPException(status_code=401, detail="Invalid or missing API key")
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # Lifespan context manager (modern FastAPI pattern)
77
+ # ---------------------------------------------------------------------------
78
+
79
+ @asynccontextmanager
80
+ async def lifespan(app: FastAPI):
81
+ # Startup
82
+ log.info("Starting up - connecting to Gradio client...")
83
+ await get_client()
84
+ log.info("Startup complete.")
85
+ yield
86
+ # Shutdown (if needed)
87
+ log.info("Shutting down.")
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # App
91
+ # ---------------------------------------------------------------------------
92
+
93
+ app = FastAPI(
94
+ title="Falcon H1R API",
95
+ version="3.2.0",
96
+ lifespan=lifespan,
97
+ )
98
+
99
+ app.add_middleware(
100
+ CORSMiddleware,
101
+ allow_origins=["*"],
102
+ allow_credentials=True,
103
+ allow_methods=["*"],
104
+ allow_headers=["*"],
105
+ )
106
+
107
+ # ---------------------------------------------------------------------------
108
+ # Business logic
109
+ # ---------------------------------------------------------------------------
110
+
111
+ def _content_str(m: Message) -> str:
112
+ if isinstance(m.content, str):
113
+ return m.content
114
+ return "".join(p.get("text", "") for p in m.content if p.get("type") == "text")
115
+
116
+ def _build_prompt(messages: list[Message]) -> str:
117
+ """Flatten messages into a single prompt string."""
118
+ system, parts = [], []
119
+ for m in messages:
120
+ c = _content_str(m)
121
+ if m.role == "system": system.append(c)
122
+ elif m.role == "user": parts.append(c)
123
+ elif m.role == "assistant": parts.append(f"[ASSISTANT]\n{c}")
124
+ prefix = "[SYSTEM]\n" + "\n".join(system) + "\n[/SYSTEM]\n" if system else ""
125
+ return prefix + "\n".join(parts)
126
+
127
+ def _extract_text(result: Any) -> str:
128
+ """
129
+ Extract assistant reply from Gradio client.predict() result.
130
+
131
+ gradio_client returns either:
132
+ - tuple: (output1, output2, ...) where one element is the chatbot data
133
+ - object with .data attribute containing a list
134
+
135
+ We need to find the conversation list and extract the last message.
136
+ """
137
+ try:
138
+ # Handle both tuple and object with .data
139
+ if isinstance(result, tuple):
140
+ data = result
141
+ elif hasattr(result, 'data'):
142
+ data = result.data
143
+ else:
144
+ data = [result]
145
+
146
+ log.info("Raw result type: %s, length: %s", type(data).__name__, len(data) if hasattr(data, '__len__') else 'N/A')
147
+
148
+ # Search through all returned values for the conversation
149
+ conversation = None
150
+ for idx, item in enumerate(data):
151
+ log.debug("Item %d type: %s", idx, type(item).__name__)
152
+
153
+ # Check if this item is a dict with 'value' key (chatbot component)
154
+ if isinstance(item, dict) and "value" in item:
155
+ val = item["value"]
156
+ if isinstance(val, list) and val:
157
+ conversation = val
158
+ log.info("Found conversation in dict at index %d, length: %d", idx, len(val))
159
+ break
160
+
161
+ # Check if item itself is a list (direct conversation)
162
+ elif isinstance(item, list) and item:
163
+ # Verify it looks like a conversation (list of message dicts/tuples)
164
+ first = item[0]
165
+ if isinstance(first, (dict, list, tuple)):
166
+ conversation = item
167
+ log.info("Found conversation as list at index %d, length: %d", idx, len(item))
168
+ break
169
+
170
+ if conversation is None:
171
+ raise ValueError(f"Cannot find conversation in result. Data structure: {json.dumps(str(data)[:500])}")
172
+
173
+ # Extract last message
174
+ last = conversation[-1]
175
+ log.info("Last message type: %s, value: %s", type(last).__name__, str(last)[:200])
176
+
177
+ # Handle different message formats
178
+ content = None
179
+
180
+ # Format 1: dict with 'content' key (Gradio 4.x)
181
+ if isinstance(last, dict):
182
+ content = last.get("content", "")
183
+
184
+ # Format 2: tuple/list [user_msg, assistant_msg] (Gradio 3.x)
185
+ elif isinstance(last, (list, tuple)) and len(last) >= 2:
186
+ content = last[1] or ""
187
+
188
+ # Format 3: plain string
189
+ elif isinstance(last, str):
190
+ content = last
191
+
192
+ if content is None:
193
+ raise ValueError(f"Cannot extract content from last message: {last}")
194
+
195
+ # If content is a list of content blocks, extract text
196
+ if isinstance(content, list):
197
+ text_parts = []
198
+ for block in content:
199
+ if isinstance(block, dict):
200
+ if block.get("type") == "text":
201
+ text_parts.append(block.get("content", block.get("text", "")).strip())
202
+ elif isinstance(block, str):
203
+ text_parts.append(block.strip())
204
+ return "".join(text_parts)
205
+
206
+ return str(content).strip()
207
+
208
+ except Exception as e:
209
+ log.error("_extract_text failed: %s", e, exc_info=True)
210
+ log.error("Raw result dump: %s", str(result)[:1000])
211
+ raise ValueError(f"Failed to extract text: {e}") from e
212
+
213
+ async def _call_falcon(prompt: str, req: ChatCompletionRequest) -> str:
214
+ """
215
+ Call Falcon H1R via Gradio client.
216
+ """
217
+ client = await get_client()
218
+
219
+ settings = {
220
+ "model": req.model,
221
+ "temperature": req.temperature,
222
+ "max_new_tokens": req.max_tokens,
223
+ "top_p": req.top_p,
224
+ }
225
+
226
+ # Step 1: Reset chat
227
+ log.info("Resetting chat session...")
228
+ await asyncio.to_thread(
229
+ client.predict,
230
+ api_name="/new_chat"
231
+ )
232
+
233
+ # Step 2: Send message
234
+ log.info("Sending message to Falcon...")
235
+ result = await asyncio.to_thread(
236
+ client.predict,
237
+ input_value=prompt,
238
+ settings_form_value=settings,
239
+ api_name="/add_message"
240
+ )
241
+
242
+ log.info("Received result, extracting text...")
243
+ return _extract_text(result)
244
+
245
+ def _make_response(text: str, req: ChatCompletionRequest) -> dict:
246
+ pt = sum(len(_content_str(m)) for m in req.messages) // 4
247
+ ct = len(text) // 4
248
+ return {
249
+ "id": f"chatcmpl-{uuid.uuid4().hex}",
250
+ "object": "chat.completion",
251
+ "created": int(time.time()),
252
+ "model": req.model,
253
+ "system_fingerprint": f"fp_{uuid.uuid4().hex[:8]}",
254
+ "choices": [{
255
+ "index": 0,
256
+ "message": {
257
+ "role": "assistant",
258
+ "content": text,
259
+ "tool_calls": None,
260
+ "function_call": None,
261
+ },
262
+ "finish_reason": "stop",
263
+ "logprobs": None,
264
+ }],
265
+ "usage": {
266
+ "prompt_tokens": pt,
267
+ "completion_tokens": ct,
268
+ "total_tokens": pt + ct,
269
+ },
270
+ }
271
+
272
+ async def _stream_sse(text: str, req: ChatCompletionRequest) -> AsyncGenerator[str, None]:
273
+ """Simulate streaming by chunking the full response."""
274
+ cid = f"chatcmpl-{uuid.uuid4().hex}"
275
+ created = int(time.time())
276
+
277
+ # Stream in small chunks
278
+ for i in range(0, len(text), 6):
279
+ chunk = {
280
+ "id": cid,
281
+ "object": "chat.completion.chunk",
282
+ "created": created,
283
+ "model": req.model,
284
+ "choices": [{
285
+ "index": 0,
286
+ "delta": {"role": "assistant", "content": text[i:i+6]},
287
+ "finish_reason": None,
288
+ }],
289
+ }
290
+ yield f"data: {json.dumps(chunk)}\n\n"
291
+ await asyncio.sleep(0.01)
292
+
293
+ # Final chunk
294
+ pt = sum(len(_content_str(m)) for m in req.messages) // 4
295
+ ct = len(text) // 4
296
+ final = {
297
+ "id": cid,
298
+ "object": "chat.completion.chunk",
299
+ "created": created,
300
+ "model": req.model,
301
+ "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
302
+ "usage": {"prompt_tokens": pt, "completion_tokens": ct, "total_tokens": pt + ct},
303
+ }
304
+ yield f"data: {json.dumps(final)}\n\n"
305
+ yield "data: [DONE]\n\n"
306
+
307
+ # ---------------------------------------------------------------------------
308
+ # Routes
309
+ # ---------------------------------------------------------------------------
310
+
311
+ @app.get("/")
312
+ async def root():
313
+ return {
314
+ "service": "FOC API",
315
+ "version": "3.2.0",
316
+ "endpoints": {
317
+ "health": "/health",
318
+ "models": "/v1/models",
319
+ "chat": "/v1/chat/completions",
320
+ },
321
+ }
322
+
323
+ @app.get("/health")
324
+ async def health():
325
+ return {"status": "ok", "model": MODEL_ID, "space": HF_SPACE_URL}
326
+
327
+ @app.get("/v1/models")
328
+ async def list_models(_: None = Depends(verify_key)):
329
+ return {"object": "list", "data": [{
330
+ "id": MODEL_ID,
331
+ "object": "model",
332
+ "created": 1710000000,
333
+ "owned_by": "tiiuae",
334
+ }]}
335
+
336
+ @app.post("/v1/chat/completions")
337
+ async def chat_completions(req: ChatCompletionRequest, _: None = Depends(verify_key)):
338
+ prompt = _build_prompt(req.messages)
339
+ log.info("Request | model=%s temp=%.2f tokens=%d stream=%s",
340
+ req.model, req.temperature, req.max_tokens, req.stream)
341
+
342
+ try:
343
+ text = await _call_falcon(prompt, req)
344
+ except Exception as exc:
345
+ log.exception("Falcon call failed")
346
+ raise HTTPException(status_code=502, detail=f"Upstream error: {exc}") from exc
347
+
348
+ if req.stream:
349
+ return StreamingResponse(
350
+ _stream_sse(text, req),
351
+ media_type="text/event-stream",
352
+ headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
353
+ )
354
+
355
+ return JSONResponse(content=_make_response(text, req))
356
+
357
+
358
+
359
+ zobacz jak było w pierwszej powyższej wersji kodu, wtedy działało, a nastepnie wprowadz odpowiednie poprawki w ponizszym kodzie i podaj caly kod po modyfikacji: from __future__ import annotations
360
+
361
  import os
362
  import json
363
  import time