Spaces:
Sleeping
Sleeping
Mise à jour: routes d'analyse et moteur k2
Browse files- app/api/routes/analysis.py +136 -0
- app/services/k2_think_engine.py +4 -3
app/api/routes/analysis.py
CHANGED
|
@@ -234,6 +234,124 @@ def _normalize_k2_result_for_frontend(result_dict: dict) -> dict:
|
|
| 234 |
Ensures all required fields are present with proper defaults.
|
| 235 |
"""
|
| 236 |
from app.core.logging import logger
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
|
| 238 |
logger.info(f"NORMALIZE: Input result keys: {list(result_dict.keys())}")
|
| 239 |
logger.info(f"NORMALIZE: Status is: {result_dict.get('status')}")
|
|
@@ -277,6 +395,19 @@ def _normalize_k2_result_for_frontend(result_dict: dict) -> dict:
|
|
| 277 |
|
| 278 |
comp_analysis.setdefault('research_gaps', result_dict.get('research_gaps', []))
|
| 279 |
comp_analysis.setdefault('confidence_score', result_dict.get('confidence_overall', 0.8))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
|
| 281 |
# Ensure other required fields exist
|
| 282 |
result_dict.setdefault('research_gaps', [])
|
|
@@ -296,6 +427,11 @@ def _normalize_k2_result_for_frontend(result_dict: dict) -> dict:
|
|
| 296 |
result_dict['comparative_analysis'].setdefault('confidence_score', result_dict['confidence_overall'])
|
| 297 |
if result_dict['comparative_analysis'].get('confidence_score') == 0:
|
| 298 |
result_dict['comparative_analysis']['confidence_score'] = result_dict['confidence_overall']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
|
| 300 |
# Ensure arrays are properly formatted
|
| 301 |
for field in ['research_gaps', 'counter_hypotheses', 'strategic_recommendations', 'reasoning_trace']:
|
|
|
|
| 234 |
Ensures all required fields are present with proper defaults.
|
| 235 |
"""
|
| 236 |
from app.core.logging import logger
|
| 237 |
+
|
| 238 |
+
def _first_present(source: dict, keys: list, default=None):
|
| 239 |
+
for key in keys:
|
| 240 |
+
value = source.get(key)
|
| 241 |
+
if value not in (None, "", [], {}):
|
| 242 |
+
return value
|
| 243 |
+
return default
|
| 244 |
+
|
| 245 |
+
def _as_percent(value, default=80):
|
| 246 |
+
try:
|
| 247 |
+
score = float(value)
|
| 248 |
+
return int(round(score * 100)) if score <= 1 else int(round(score))
|
| 249 |
+
except (TypeError, ValueError):
|
| 250 |
+
return default
|
| 251 |
+
|
| 252 |
+
def _normalize_contradiction(item):
|
| 253 |
+
if not isinstance(item, dict):
|
| 254 |
+
return {"description": str(item), "resolution": "Investigation required", "confidence": 0.8}
|
| 255 |
+
|
| 256 |
+
topic = _first_present(item, ["topic", "variable", "title"], "Scientific conflict")
|
| 257 |
+
conflict = _first_present(item, ["conflict", "description", "explanation", "impact"])
|
| 258 |
+
statement_a = _first_present(item, ["statement_a", "finding_a"])
|
| 259 |
+
statement_b = _first_present(item, ["statement_b", "finding_b"])
|
| 260 |
+
|
| 261 |
+
if not conflict and (statement_a or statement_b):
|
| 262 |
+
conflict = " vs. ".join([p for p in [statement_a, statement_b] if p])
|
| 263 |
+
|
| 264 |
+
description = conflict or topic or "Scientific contradiction detected"
|
| 265 |
+
resolution = _first_present(item, ["resolution_path", "resolution", "suggested_resolution"], "Investigation required")
|
| 266 |
+
confidence = _first_present(item, ["confidence", "confidence_score"], 0.8)
|
| 267 |
+
|
| 268 |
+
normalized = dict(item)
|
| 269 |
+
normalized.setdefault("topic", topic)
|
| 270 |
+
normalized.setdefault("title", topic)
|
| 271 |
+
normalized.setdefault("description", description)
|
| 272 |
+
normalized.setdefault("conflict", description)
|
| 273 |
+
normalized.setdefault("resolution", resolution)
|
| 274 |
+
normalized.setdefault("resolution_path", resolution)
|
| 275 |
+
normalized.setdefault("confidence", confidence)
|
| 276 |
+
normalized.setdefault("impact_score", _as_percent(confidence))
|
| 277 |
+
return normalized
|
| 278 |
+
|
| 279 |
+
def _normalize_gap(item):
|
| 280 |
+
if not isinstance(item, dict):
|
| 281 |
+
return {"description": str(item), "gap_description": str(item), "importance_score": 0.8}
|
| 282 |
+
|
| 283 |
+
description = _first_present(
|
| 284 |
+
item,
|
| 285 |
+
["gap_description", "description", "opportunity", "title"],
|
| 286 |
+
"Research opportunity identified"
|
| 287 |
+
)
|
| 288 |
+
suggested = _first_present(
|
| 289 |
+
item,
|
| 290 |
+
["suggested_investigation", "suggested_direction", "recommendation", "next_step"],
|
| 291 |
+
"Design a targeted follow-up investigation."
|
| 292 |
+
)
|
| 293 |
+
score = _first_present(item, ["importance_score", "impact_score", "score"], 0.8)
|
| 294 |
+
|
| 295 |
+
normalized = dict(item)
|
| 296 |
+
normalized.setdefault("gap_description", description)
|
| 297 |
+
normalized.setdefault("description", description)
|
| 298 |
+
normalized.setdefault("title", description)
|
| 299 |
+
normalized.setdefault("suggested_investigation", suggested)
|
| 300 |
+
normalized.setdefault("suggested_direction", suggested)
|
| 301 |
+
normalized.setdefault("importance_score", score)
|
| 302 |
+
normalized.setdefault("impact_score", _as_percent(score))
|
| 303 |
+
normalized.setdefault("related_variables", [])
|
| 304 |
+
normalized.setdefault("source_documents", [])
|
| 305 |
+
return normalized
|
| 306 |
+
|
| 307 |
+
def _normalize_divergence(item):
|
| 308 |
+
if not isinstance(item, dict):
|
| 309 |
+
return {"description": str(item)}
|
| 310 |
+
|
| 311 |
+
topic = _first_present(item, ["topic", "variable", "title"], "Scientific divergence")
|
| 312 |
+
description = _first_present(
|
| 313 |
+
item,
|
| 314 |
+
["description", "impact", "conflict", "finding_a", "finding_b"],
|
| 315 |
+
topic
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
normalized = dict(item)
|
| 319 |
+
normalized.setdefault("topic", topic)
|
| 320 |
+
normalized.setdefault("title", topic)
|
| 321 |
+
normalized.setdefault("description", description)
|
| 322 |
+
return normalized
|
| 323 |
+
|
| 324 |
+
def _is_placeholder_summary(value):
|
| 325 |
+
if not isinstance(value, str):
|
| 326 |
+
return True
|
| 327 |
+
return value.strip().lower() in {
|
| 328 |
+
"",
|
| 329 |
+
"analysis completed.",
|
| 330 |
+
"analysis completed",
|
| 331 |
+
"analysis completed successfully",
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
def _build_summary_from_result(data):
|
| 335 |
+
comp = data.get("comparative_analysis") or {}
|
| 336 |
+
parts = []
|
| 337 |
+
|
| 338 |
+
findings = comp.get("common_findings") or []
|
| 339 |
+
if findings:
|
| 340 |
+
parts.append("Key findings: " + " ".join(str(item) for item in findings[:3]))
|
| 341 |
+
|
| 342 |
+
contradictions = comp.get("contradictions") or []
|
| 343 |
+
if contradictions:
|
| 344 |
+
descriptions = [
|
| 345 |
+
item.get("description") if isinstance(item, dict) else str(item)
|
| 346 |
+
for item in contradictions[:2]
|
| 347 |
+
]
|
| 348 |
+
parts.append("Main conflicts: " + " ".join(d for d in descriptions if d))
|
| 349 |
+
|
| 350 |
+
recommendations = data.get("strategic_recommendations") or []
|
| 351 |
+
if recommendations:
|
| 352 |
+
parts.append("Recommended strategy: " + " ".join(str(item) for item in recommendations[:2]))
|
| 353 |
+
|
| 354 |
+
return "\n\n".join(parts) if parts else None
|
| 355 |
|
| 356 |
logger.info(f"NORMALIZE: Input result keys: {list(result_dict.keys())}")
|
| 357 |
logger.info(f"NORMALIZE: Status is: {result_dict.get('status')}")
|
|
|
|
| 395 |
|
| 396 |
comp_analysis.setdefault('research_gaps', result_dict.get('research_gaps', []))
|
| 397 |
comp_analysis.setdefault('confidence_score', result_dict.get('confidence_overall', 0.8))
|
| 398 |
+
|
| 399 |
+
comp_analysis['contradictions'] = [
|
| 400 |
+
_normalize_contradiction(item) for item in comp_analysis.get('contradictions', [])
|
| 401 |
+
]
|
| 402 |
+
comp_analysis['divergences'] = [
|
| 403 |
+
_normalize_divergence(item) for item in comp_analysis.get('divergences', [])
|
| 404 |
+
]
|
| 405 |
+
comp_analysis['research_gaps'] = [
|
| 406 |
+
_normalize_gap(item) for item in comp_analysis.get('research_gaps', [])
|
| 407 |
+
]
|
| 408 |
+
result_dict['research_gaps'] = [
|
| 409 |
+
_normalize_gap(item) for item in (result_dict.get('research_gaps') or comp_analysis.get('research_gaps', []))
|
| 410 |
+
]
|
| 411 |
|
| 412 |
# Ensure other required fields exist
|
| 413 |
result_dict.setdefault('research_gaps', [])
|
|
|
|
| 427 |
result_dict['comparative_analysis'].setdefault('confidence_score', result_dict['confidence_overall'])
|
| 428 |
if result_dict['comparative_analysis'].get('confidence_score') == 0:
|
| 429 |
result_dict['comparative_analysis']['confidence_score'] = result_dict['confidence_overall']
|
| 430 |
+
|
| 431 |
+
if _is_placeholder_summary(result_dict.get('reasoning_summary')):
|
| 432 |
+
rebuilt_summary = _build_summary_from_result(result_dict)
|
| 433 |
+
if rebuilt_summary:
|
| 434 |
+
result_dict['reasoning_summary'] = rebuilt_summary
|
| 435 |
|
| 436 |
# Ensure arrays are properly formatted
|
| 437 |
for field in ['research_gaps', 'counter_hypotheses', 'strategic_recommendations', 'reasoning_trace']:
|
app/services/k2_think_engine.py
CHANGED
|
@@ -207,12 +207,12 @@ class K2ThinkEngine:
|
|
| 207 |
{{
|
| 208 |
"comparative_analysis": {{
|
| 209 |
"document_ids": ["doc_id1", "doc_id2"],
|
| 210 |
-
"divergences": [{{"topic": "description"}}],
|
| 211 |
-
"contradictions": [{{"topic": "...", "conflict": "...", "resolution_path": "...", "confidence": 0.8}}],
|
| 212 |
"common_findings": ["finding1"],
|
| 213 |
"confidence_score": 0.9
|
| 214 |
}},
|
| 215 |
-
"research_gaps": [{{"gap_description": "...", "importance_score": 0.8, "related_variables": [], "suggested_investigation": "...", "source_documents": ["doc_id1"]}}],
|
| 216 |
"counter_hypotheses": [{{"hypothesis": "...", "rationale": "...", "potential_bias": "...", "validation_experiment": "...", "confidence_against": 0.8}}],
|
| 217 |
"proposed_protocol": {{
|
| 218 |
"title": "...", "objective": "...", "steps": [{{"step_number": 1, "description": "...", "materials": [], "critical_parameters": []}}],
|
|
@@ -227,6 +227,7 @@ class K2ThinkEngine:
|
|
| 227 |
- NO preamble, NO explanations before or after JSON.
|
| 228 |
- KEEP REASONING CONCISE: Focus on direct analysis to stay within processing time limits.
|
| 229 |
- Use valid citations e.g. (Author, Year).
|
|
|
|
| 230 |
"""
|
| 231 |
|
| 232 |
# 3. Appel au modèle via ChatOpenAI (manual JSON parsing)
|
|
|
|
| 207 |
{{
|
| 208 |
"comparative_analysis": {{
|
| 209 |
"document_ids": ["doc_id1", "doc_id2"],
|
| 210 |
+
"divergences": [{{"topic": "...", "description": "...", "impact": "..."}}],
|
| 211 |
+
"contradictions": [{{"topic": "...", "description": "...", "conflict": "...", "resolution": "...", "resolution_path": "...", "confidence": 0.8, "impact_score": 80}}],
|
| 212 |
"common_findings": ["finding1"],
|
| 213 |
"confidence_score": 0.9
|
| 214 |
}},
|
| 215 |
+
"research_gaps": [{{"title": "...", "description": "...", "gap_description": "...", "importance_score": 0.8, "impact_score": 80, "related_variables": [], "suggested_investigation": "...", "suggested_direction": "...", "source_documents": ["doc_id1"]}}],
|
| 216 |
"counter_hypotheses": [{{"hypothesis": "...", "rationale": "...", "potential_bias": "...", "validation_experiment": "...", "confidence_against": 0.8}}],
|
| 217 |
"proposed_protocol": {{
|
| 218 |
"title": "...", "objective": "...", "steps": [{{"step_number": 1, "description": "...", "materials": [], "critical_parameters": []}}],
|
|
|
|
| 227 |
- NO preamble, NO explanations before or after JSON.
|
| 228 |
- KEEP REASONING CONCISE: Focus on direct analysis to stay within processing time limits.
|
| 229 |
- Use valid citations e.g. (Author, Year).
|
| 230 |
+
- Avoid placeholder text. Every contradiction and research gap must include a display-ready description.
|
| 231 |
"""
|
| 232 |
|
| 233 |
# 3. Appel au modèle via ChatOpenAI (manual JSON parsing)
|