Dama12 commited on
Commit
df2f637
·
1 Parent(s): 5fad32f

fix(backend): sanitize Ellipsis and placeholders in K2 response conversion

Browse files
Files changed (1) hide show
  1. 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 not gap: continue
 
505
  if isinstance(gap, dict):
 
 
 
 
 
 
 
506
  gaps.append(ResearchGap(
507
- gap_description=gap.get("description", gap.get("gap_description", gap.get("name", "Research Gap Detected"))),
508
- importance_score=float(gap.get("importance_score", gap.get("importance", 0.8))),
509
- related_variables=gap.get("related_variables", gap.get("variables", [])),
510
- suggested_investigation=gap.get("suggested_investigation", gap.get("investigation", "Investigation required")),
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=raw_comp.get("divergences", []),
526
- contradictions=raw_comp.get("contradictions", []),
527
- common_findings=raw_comp.get("common_findings", []),
528
  research_gaps=gaps,
529
- confidence_score=raw_comp.get("confidence_score", 0.8)
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
- for h in k2_result.get("counter_hypotheses", []):
 
 
 
 
 
538
  if isinstance(h, dict):
 
 
 
 
 
 
539
  hypotheses.append(CounterHypothesis(
540
- hypothesis=h.get("hypothesis", "Hypothesis"),
541
- rationale=h.get("rationale", ""),
542
- potential_bias=h.get("potential_bias", ""),
543
- validation_experiment=h.get("validation_experiment", ""),
544
- confidence_against=h.get("confidence_against", 0.5)
 
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
- for i, s in enumerate(proto_data.get("steps", []), 1):
 
 
 
 
557
  if isinstance(s, dict):
 
 
 
 
 
 
558
  steps.append(ExperimentalStep(
559
  step_number=i,
560
- description=s.get("description", f"Step {i}"),
561
- duration_hours=float(s.get("duration_hours", 1)),
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(v)
 
 
 
 
 
 
571
  else:
572
- valid_vars.append({"name": str(v), "type": "independent", "measurement_method": "TBD"})
 
 
 
 
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", ["Completion of all steps"]),
589
- estimated_duration_days=float(proto_data.get("estimated_duration_days", 30.0)),
590
- alternative_approaches=proto_data.get("alternative_approaches", ["None specified"]),
591
- risk_assessment=proto_data.get("risk_assessment", {"overall_risk": "low"})
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):