Spaces:
Sleeping
Sleeping
fix(backend): sanitize Ellipsis and placeholders in K2 response conversion
Browse files- app/services/k2_think_engine.py +153 -28
app/services/k2_think_engine.py
CHANGED
|
@@ -21,6 +21,61 @@ from langchain_openai import ChatOpenAI
|
|
| 21 |
from langchain.schema import HumanMessage
|
| 22 |
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
class K2ThinkEngine:
|
| 25 |
"""
|
| 26 |
Moteur K2 Think - IA Principal Unique
|
|
@@ -427,6 +482,9 @@ DO NOT USE <think> TAGS. DO NOT CONVERSE.
|
|
| 427 |
}
|
| 428 |
|
| 429 |
# 7. Conversion en objets schemas.py
|
|
|
|
|
|
|
|
|
|
| 430 |
comp_analysis = self._convert_k2_to_comparative_analysis(k2_analysis, request.documents)
|
| 431 |
hypotheses = self._convert_k2_to_counter_hypotheses(k2_analysis)
|
| 432 |
protocol = await self._convert_k2_to_protocol(k2_analysis)
|
|
@@ -501,15 +559,23 @@ DO NOT USE <think> TAGS. DO NOT CONVERSE.
|
|
| 501 |
raw_gaps = [raw_gaps]
|
| 502 |
|
| 503 |
for gap in raw_gaps:
|
| 504 |
-
if
|
|
|
|
| 505 |
if isinstance(gap, dict):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 506 |
gaps.append(ResearchGap(
|
| 507 |
-
gap_description=
|
| 508 |
-
importance_score=
|
| 509 |
-
related_variables=gap.get("related_variables"
|
| 510 |
-
suggested_investigation=gap.get("suggested_investigation"
|
| 511 |
source_documents=[doc.id for doc in docs],
|
| 512 |
-
citations=gap.get("citations"
|
| 513 |
))
|
| 514 |
else:
|
| 515 |
gaps.append(ResearchGap(
|
|
@@ -520,13 +586,28 @@ DO NOT USE <think> TAGS. DO NOT CONVERSE.
|
|
| 520 |
source_documents=[doc.id for doc in docs]
|
| 521 |
))
|
| 522 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 523 |
return ComparativeAnalysis(
|
| 524 |
document_ids=[doc.id for doc in docs],
|
| 525 |
-
divergences=
|
| 526 |
-
contradictions=
|
| 527 |
-
common_findings=
|
| 528 |
research_gaps=gaps,
|
| 529 |
-
confidence_score=
|
| 530 |
)
|
| 531 |
|
| 532 |
def _convert_k2_to_counter_hypotheses(
|
|
@@ -534,14 +615,26 @@ DO NOT USE <think> TAGS. DO NOT CONVERSE.
|
|
| 534 |
k2_result: Dict[str, Any]
|
| 535 |
) -> List[CounterHypothesis]:
|
| 536 |
hypotheses = []
|
| 537 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 538 |
if isinstance(h, dict):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
hypotheses.append(CounterHypothesis(
|
| 540 |
-
hypothesis=h.get("hypothesis"
|
| 541 |
-
rationale=h.get("rationale"
|
| 542 |
-
potential_bias=h.get("potential_bias"
|
| 543 |
-
validation_experiment=h.get("validation_experiment"
|
| 544 |
-
confidence_against=
|
|
|
|
| 545 |
))
|
| 546 |
return hypotheses
|
| 547 |
|
|
@@ -553,23 +646,49 @@ DO NOT USE <think> TAGS. DO NOT CONVERSE.
|
|
| 553 |
if not isinstance(proto_data, dict): proto_data = {}
|
| 554 |
|
| 555 |
steps = []
|
| 556 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 557 |
if isinstance(s, dict):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 558 |
steps.append(ExperimentalStep(
|
| 559 |
step_number=i,
|
| 560 |
-
description=s.get("description"
|
| 561 |
-
duration_hours=
|
| 562 |
-
materials=s.get("materials"
|
| 563 |
-
critical_parameters=s.get("critical_parameters"
|
|
|
|
|
|
|
|
|
|
| 564 |
))
|
| 565 |
|
| 566 |
-
raw_vars = proto_data.get("variables"
|
|
|
|
|
|
|
|
|
|
| 567 |
valid_vars = []
|
| 568 |
for v in raw_vars:
|
| 569 |
if isinstance(v, dict):
|
| 570 |
-
valid_vars.append(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 571 |
else:
|
| 572 |
-
valid_vars.append(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 573 |
|
| 574 |
def _ensure_str(val, default="TBD"):
|
| 575 |
if val is None: return default
|
|
@@ -577,6 +696,12 @@ DO NOT USE <think> TAGS. DO NOT CONVERSE.
|
|
| 577 |
return "\n- ".join([str(x) for x in val]) if val else default
|
| 578 |
return str(val)
|
| 579 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
return ExperimentalProtocol(
|
| 581 |
title=_ensure_str(proto_data.get("title"), "New Protocol"),
|
| 582 |
objective=_ensure_str(proto_data.get("objective"), "Objective"),
|
|
@@ -585,10 +710,10 @@ DO NOT USE <think> TAGS. DO NOT CONVERSE.
|
|
| 585 |
expected_outcomes=_ensure_str(proto_data.get("expected_outcomes"), "TBD"),
|
| 586 |
variables=valid_vars,
|
| 587 |
statistical_analysis_plan=_ensure_str(proto_data.get("statistical_analysis_plan"), "Standard descriptive statistics"),
|
| 588 |
-
success_criteria=proto_data.get("success_criteria"
|
| 589 |
-
estimated_duration_days=
|
| 590 |
-
alternative_approaches=proto_data.get("alternative_approaches"
|
| 591 |
-
risk_assessment=proto_data.get("risk_assessment"
|
| 592 |
)
|
| 593 |
|
| 594 |
def _log_reasoning(self, phase: str, step: str, description: str):
|
|
|
|
| 21 |
from langchain.schema import HumanMessage
|
| 22 |
|
| 23 |
|
| 24 |
+
def is_empty_or_placeholder_dict(d: dict) -> bool:
|
| 25 |
+
"""Check if a dictionary only contains None, Ellipsis, or empty placeholder strings."""
|
| 26 |
+
for v in d.values():
|
| 27 |
+
if v is not None and v is not Ellipsis and v != "":
|
| 28 |
+
return False
|
| 29 |
+
return True
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def sanitize_ellipsis_and_placeholders(data: Any) -> Any:
|
| 33 |
+
"""
|
| 34 |
+
Recursively remove Ellipsis, None, "...", and invalid placeholders from the data structure.
|
| 35 |
+
"""
|
| 36 |
+
if data is Ellipsis or data is None:
|
| 37 |
+
return None
|
| 38 |
+
|
| 39 |
+
if isinstance(data, str):
|
| 40 |
+
cleaned_str = data.strip()
|
| 41 |
+
if cleaned_str in ("...", "Ellipsis", "..", "."):
|
| 42 |
+
return ""
|
| 43 |
+
return data
|
| 44 |
+
|
| 45 |
+
if isinstance(data, list):
|
| 46 |
+
cleaned_list = []
|
| 47 |
+
for item in data:
|
| 48 |
+
if item is Ellipsis or item is None:
|
| 49 |
+
continue
|
| 50 |
+
if isinstance(item, str):
|
| 51 |
+
cleaned_item_str = item.strip()
|
| 52 |
+
if cleaned_item_str in ("...", "Ellipsis", "..", "."):
|
| 53 |
+
continue
|
| 54 |
+
cleaned_item = sanitize_ellipsis_and_placeholders(item)
|
| 55 |
+
if cleaned_item is not None:
|
| 56 |
+
if isinstance(cleaned_item, dict) and is_empty_or_placeholder_dict(cleaned_item):
|
| 57 |
+
continue
|
| 58 |
+
cleaned_list.append(cleaned_item)
|
| 59 |
+
return cleaned_list
|
| 60 |
+
|
| 61 |
+
if isinstance(data, dict):
|
| 62 |
+
cleaned_dict = {}
|
| 63 |
+
for k, v in data.items():
|
| 64 |
+
if v is Ellipsis or v is None:
|
| 65 |
+
continue
|
| 66 |
+
if isinstance(v, str):
|
| 67 |
+
cleaned_val_str = v.strip()
|
| 68 |
+
if cleaned_val_str in ("...", "Ellipsis", "..", "."):
|
| 69 |
+
cleaned_dict[k] = ""
|
| 70 |
+
continue
|
| 71 |
+
cleaned_v = sanitize_ellipsis_and_placeholders(v)
|
| 72 |
+
if cleaned_v is not None:
|
| 73 |
+
cleaned_dict[k] = cleaned_v
|
| 74 |
+
return cleaned_dict
|
| 75 |
+
|
| 76 |
+
return data
|
| 77 |
+
|
| 78 |
+
|
| 79 |
class K2ThinkEngine:
|
| 80 |
"""
|
| 81 |
Moteur K2 Think - IA Principal Unique
|
|
|
|
| 482 |
}
|
| 483 |
|
| 484 |
# 7. Conversion en objets schemas.py
|
| 485 |
+
# Clean k2_analysis of Ellipsis and other placeholders
|
| 486 |
+
k2_analysis = sanitize_ellipsis_and_placeholders(k2_analysis)
|
| 487 |
+
|
| 488 |
comp_analysis = self._convert_k2_to_comparative_analysis(k2_analysis, request.documents)
|
| 489 |
hypotheses = self._convert_k2_to_counter_hypotheses(k2_analysis)
|
| 490 |
protocol = await self._convert_k2_to_protocol(k2_analysis)
|
|
|
|
| 559 |
raw_gaps = [raw_gaps]
|
| 560 |
|
| 561 |
for gap in raw_gaps:
|
| 562 |
+
if gap is None or gap is Ellipsis:
|
| 563 |
+
continue
|
| 564 |
if isinstance(gap, dict):
|
| 565 |
+
gap_desc = gap.get("description") or gap.get("gap_description") or gap.get("name") or "Research Gap Detected"
|
| 566 |
+
imp_score = gap.get("importance_score") or gap.get("importance")
|
| 567 |
+
try:
|
| 568 |
+
imp_score = float(imp_score) if imp_score is not None else 0.8
|
| 569 |
+
except (ValueError, TypeError):
|
| 570 |
+
imp_score = 0.8
|
| 571 |
+
|
| 572 |
gaps.append(ResearchGap(
|
| 573 |
+
gap_description=gap_desc,
|
| 574 |
+
importance_score=imp_score,
|
| 575 |
+
related_variables=gap.get("related_variables") or gap.get("variables") or [],
|
| 576 |
+
suggested_investigation=gap.get("suggested_investigation") or gap.get("investigation") or "Investigation required",
|
| 577 |
source_documents=[doc.id for doc in docs],
|
| 578 |
+
citations=gap.get("citations") or []
|
| 579 |
))
|
| 580 |
else:
|
| 581 |
gaps.append(ResearchGap(
|
|
|
|
| 586 |
source_documents=[doc.id for doc in docs]
|
| 587 |
))
|
| 588 |
|
| 589 |
+
raw_divergences = raw_comp.get("divergences")
|
| 590 |
+
divergences = raw_divergences if isinstance(raw_divergences, list) else []
|
| 591 |
+
|
| 592 |
+
raw_contradictions = raw_comp.get("contradictions")
|
| 593 |
+
contradictions = raw_contradictions if isinstance(raw_contradictions, list) else []
|
| 594 |
+
|
| 595 |
+
raw_common = raw_comp.get("common_findings")
|
| 596 |
+
common_findings = raw_common if isinstance(raw_common, list) else []
|
| 597 |
+
|
| 598 |
+
conf_score = raw_comp.get("confidence_score")
|
| 599 |
+
try:
|
| 600 |
+
conf_score = float(conf_score) if conf_score is not None else 0.8
|
| 601 |
+
except (ValueError, TypeError):
|
| 602 |
+
conf_score = 0.8
|
| 603 |
+
|
| 604 |
return ComparativeAnalysis(
|
| 605 |
document_ids=[doc.id for doc in docs],
|
| 606 |
+
divergences=divergences,
|
| 607 |
+
contradictions=contradictions,
|
| 608 |
+
common_findings=common_findings,
|
| 609 |
research_gaps=gaps,
|
| 610 |
+
confidence_score=conf_score
|
| 611 |
)
|
| 612 |
|
| 613 |
def _convert_k2_to_counter_hypotheses(
|
|
|
|
| 615 |
k2_result: Dict[str, Any]
|
| 616 |
) -> List[CounterHypothesis]:
|
| 617 |
hypotheses = []
|
| 618 |
+
raw_hypotheses = k2_result.get("counter_hypotheses") or []
|
| 619 |
+
if not isinstance(raw_hypotheses, list):
|
| 620 |
+
raw_hypotheses = []
|
| 621 |
+
|
| 622 |
+
for h in raw_hypotheses:
|
| 623 |
+
if not h: continue
|
| 624 |
if isinstance(h, dict):
|
| 625 |
+
conf_against = h.get("confidence_against")
|
| 626 |
+
try:
|
| 627 |
+
conf_against = float(conf_against) if conf_against is not None else 0.5
|
| 628 |
+
except (ValueError, TypeError):
|
| 629 |
+
conf_against = 0.5
|
| 630 |
+
|
| 631 |
hypotheses.append(CounterHypothesis(
|
| 632 |
+
hypothesis=h.get("hypothesis") or "Hypothesis",
|
| 633 |
+
rationale=h.get("rationale") or "",
|
| 634 |
+
potential_bias=h.get("potential_bias") or "",
|
| 635 |
+
validation_experiment=h.get("validation_experiment") or "",
|
| 636 |
+
confidence_against=conf_against,
|
| 637 |
+
citations=h.get("citations") or []
|
| 638 |
))
|
| 639 |
return hypotheses
|
| 640 |
|
|
|
|
| 646 |
if not isinstance(proto_data, dict): proto_data = {}
|
| 647 |
|
| 648 |
steps = []
|
| 649 |
+
raw_steps = proto_data.get("steps") or []
|
| 650 |
+
if not isinstance(raw_steps, list):
|
| 651 |
+
raw_steps = []
|
| 652 |
+
|
| 653 |
+
for i, s in enumerate(raw_steps, 1):
|
| 654 |
if isinstance(s, dict):
|
| 655 |
+
dur_hours = s.get("duration_hours")
|
| 656 |
+
try:
|
| 657 |
+
dur_hours = float(dur_hours) if dur_hours is not None else 1.0
|
| 658 |
+
except (ValueError, TypeError):
|
| 659 |
+
dur_hours = 1.0
|
| 660 |
+
|
| 661 |
steps.append(ExperimentalStep(
|
| 662 |
step_number=i,
|
| 663 |
+
description=s.get("description") or f"Step {i}",
|
| 664 |
+
duration_hours=dur_hours,
|
| 665 |
+
materials=s.get("materials") or [],
|
| 666 |
+
critical_parameters=s.get("critical_parameters") or [],
|
| 667 |
+
validation_criteria=s.get("validation_criteria") or "Standard validation",
|
| 668 |
+
risk_level=s.get("risk_level") or "low",
|
| 669 |
+
contingency_plan=s.get("contingency_plan")
|
| 670 |
))
|
| 671 |
|
| 672 |
+
raw_vars = proto_data.get("variables") or []
|
| 673 |
+
if not isinstance(raw_vars, list):
|
| 674 |
+
raw_vars = []
|
| 675 |
+
|
| 676 |
valid_vars = []
|
| 677 |
for v in raw_vars:
|
| 678 |
if isinstance(v, dict):
|
| 679 |
+
valid_vars.append(ExperimentalVariable(
|
| 680 |
+
name=v.get("name") or "Variable",
|
| 681 |
+
type=v.get("type") or "independent",
|
| 682 |
+
measurement_unit=v.get("measurement_unit"),
|
| 683 |
+
measurement_method=v.get("measurement_method") or "TBD",
|
| 684 |
+
possible_values=v.get("possible_values")
|
| 685 |
+
))
|
| 686 |
else:
|
| 687 |
+
valid_vars.append(ExperimentalVariable(
|
| 688 |
+
name=str(v),
|
| 689 |
+
type="independent",
|
| 690 |
+
measurement_method="TBD"
|
| 691 |
+
))
|
| 692 |
|
| 693 |
def _ensure_str(val, default="TBD"):
|
| 694 |
if val is None: return default
|
|
|
|
| 696 |
return "\n- ".join([str(x) for x in val]) if val else default
|
| 697 |
return str(val)
|
| 698 |
|
| 699 |
+
est_dur = proto_data.get("estimated_duration_days")
|
| 700 |
+
try:
|
| 701 |
+
est_dur = float(est_dur) if est_dur is not None else 30.0
|
| 702 |
+
except (ValueError, TypeError):
|
| 703 |
+
est_dur = 30.0
|
| 704 |
+
|
| 705 |
return ExperimentalProtocol(
|
| 706 |
title=_ensure_str(proto_data.get("title"), "New Protocol"),
|
| 707 |
objective=_ensure_str(proto_data.get("objective"), "Objective"),
|
|
|
|
| 710 |
expected_outcomes=_ensure_str(proto_data.get("expected_outcomes"), "TBD"),
|
| 711 |
variables=valid_vars,
|
| 712 |
statistical_analysis_plan=_ensure_str(proto_data.get("statistical_analysis_plan"), "Standard descriptive statistics"),
|
| 713 |
+
success_criteria=proto_data.get("success_criteria") or ["Completion of all steps"],
|
| 714 |
+
estimated_duration_days=est_dur,
|
| 715 |
+
alternative_approaches=proto_data.get("alternative_approaches") or ["None specified"],
|
| 716 |
+
risk_assessment=proto_data.get("risk_assessment") or {"overall_risk": "low"}
|
| 717 |
)
|
| 718 |
|
| 719 |
def _log_reasoning(self, phase: str, step: str, description: str):
|