Spaces:
Sleeping
Sleeping
fix: Add chat method to K2ThinkEngine for scientific_chat route
Browse files- HF_PRIVATE_BACKEND.md +202 -0
- app/services/k2_think_engine.py +128 -0
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,
|