Simonc-44 commited on
Commit
8cd74d0
·
verified ·
1 Parent(s): fd1f3c0

Create main.py

Browse files
Files changed (1) hide show
  1. main.py +155 -0
main.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException, Header, Depends
2
+ from pydantic import BaseModel
3
+ import requests
4
+ import os
5
+ import json
6
+ from typing import Optional, Dict
7
+
8
+ app = FastAPI(title="CygnisAI Studio API")
9
+
10
+ # --- CONFIGURATION ---
11
+ # Token HF pour appeler les modèles (à configurer dans les Secrets du Space)
12
+ HF_TOKEN = os.environ.get("HF_TOKEN")
13
+
14
+ # Clé API statique pour sécuriser VOTRE API (à configurer dans les Secrets du Space)
15
+ # Par défaut pour le test local :
16
+ CYGNIS_API_KEY = os.environ.get("CYGNIS_API_KEY", "cgn_live_stable_demo_api_key_012345")
17
+
18
+ # Mapping des modèles demandés vers les endpoints réels Hugging Face
19
+ # Note: J'ai mappé vers les modèles réels les plus proches car Llama 4 / Gemma 3 n'existent pas encore publiquement.
20
+ # Vous pourrez mettre à jour ces IDs dès leur sortie.
21
+ MODELS = {
22
+ "google/gemma-3-27b-it": "google/gemma-2-27b-it", # Fallback Gemma 2
23
+ "openai/gpt-oss-120b": "meta-llama/Meta-Llama-3.1-70B-Instruct", # Fallback Llama 3.1 70B (puissant)
24
+ "Qwen/Qwen3-VL-8B-Thinking": "Qwen/Qwen2-VL-7B-Instruct", # Fallback Qwen 2 VL
25
+ "XiaomiMiMo/MiMo-V2-Flash": "Xiaomi/MIMO", # Fallback Xiaomi
26
+ "deepseek-ai/DeepSeek-V3.2": "deepseek-ai/DeepSeek-V3", # Fallback V3
27
+ "meta-llama/Llama-4-Scout-17B-16E-Instruct": "meta-llama/Meta-Llama-3.1-8B-Instruct", # Fallback Llama 3.1
28
+ "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16": "nvidia/Llama-3.1-Nemotron-70B-Instruct-HF", # Fallback Nemotron
29
+ "default": "meta-llama/Meta-Llama-3-8B-Instruct"
30
+ }
31
+
32
+ # URL de base du routeur d'inférence HF
33
+ HF_INFERENCE_BASE = "https://router.huggingface.co/hf-inference/models"
34
+
35
+ # --- SCHEMAS ---
36
+ class ChatRequest(BaseModel):
37
+ question: str
38
+ model: Optional[str] = "default"
39
+ system_prompt: Optional[str] = None
40
+ temperature: Optional[float] = 0.7
41
+ max_tokens: Optional[int] = 1024
42
+
43
+ class ChatResponse(BaseModel):
44
+ answer: str
45
+ model_used: str
46
+ sources: list = []
47
+
48
+ # --- SECURITE ---
49
+ async def verify_api_key(authorization: str = Header(None)):
50
+ if not authorization:
51
+ raise HTTPException(status_code=401, detail="Missing Authorization header")
52
+
53
+ try:
54
+ scheme, token = authorization.split()
55
+ if scheme.lower() != 'bearer':
56
+ raise HTTPException(status_code=401, detail="Invalid authentication scheme")
57
+
58
+ if token != CYGNIS_API_KEY:
59
+ raise HTTPException(status_code=403, detail="Invalid API Key")
60
+
61
+ except ValueError:
62
+ raise HTTPException(status_code=401, detail="Invalid authorization header format")
63
+
64
+ # --- ENDPOINTS ---
65
+
66
+ @app.get("/")
67
+ def read_root():
68
+ return {"status": "online", "service": "CygnisAI Studio API"}
69
+
70
+ @app.post("/api/ask", response_model=ChatResponse)
71
+ async def ask_model(req: ChatRequest, authorized: bool = Depends(verify_api_key)):
72
+ if not HF_TOKEN:
73
+ print("⚠️ WARNING: HF_TOKEN not set. Calls to HF will fail.")
74
+
75
+ # 1. Sélection du modèle
76
+ model_id = MODELS.get(req.model, MODELS["default"])
77
+ api_url = f"{HF_INFERENCE_BASE}/{model_id}"
78
+
79
+ print(f"🤖 Routing request to: {model_id}")
80
+
81
+ # 2. Construction du prompt
82
+ # On utilise le format standard chat template si possible, sinon raw text
83
+ messages = []
84
+ if req.system_prompt:
85
+ messages.append({"role": "system", "content": req.system_prompt})
86
+ messages.append({"role": "user", "content": req.question})
87
+
88
+ payload = {
89
+ "model": model_id,
90
+ "messages": messages,
91
+ "max_tokens": req.max_tokens,
92
+ "temperature": req.temperature,
93
+ "stream": False
94
+ }
95
+
96
+ headers = {
97
+ "Authorization": f"Bearer {HF_TOKEN}",
98
+ "Content-Type": "application/json"
99
+ }
100
+
101
+ try:
102
+ # 3. Appel à Hugging Face (Endpoint compatible OpenAI)
103
+ # Note: router.huggingface.co supporte souvent /v1/chat/completions
104
+ # Si ça échoue, on tentera l'appel direct
105
+ hf_chat_url = f"{HF_INFERENCE_BASE}/{model_id}/v1/chat/completions"
106
+
107
+ response = requests.post(hf_chat_url, headers=headers, json=payload)
108
+
109
+ # Fallback si le endpoint OpenAI n'est pas supporté pour ce modèle
110
+ if response.status_code == 404:
111
+ print("🔄 Fallback to standard inference API")
112
+ # Pour l'API standard, on doit souvent envoyer une string unique
113
+ # Ceci est une simplification, idéalement on utiliserait le tokenizer du modèle
114
+ prompt_str = f"System: {req.system_prompt}\nUser: {req.question}\nAssistant:" if req.system_prompt else f"User: {req.question}\nAssistant:"
115
+
116
+ payload_standard = {
117
+ "inputs": prompt_str,
118
+ "parameters": {
119
+ "max_new_tokens": req.max_tokens,
120
+ "temperature": req.temperature,
121
+ "return_full_text": False
122
+ }
123
+ }
124
+ response = requests.post(api_url, headers=headers, json=payload_standard)
125
+
126
+ if response.status_code != 200:
127
+ print(f"❌ HF Error ({response.status_code}): {response.text}")
128
+ raise HTTPException(status_code=502, detail=f"Model provider error: {response.text}")
129
+
130
+ data = response.json()
131
+
132
+ # Parsing de la réponse (gère les deux formats possibles)
133
+ answer = ""
134
+ if "choices" in data and len(data["choices"]) > 0:
135
+ answer = data["choices"][0]["message"]["content"]
136
+ elif isinstance(data, list) and len(data) > 0 and "generated_text" in data[0]:
137
+ answer = data[0]["generated_text"]
138
+ elif "generated_text" in data:
139
+ answer = data["generated_text"]
140
+ else:
141
+ answer = "Error: Could not parse model response."
142
+
143
+ return {
144
+ "answer": answer,
145
+ "model_used": model_id,
146
+ "sources": []
147
+ }
148
+
149
+ except Exception as e:
150
+ print(f"❌ Internal Error: {str(e)}")
151
+ raise HTTPException(status_code=500, detail=str(e))
152
+
153
+ if __name__ == "__main__":
154
+ import uvicorn
155
+ uvicorn.run(app, host="0.0.0.0", port=7860)