Dama12 commited on
Commit
c68925d
·
1 Parent(s): e01ba40

fix: Add chat method to K2ThinkEngine for scientific_chat route

Browse files
HF_PRIVATE_BACKEND.md ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🔒 Rapport : Mise en Privé du Backend Hugging Face Space
2
+
3
+ **Date** : 21 juin 2026
4
+ **Projet** : AI Scientific Co-Investigator
5
+ **Backend HF Space** : `dama12-scientific-backend.hf.space`
6
+ **Frontend** : Next.js — `ai-scientific-coinvestigator-frontend`
7
+
8
+ ---
9
+
10
+ ## 1. Contexte
11
+
12
+ Le backend FastAPI est actuellement déployé sur Hugging Face Spaces en mode **public**. L'objectif est de le passer en **privé** pour améliorer la sécurité, tout en maintenant l'accès depuis le frontend Next.js.
13
+
14
+ ---
15
+
16
+ ## 2. État actuel du Frontend
17
+
18
+ ### Fichiers clés analysés
19
+
20
+ | Fichier | Rôle |
21
+ |---------|------|
22
+ | [`src/lib/api/client.ts`](file:///c:/Users/user/Desktop/ai-scientific-coinvestigator-frontend/src/lib/api/client.ts) | Client Axios centralisé — **seul fichier à modifier** |
23
+ | [`.env.local`](file:///c:/Users/user/Desktop/ai-scientific-coinvestigator-frontend/.env.local) | Variables d'environnement locales |
24
+ | [`.env.example`](file:///c:/Users/user/Desktop/ai-scientific-coinvestigator-frontend/.env.example) | Template de configuration |
25
+
26
+ ### Architecture actuelle des appels API
27
+
28
+ ```
29
+ Frontend (Next.js)
30
+
31
+ ├── axios instance (client.ts)
32
+ │ ├── baseURL → NEXT_PUBLIC_API_URL (HF Space public)
33
+ │ └── intercepteur → ajoute JWT user en Authorization header
34
+
35
+ └── → Backend HF Space (public, aucune restriction)
36
+ ```
37
+
38
+ ### Gestion d'auth déjà présente
39
+
40
+ Le frontend gère déjà un token JWT pour authentifier les **utilisateurs de l'app** :
41
+
42
+ ```typescript
43
+ // client.ts — intercepteur existant
44
+ apiClient.interceptors.request.use((config) => {
45
+ const token = localStorage.getItem('token'); // JWT user
46
+ if (token && config.headers) {
47
+ config.headers.Authorization = `Bearer ${token}`;
48
+ }
49
+ return config;
50
+ });
51
+ ```
52
+
53
+ ---
54
+
55
+ ## 3. Ce que change un Space Privé HF
56
+
57
+ Quand un Hugging Face Space est en **privé** :
58
+
59
+ - ❌ Tout appel HTTP sans token HF valide reçoit une **erreur 401**
60
+ - ✅ Les appels avec `Authorization: Bearer hf_xxxxx` fonctionnent normalement
61
+ - ✅ L'API du Space reste identique — seule l'authentification d'accès change
62
+
63
+ > [!IMPORTANT]
64
+ > Le token HF requis pour accéder au Space **privé** est différent du JWT JWT applicatif utilisé pour authentifier les users de l'app. Ce sont deux niveaux d'authentification distincts.
65
+
66
+ ---
67
+
68
+ ## 4. Problème : Conflit de Headers
69
+
70
+ Le frontend utilise déjà `Authorization: Bearer <JWT>` pour l'auth utilisateur.
71
+ Le Space privé HF exige `Authorization: Bearer <hf_token>` pour l'accès.
72
+
73
+ **Deux approches pour résoudre ce conflit :**
74
+
75
+ ### Option A — Header custom HF (Recommandée ✅)
76
+
77
+ Hugging Face accepte le header `Authorization` standard pour son token.
78
+ On peut utiliser un header séparé côté backend pour ne pas interférer.
79
+
80
+ ```
81
+ Frontend → HF Space : Authorization: Bearer hf_xxxx (accès au Space)
82
+ Frontend → App API : X-User-Token: Bearer <JWT> (auth utilisateur)
83
+ ```
84
+
85
+ Mais cela **nécessite de modifier le backend** pour lire le JWT depuis un header custom.
86
+
87
+ ---
88
+
89
+ ### Option B — Proxy Next.js (Recommandée ✅ si pas de modif backend)
90
+
91
+ Créer des **API Routes Next.js** comme proxy intermédiaire :
92
+
93
+ ```
94
+ Navigateur → Next.js API Route → HF Space (avec token HF en secret serveur)
95
+ ```
96
+
97
+ Avantages :
98
+ - Le token HF n'est **jamais exposé** côté client (secret serveur)
99
+ - Le JWT user est géré séparément par le Next.js proxy
100
+ - Aucune modification backend requise
101
+
102
+ ```typescript
103
+ // pages/api/proxy/[...path].ts (NOUVEAU)
104
+ export default async function handler(req, res) {
105
+ const response = await fetch(`${process.env.HF_SPACE_URL}${req.url}`, {
106
+ headers: {
107
+ 'Authorization': `Bearer ${process.env.HF_TOKEN}`, // secret serveur
108
+ 'X-User-Token': req.headers['x-user-token'], // JWT user transmis
109
+ },
110
+ });
111
+ // ...
112
+ }
113
+ ```
114
+
115
+ ---
116
+
117
+ ### Option C — Token HF côté client (Simple mais risquée ⚠️)
118
+
119
+ Injecter le token HF directement dans les headers du client Axios :
120
+
121
+ ```typescript
122
+ // client.ts modifié
123
+ const apiClient = axios.create({
124
+ headers: {
125
+ 'Content-Type': 'application/json',
126
+ 'X-HF-Space-Token': process.env.NEXT_PUBLIC_HF_TOKEN, // exposé !
127
+ },
128
+ });
129
+ ```
130
+
131
+ > [!WARNING]
132
+ > `NEXT_PUBLIC_*` variables sont **visibles dans le bundle JS** du navigateur. Le token HF serait exposé publiquement — cela ne sécurise pas vraiment l'accès.
133
+
134
+ ---
135
+
136
+ ## 5. Tableau Comparatif des Options
137
+
138
+ | Critère | Option A (Header custom) | Option B (Proxy Next.js) | Option C (Client-side) |
139
+ |---------|--------------------------|--------------------------|------------------------|
140
+ | Sécurité du token HF | ✅ Serveur | ✅ Serveur | ❌ Exposé client |
141
+ | Modif backend requise | ✅ Oui | ❌ Non | ❌ Non |
142
+ | Complexité | Moyenne | Moyenne | Faible |
143
+ | WebSocket (WSS) supporté | ✅ | ⚠️ Complexe | ✅ |
144
+ | Recommandation | Si backend accessible | **Meilleure option** | À éviter |
145
+
146
+ ---
147
+
148
+ ## 6. Impact sur les WebSockets
149
+
150
+ Le frontend utilise aussi `NEXT_PUBLIC_WS_URL=wss://dama12-scientific-backend.hf.space`.
151
+
152
+ > [!CAUTION]
153
+ > Les WebSockets dans un Space HF privé **ne supportent pas** le header `Authorization` standard (limitation du protocole WS). Il faudra passer le token HF en **query parameter** lors de la connexion WS :
154
+ > ```
155
+ > wss://dama12-scientific-backend.hf.space?token=hf_xxxx
156
+ > ```
157
+ > Ce qui expose aussi le token dans les logs. À évaluer selon l'usage réel des WebSockets dans l'app.
158
+
159
+ ---
160
+
161
+ ## 7. Plan d'Action Recommandé
162
+
163
+ ```
164
+ Étape 1 — Rendre le Space privé sur HF
165
+ └── Settings → Space visibility → Private
166
+
167
+ Étape 2 — Générer un token HF dédié
168
+ └── HF Settings → Access Tokens → New token (role: read)
169
+
170
+ Étape 3 — Implémenter le Proxy Next.js (Option B)
171
+ ├── Créer pages/api/proxy/[...path].ts
172
+ ├── Ajouter HF_TOKEN dans .env.local (variable serveur, sans NEXT_PUBLIC_)
173
+ └── Modifier client.ts pour pointer vers /api/proxy/ au lieu de HF directement
174
+
175
+ Étape 4 — Gérer les WebSockets
176
+ └── Évaluer si les WS sont utilisés activement et adapter si nécessaire
177
+
178
+ Étape 5 — Tests
179
+ └── Vérifier tous les endpoints via le proxy
180
+ ```
181
+
182
+ ---
183
+
184
+ ## 8. Fichiers à Créer / Modifier
185
+
186
+ | Action | Fichier | Effort |
187
+ |--------|---------|--------|
188
+ | **CRÉER** | `src/pages/api/proxy/[...path].ts` | ~40 lignes |
189
+ | **MODIFIER** | `src/lib/api/client.ts` | ~3 lignes |
190
+ | **MODIFIER** | `.env.local` | +1 variable |
191
+ | **MODIFIER** | `.env.example` | +1 variable |
192
+
193
+ **Effort estimé : 1-2 heures**
194
+
195
+ ---
196
+
197
+ ## 9. Conclusion
198
+
199
+ > [!NOTE]
200
+ > La mise en privé du backend HF est **totalement faisable** sans modifier le backend. L'Option B (Proxy Next.js) est la plus sécurisée et la plus propre car elle garde le token HF côté serveur et ne change pas l'architecture existante du backend.
201
+
202
+ Le principal risque est lié aux **WebSockets** si ils sont utilisés en production — à vérifier avant l'implémentation.
app/services/k2_think_engine.py CHANGED
@@ -491,6 +491,134 @@ class K2ThinkEngine:
491
  logger.critical(f"Even fallback generation failed: {fallback_err}")
492
  raise ValueError(f"Analysis failed with error: {str(e)[:200]}.") from e
493
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
494
  def _generate_fallback_analysis_result(
495
  self,
496
  request_id: str,
 
491
  logger.critical(f"Even fallback generation failed: {fallback_err}")
492
  raise ValueError(f"Analysis failed with error: {str(e)[:200]}.") from e
493
 
494
+ async def chat(
495
+ self,
496
+ message: str,
497
+ analysis_context: Optional[Dict[str, Any]],
498
+ history: List[Dict[str, str]],
499
+ user_id: str
500
+ ) -> "ChatResponse":
501
+ """
502
+ Interactive scientific chat using K2 Think API.
503
+ Answers user questions grounded in the provided analysis context.
504
+ """
505
+ from app.models.schemas import ChatResponse
506
+
507
+ # Build context summary from analysis result
508
+ context_summary = ""
509
+ if analysis_context:
510
+ reasoning = analysis_context.get("reasoning_summary", "")
511
+ recommendations = analysis_context.get("strategic_recommendations", [])
512
+ gaps = []
513
+ comp = analysis_context.get("comparative_analysis") or {}
514
+ for g in (analysis_context.get("research_gaps") or comp.get("research_gaps") or []):
515
+ if isinstance(g, dict):
516
+ gaps.append(g.get("gap_description") or g.get("description") or "")
517
+ elif isinstance(g, str):
518
+ gaps.append(g)
519
+ contradictions = []
520
+ for c in (comp.get("contradictions") or []):
521
+ if isinstance(c, dict):
522
+ contradictions.append(c.get("description") or c.get("conflict") or "")
523
+ elif isinstance(c, str):
524
+ contradictions.append(c)
525
+ common_findings = comp.get("common_findings") or []
526
+
527
+ context_parts = []
528
+ if reasoning:
529
+ context_parts.append(f"[ANALYSIS SUMMARY]\n{reasoning}")
530
+ if common_findings:
531
+ context_parts.append("[COMMON FINDINGS]\n" + "\n".join(f"- {f}" for f in common_findings[:5]))
532
+ if gaps:
533
+ context_parts.append("[RESEARCH GAPS]\n" + "\n".join(f"- {g}" for g in gaps[:5] if g))
534
+ if contradictions:
535
+ context_parts.append("[CONTRADICTIONS / CONFLICTS]\n" + "\n".join(f"- {c}" for c in contradictions[:3] if c))
536
+ if recommendations:
537
+ context_parts.append("[STRATEGIC RECOMMENDATIONS]\n" + "\n".join(f"- {r}" for r in recommendations[:5]))
538
+ context_summary = "\n\n".join(context_parts)
539
+
540
+ # Build conversation history for the prompt
541
+ history_text = ""
542
+ if history:
543
+ history_lines = []
544
+ for turn in history[-8:]: # keep last 8 turns to stay within token limits
545
+ role = turn.get("role", "user")
546
+ content = turn.get("content", "")
547
+ prefix = "User" if role == "user" else "Assistant"
548
+ history_lines.append(f"{prefix}: {content}")
549
+ history_text = "\n".join(history_lines)
550
+
551
+ # Retrieve relevant memories if available
552
+ memory_context = ""
553
+ try:
554
+ memories = await self.memory_service.search_memory(
555
+ user_id=user_id,
556
+ query=message,
557
+ limit=3
558
+ )
559
+ if memories:
560
+ memory_context = "[RELEVANT MEMORY]\n" + "\n".join(
561
+ f"- {m}" for m in memories if m
562
+ )
563
+ except Exception as mem_err:
564
+ logger.warning(f"Memory retrieval failed during chat: {mem_err}")
565
+
566
+ system_prompt = """You are K2, an expert AI scientific co-investigator. You have access to the results of a deep comparative analysis of scientific papers. Your role is to answer the user's questions in a precise, insightful, and scientifically rigorous manner, always grounding your answers in the analysis context provided.
567
+
568
+ If the user asks about a specific paper, summarise it from the context. If they ask for experimental design help, leverage the identified research gaps and recommendations. Keep answers concise but substantive. Reply in the same language as the user's question."""
569
+
570
+ user_prompt_parts = []
571
+ if context_summary:
572
+ user_prompt_parts.append(f"[ANALYSIS CONTEXT]\n{context_summary}")
573
+ if memory_context:
574
+ user_prompt_parts.append(memory_context)
575
+ if history_text:
576
+ user_prompt_parts.append(f"[CONVERSATION HISTORY]\n{history_text}")
577
+ user_prompt_parts.append(f"[USER QUESTION]\n{message}")
578
+
579
+ full_prompt = system_prompt + "\n\n" + "\n\n".join(user_prompt_parts)
580
+
581
+ chat_config = {
582
+ "model": "MBZUAI-IFM/K2-Think-v2",
583
+ "openai_api_key": settings.K2_THINK_API_KEY,
584
+ "openai_api_base": settings.K2_THINK_API_URL,
585
+ "timeout": 120,
586
+ "max_retries": 1,
587
+ }
588
+
589
+ try:
590
+ llm = ChatOpenAI(**chat_config)
591
+ response = await llm.ainvoke([HumanMessage(content=full_prompt)])
592
+ raw_answer = response.content if hasattr(response, "content") else str(response)
593
+
594
+ # Strip any <think>...</think> reasoning block from the visible answer
595
+ visible_answer = re.sub(r"<think>.*?</think>", "", raw_answer, flags=re.DOTALL).strip()
596
+ reasoning_log = ""
597
+ think_match = re.search(r"<think>(.*?)</think>", raw_answer, flags=re.DOTALL)
598
+ if think_match:
599
+ reasoning_log = think_match.group(1).strip()
600
+
601
+ # Suggested follow-up actions
602
+ suggested_actions = [
603
+ "Explore this further",
604
+ "Generate a targeted protocol",
605
+ "Identify related research gaps",
606
+ ]
607
+
608
+ self._record_api_success()
609
+ logger.info("Chat response generated by K2 Think Engine")
610
+
611
+ return ChatResponse(
612
+ answer=visible_answer or raw_answer,
613
+ reasoning_log=reasoning_log or "K2 Think reasoning applied.",
614
+ suggested_actions=suggested_actions,
615
+ )
616
+
617
+ except Exception as e:
618
+ self._record_api_failure()
619
+ logger.error(f"K2 chat API call failed: {e}")
620
+ raise
621
+
622
  def _generate_fallback_analysis_result(
623
  self,
624
  request_id: str,