evalvault 1.75.0__py3-none-any.whl → 1.77.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- evalvault/adapters/inbound/api/adapter.py +123 -64
- evalvault/adapters/inbound/api/main.py +2 -0
- evalvault/adapters/inbound/api/routers/config.py +3 -1
- evalvault/adapters/inbound/cli/app.py +3 -0
- evalvault/adapters/inbound/cli/commands/analyze.py +6 -1
- evalvault/adapters/inbound/cli/commands/method.py +3 -3
- evalvault/adapters/inbound/cli/commands/run.py +153 -30
- evalvault/adapters/inbound/cli/commands/run_helpers.py +166 -62
- evalvault/adapters/outbound/analysis/llm_report_module.py +515 -33
- evalvault/adapters/outbound/llm/factory.py +1 -1
- evalvault/adapters/outbound/phoenix/sync_service.py +100 -1
- evalvault/adapters/outbound/report/markdown_adapter.py +92 -0
- evalvault/adapters/outbound/storage/factory.py +1 -4
- evalvault/adapters/outbound/tracker/mlflow_adapter.py +209 -54
- evalvault/adapters/outbound/tracker/phoenix_adapter.py +178 -12
- evalvault/config/instrumentation.py +8 -6
- evalvault/config/phoenix_support.py +5 -0
- evalvault/config/runtime_services.py +122 -0
- evalvault/config/settings.py +40 -4
- evalvault/domain/services/evaluator.py +2 -0
- {evalvault-1.75.0.dist-info → evalvault-1.77.0.dist-info}/METADATA +2 -1
- {evalvault-1.75.0.dist-info → evalvault-1.77.0.dist-info}/RECORD +25 -24
- {evalvault-1.75.0.dist-info → evalvault-1.77.0.dist-info}/WHEEL +0 -0
- {evalvault-1.75.0.dist-info → evalvault-1.77.0.dist-info}/entry_points.txt +0 -0
- {evalvault-1.75.0.dist-info → evalvault-1.77.0.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -172,11 +172,11 @@ class LLMReportModule(BaseAnalysisModule):
|
|
|
172
172
|
additional = context.get("additional_params", {}) or {}
|
|
173
173
|
value = params.get("evidence_limit")
|
|
174
174
|
if value is None:
|
|
175
|
-
value = additional.get("evidence_limit",
|
|
175
|
+
value = additional.get("evidence_limit", 12)
|
|
176
176
|
try:
|
|
177
177
|
return max(1, int(value))
|
|
178
178
|
except (TypeError, ValueError):
|
|
179
|
-
return
|
|
179
|
+
return 12
|
|
180
180
|
|
|
181
181
|
def _build_context(self, inputs: dict[str, Any], params: dict[str, Any]) -> dict[str, Any]:
|
|
182
182
|
context = inputs.get("__context__", {})
|
|
@@ -393,34 +393,27 @@ class LLMReportModule(BaseAnalysisModule):
|
|
|
393
393
|
threshold = self._resolve_threshold(run, metric)
|
|
394
394
|
pass_rate = pass_rates.get(metric) if isinstance(pass_rates, dict) else None
|
|
395
395
|
status = "unknown"
|
|
396
|
-
if isinstance(mean, int
|
|
396
|
+
if isinstance(mean, (int, float)):
|
|
397
397
|
status = "pass" if float(mean) >= threshold else "risk"
|
|
398
|
-
elif isinstance(pass_rate, int
|
|
398
|
+
elif isinstance(pass_rate, (int, float)):
|
|
399
399
|
status = "pass" if float(pass_rate) >= 0.7 else "risk"
|
|
400
400
|
|
|
401
|
+
mean_value = float(mean) if isinstance(mean, (int, float)) else None
|
|
402
|
+
pass_rate_value = float(pass_rate) if isinstance(pass_rate, (int, float)) else None
|
|
403
|
+
|
|
401
404
|
scorecard.append(
|
|
402
405
|
{
|
|
403
406
|
"metric": metric,
|
|
404
|
-
"mean":
|
|
405
|
-
"std":
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
"
|
|
409
|
-
if isinstance(stats.get("min"), int | float)
|
|
410
|
-
else None,
|
|
411
|
-
"max": round(float(stats.get("max")), 4)
|
|
412
|
-
if isinstance(stats.get("max"), int | float)
|
|
413
|
-
else None,
|
|
414
|
-
"median": round(float(stats.get("median")), 4)
|
|
415
|
-
if isinstance(stats.get("median"), int | float)
|
|
416
|
-
else None,
|
|
407
|
+
"mean": self._round_if_number(mean_value),
|
|
408
|
+
"std": self._round_if_number(stats.get("std")),
|
|
409
|
+
"min": self._round_if_number(stats.get("min")),
|
|
410
|
+
"max": self._round_if_number(stats.get("max")),
|
|
411
|
+
"median": self._round_if_number(stats.get("median")),
|
|
417
412
|
"count": stats.get("count") if isinstance(stats.get("count"), int) else None,
|
|
418
|
-
"pass_rate": (
|
|
419
|
-
round(float(pass_rate), 4) if isinstance(pass_rate, int | float) else None
|
|
420
|
-
),
|
|
413
|
+
"pass_rate": self._round_if_number(pass_rate_value),
|
|
421
414
|
"threshold": threshold,
|
|
422
|
-
"gap":
|
|
423
|
-
if isinstance(
|
|
415
|
+
"gap": self._round_if_number(threshold - mean_value)
|
|
416
|
+
if isinstance(mean_value, float)
|
|
424
417
|
else None,
|
|
425
418
|
"status": status,
|
|
426
419
|
}
|
|
@@ -596,10 +589,83 @@ class LLMReportModule(BaseAnalysisModule):
|
|
|
596
589
|
"comparison_scorecard": comparison_scorecard,
|
|
597
590
|
"quality_summary": quality_summary,
|
|
598
591
|
"artifact_manifest": artifact_manifest,
|
|
592
|
+
"artifact_highlights": self._build_artifact_highlights(context),
|
|
599
593
|
"evidence_index": evidence_index,
|
|
594
|
+
"signal_group_summary": self._build_signal_group_summary(scorecard),
|
|
600
595
|
"risk_metrics": risk_metrics,
|
|
601
596
|
}
|
|
602
597
|
|
|
598
|
+
def _build_artifact_highlights(self, context: dict[str, Any]) -> list[str]:
|
|
599
|
+
highlights: list[str] = []
|
|
600
|
+
|
|
601
|
+
def add_items(items: list[Any], prefix: str | None = None, limit: int = 3) -> None:
|
|
602
|
+
count = 0
|
|
603
|
+
for item in items:
|
|
604
|
+
if count >= limit:
|
|
605
|
+
break
|
|
606
|
+
text = self._stringify_artifact_item(item)
|
|
607
|
+
if not text:
|
|
608
|
+
continue
|
|
609
|
+
highlights.append(f"{prefix}: {text}" if prefix else text)
|
|
610
|
+
count += 1
|
|
611
|
+
|
|
612
|
+
root_causes = context.get("root_causes") or []
|
|
613
|
+
if isinstance(root_causes, list):
|
|
614
|
+
add_items(root_causes, prefix="원인")
|
|
615
|
+
|
|
616
|
+
recommendations = context.get("recommendations") or []
|
|
617
|
+
if isinstance(recommendations, list):
|
|
618
|
+
add_items(recommendations, prefix="권장")
|
|
619
|
+
|
|
620
|
+
patterns = context.get("patterns") or []
|
|
621
|
+
if isinstance(patterns, list):
|
|
622
|
+
add_items(patterns, prefix="패턴")
|
|
623
|
+
|
|
624
|
+
trends = context.get("trends") or []
|
|
625
|
+
if isinstance(trends, list):
|
|
626
|
+
add_items(trends, prefix="추세")
|
|
627
|
+
|
|
628
|
+
diagnostics = context.get("diagnostics") or []
|
|
629
|
+
if isinstance(diagnostics, list):
|
|
630
|
+
add_items(diagnostics, prefix="진단", limit=2)
|
|
631
|
+
|
|
632
|
+
nlp_keywords = context.get("nlp_keywords") or []
|
|
633
|
+
if isinstance(nlp_keywords, list) and nlp_keywords:
|
|
634
|
+
keywords = ", ".join([str(item) for item in nlp_keywords[:6]])
|
|
635
|
+
highlights.append(f"키워드: {keywords}")
|
|
636
|
+
|
|
637
|
+
nlp_summary = context.get("nlp_summary") or {}
|
|
638
|
+
if isinstance(nlp_summary, dict) and nlp_summary:
|
|
639
|
+
summary_text = self._stringify_artifact_item(nlp_summary)
|
|
640
|
+
if summary_text:
|
|
641
|
+
highlights.append(f"NLP 요약: {summary_text}")
|
|
642
|
+
|
|
643
|
+
priority_summary = context.get("priority_summary") or {}
|
|
644
|
+
if isinstance(priority_summary, dict) and priority_summary:
|
|
645
|
+
summary_text = self._stringify_artifact_item(priority_summary)
|
|
646
|
+
if summary_text:
|
|
647
|
+
highlights.append(f"우선순위: {summary_text}")
|
|
648
|
+
|
|
649
|
+
quality_checks = context.get("quality_checks") or []
|
|
650
|
+
if isinstance(quality_checks, list) and quality_checks:
|
|
651
|
+
add_items(quality_checks, prefix="품질 체크", limit=2)
|
|
652
|
+
|
|
653
|
+
return highlights[:10]
|
|
654
|
+
|
|
655
|
+
@staticmethod
|
|
656
|
+
def _stringify_artifact_item(item: Any) -> str:
|
|
657
|
+
if item is None:
|
|
658
|
+
return ""
|
|
659
|
+
if isinstance(item, str):
|
|
660
|
+
return truncate_text(item, 200)
|
|
661
|
+
if isinstance(item, dict):
|
|
662
|
+
for key in ("description", "cause", "summary", "label", "pattern", "title"):
|
|
663
|
+
value = item.get(key)
|
|
664
|
+
if isinstance(value, str) and value.strip():
|
|
665
|
+
return truncate_text(value.strip(), 200)
|
|
666
|
+
return truncate_text(json.dumps(item, ensure_ascii=False), 200)
|
|
667
|
+
return truncate_text(str(item), 200)
|
|
668
|
+
|
|
603
669
|
@staticmethod
|
|
604
670
|
def _coerce_str(value: Any) -> str:
|
|
605
671
|
if value is None:
|
|
@@ -737,6 +803,172 @@ class LLMReportModule(BaseAnalysisModule):
|
|
|
737
803
|
lines.append(f"{metric}: {hypothesis}{suffix}")
|
|
738
804
|
return lines
|
|
739
805
|
|
|
806
|
+
def _build_result_meaning(
|
|
807
|
+
self,
|
|
808
|
+
scorecard: list[dict[str, Any]],
|
|
809
|
+
signal_group_summary: dict[str, dict[str, Any]] | None,
|
|
810
|
+
evidence: list[dict[str, Any]],
|
|
811
|
+
) -> list[str]:
|
|
812
|
+
signal_group_summary = signal_group_summary or {}
|
|
813
|
+
evidence_index = self._index_evidence_by_metric(evidence)
|
|
814
|
+
risk_metrics = self._build_risk_metrics(scorecard, limit=4)
|
|
815
|
+
lines: list[str] = []
|
|
816
|
+
|
|
817
|
+
group_meaning = {
|
|
818
|
+
"groundedness": "근거성 저하로 사실 오류/환각 위험 증가",
|
|
819
|
+
"intent_alignment": "질문 의도와 답변 정렬이 약해 사용자 만족도 하락",
|
|
820
|
+
"retrieval_effectiveness": "검색/랭킹 품질 저하로 정답 근거 누락",
|
|
821
|
+
"summary_fidelity": "요약 충실도 저하로 핵심 정보 손실",
|
|
822
|
+
"embedding_quality": "임베딩 품질 저하로 유사도 기반 성능 하락",
|
|
823
|
+
"efficiency": "응답 지연/비용 증가 가능성",
|
|
824
|
+
"unknown": "지표 해석을 위해 추가 분석 필요",
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
for group, bucket in signal_group_summary.items():
|
|
828
|
+
risk_count = bucket.get("risk_count", 0)
|
|
829
|
+
total = bucket.get("total", 0)
|
|
830
|
+
if not risk_count:
|
|
831
|
+
continue
|
|
832
|
+
meaning = group_meaning.get(group, group_meaning["unknown"])
|
|
833
|
+
lines.append(f"{group}: {risk_count}/{total} 지표 위험 → {meaning}")
|
|
834
|
+
|
|
835
|
+
if not lines:
|
|
836
|
+
for row in risk_metrics:
|
|
837
|
+
metric = row.get("metric")
|
|
838
|
+
if not metric:
|
|
839
|
+
continue
|
|
840
|
+
refs = self._format_evidence_refs(evidence_index.get(metric, []))
|
|
841
|
+
suffix = f" {refs}" if refs else " (추가 데이터 필요)"
|
|
842
|
+
lines.append(f"{metric} 저하로 사용자 신뢰/정확성 이슈 가능{suffix}")
|
|
843
|
+
|
|
844
|
+
return lines
|
|
845
|
+
|
|
846
|
+
def _build_dataset_deltas(
|
|
847
|
+
self,
|
|
848
|
+
report_type: str,
|
|
849
|
+
run_summary: dict[str, Any] | None,
|
|
850
|
+
comparison_runs: list[dict[str, Any]] | None,
|
|
851
|
+
change_summary: dict[str, Any] | None,
|
|
852
|
+
scorecard: list[dict[str, Any]],
|
|
853
|
+
evidence: list[dict[str, Any]],
|
|
854
|
+
) -> list[str]:
|
|
855
|
+
report_type = report_type or "analysis"
|
|
856
|
+
change_summary = change_summary or {}
|
|
857
|
+
evidence_index = self._index_evidence_by_metric(evidence)
|
|
858
|
+
lines: list[str] = []
|
|
859
|
+
|
|
860
|
+
if report_type == "comparison":
|
|
861
|
+
dataset_changes = change_summary.get("dataset_changes", [])
|
|
862
|
+
if isinstance(dataset_changes, list) and dataset_changes:
|
|
863
|
+
for item in dataset_changes[:3]:
|
|
864
|
+
lines.append(f"데이터셋 변경: {self._format_dataset_change(item)}")
|
|
865
|
+
else:
|
|
866
|
+
run_a = comparison_runs[0] if comparison_runs else {}
|
|
867
|
+
run_b = comparison_runs[1] if comparison_runs and len(comparison_runs) > 1 else {}
|
|
868
|
+
if run_a.get("dataset_name") and run_b.get("dataset_name"):
|
|
869
|
+
if run_a.get("dataset_name") != run_b.get("dataset_name"):
|
|
870
|
+
lines.append(
|
|
871
|
+
f"데이터셋 이름 변경: {run_a.get('dataset_name')} → {run_b.get('dataset_name')}"
|
|
872
|
+
)
|
|
873
|
+
elif run_a.get("dataset_version") != run_b.get("dataset_version"):
|
|
874
|
+
lines.append(
|
|
875
|
+
f"데이터셋 버전 변경: {run_a.get('dataset_version')} → {run_b.get('dataset_version')}"
|
|
876
|
+
)
|
|
877
|
+
if not lines:
|
|
878
|
+
lines.append("데이터셋 변경 근거가 부족합니다. (추가 데이터 필요)")
|
|
879
|
+
return lines
|
|
880
|
+
|
|
881
|
+
if run_summary:
|
|
882
|
+
lines.append(
|
|
883
|
+
f"데이터셋: {run_summary.get('dataset_name', '-')} v{run_summary.get('dataset_version', '-')}"
|
|
884
|
+
)
|
|
885
|
+
risk_rows = self._build_risk_metrics(scorecard, limit=4)
|
|
886
|
+
for row in risk_rows:
|
|
887
|
+
metric = row.get("metric")
|
|
888
|
+
if not metric:
|
|
889
|
+
continue
|
|
890
|
+
mean = self._format_float(row.get("mean"))
|
|
891
|
+
threshold = self._format_float(row.get("threshold"))
|
|
892
|
+
refs = self._format_evidence_refs(evidence_index.get(metric, []))
|
|
893
|
+
suffix = f" {refs}" if refs else " (추가 데이터 필요)"
|
|
894
|
+
lines.append(f"{metric}: 평균 {mean} < 기준 {threshold} (데이터셋 기준 미달){suffix}")
|
|
895
|
+
if len(lines) <= 1:
|
|
896
|
+
lines.append("데이터셋 기준 차이를 설명할 근거가 부족합니다. (추가 데이터 필요)")
|
|
897
|
+
return lines
|
|
898
|
+
|
|
899
|
+
def _build_improvement_plan(
|
|
900
|
+
self,
|
|
901
|
+
scorecard: list[dict[str, Any]],
|
|
902
|
+
evidence: list[dict[str, Any]],
|
|
903
|
+
recommendations: list[str],
|
|
904
|
+
) -> list[str]:
|
|
905
|
+
if recommendations:
|
|
906
|
+
return recommendations[:6]
|
|
907
|
+
|
|
908
|
+
action_map = {
|
|
909
|
+
"context_precision": "랭커/리랭커 도입 및 상위 문서 필터링 강화",
|
|
910
|
+
"context_recall": "검색 범위 확장 또는 하드 네거티브 추가",
|
|
911
|
+
"mrr": "상위 K 재정렬 및 쿼리 재작성 적용",
|
|
912
|
+
"ndcg": "랭킹 품질 지표 최적화(리랭킹/하이브리드 검색)",
|
|
913
|
+
"hit_rate": "검색 후보군 확대 또는 인덱싱 개선",
|
|
914
|
+
"answer_relevancy": "답변 포맷/질문 의도 정렬 프롬프트 강화",
|
|
915
|
+
"faithfulness": "근거 인용/검증 단계 추가",
|
|
916
|
+
"factual_correctness": "정답 검증 규칙 강화 및 근거 필터링",
|
|
917
|
+
"semantic_similarity": "정답 기준 문장 재정의 및 평가셋 보강",
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
evidence_index = self._index_evidence_by_metric(evidence)
|
|
921
|
+
risk_rows = self._build_risk_metrics(scorecard, limit=4)
|
|
922
|
+
lines: list[str] = []
|
|
923
|
+
for row in risk_rows:
|
|
924
|
+
metric = row.get("metric")
|
|
925
|
+
if not metric:
|
|
926
|
+
continue
|
|
927
|
+
action = action_map.get(metric, "실험을 통해 개선 방향을 재검증")
|
|
928
|
+
refs = self._format_evidence_refs(evidence_index.get(metric, []))
|
|
929
|
+
suffix = f" {refs}" if refs else " (추가 데이터 필요)"
|
|
930
|
+
lines.append(f"{metric}: {action} → 영향 지표 {metric}, 검증: analyze-compare{suffix}")
|
|
931
|
+
if not lines:
|
|
932
|
+
lines.append("개선 방향 도출을 위한 근거가 부족합니다. (추가 데이터 필요)")
|
|
933
|
+
return lines
|
|
934
|
+
|
|
935
|
+
@staticmethod
|
|
936
|
+
def _is_generic_entry(text: str) -> bool:
|
|
937
|
+
if not text:
|
|
938
|
+
return True
|
|
939
|
+
if re.search(r"\[(A|B|E)\d+\]", text):
|
|
940
|
+
return False
|
|
941
|
+
lowered = text.replace(" ", "").strip()
|
|
942
|
+
generic_phrases = (
|
|
943
|
+
"추가데이터필요",
|
|
944
|
+
"개선필요",
|
|
945
|
+
"점검필요",
|
|
946
|
+
"확인필요",
|
|
947
|
+
"재검증",
|
|
948
|
+
"보완필요",
|
|
949
|
+
"데이터부족",
|
|
950
|
+
"근거부족",
|
|
951
|
+
"점검하세요",
|
|
952
|
+
"재점검",
|
|
953
|
+
"확인하세요",
|
|
954
|
+
"추가하세요",
|
|
955
|
+
"강화하세요",
|
|
956
|
+
"필요합니다",
|
|
957
|
+
)
|
|
958
|
+
return not lowered or any(phrase in lowered for phrase in generic_phrases)
|
|
959
|
+
|
|
960
|
+
def _select_section_or_fallback(
|
|
961
|
+
self,
|
|
962
|
+
items: list[str],
|
|
963
|
+
fallback: list[str],
|
|
964
|
+
*,
|
|
965
|
+
min_items: int = 2,
|
|
966
|
+
) -> list[str]:
|
|
967
|
+
cleaned = [item.strip() for item in items if item and item.strip()]
|
|
968
|
+
if len(cleaned) >= min_items and not all(self._is_generic_entry(item) for item in cleaned):
|
|
969
|
+
return cleaned
|
|
970
|
+
return fallback
|
|
971
|
+
|
|
740
972
|
def _build_comparison_stat_notes(
|
|
741
973
|
self,
|
|
742
974
|
comparison_scorecard: list[dict[str, Any]],
|
|
@@ -762,21 +994,62 @@ class LLMReportModule(BaseAnalysisModule):
|
|
|
762
994
|
dataset_changes = change_summary.get("dataset_changes", [])
|
|
763
995
|
config_changes = change_summary.get("config_changes", [])
|
|
764
996
|
prompt_changes = change_summary.get("prompt_changes", {})
|
|
765
|
-
evidence_ids = [
|
|
997
|
+
evidence_ids: list[str] = []
|
|
998
|
+
for item in evidence:
|
|
999
|
+
evidence_id = item.get("evidence_id")
|
|
1000
|
+
if evidence_id is None:
|
|
1001
|
+
continue
|
|
1002
|
+
evidence_ids.append(str(evidence_id))
|
|
766
1003
|
refs = self._format_evidence_refs(evidence_ids, max_items=2)
|
|
767
1004
|
refs_text = f" {refs}" if refs else ""
|
|
768
1005
|
lines: list[str] = []
|
|
769
1006
|
if isinstance(dataset_changes, list) and dataset_changes:
|
|
770
|
-
|
|
1007
|
+
for item in dataset_changes[:3]:
|
|
1008
|
+
lines.append(f"데이터셋 변경: {self._format_dataset_change(item)}{refs_text}")
|
|
771
1009
|
if isinstance(config_changes, list) and config_changes:
|
|
772
1010
|
lines.append(f"설정 변경이 지표 변동에 영향 가능{refs_text}")
|
|
773
1011
|
if isinstance(prompt_changes, dict) and prompt_changes.get("status"):
|
|
774
1012
|
status = prompt_changes.get("status")
|
|
775
1013
|
lines.append(f"프롬프트 변경 상태: {status}{refs_text}")
|
|
1014
|
+
lines.extend(self._summarize_comparison_evidence(evidence))
|
|
776
1015
|
if not lines:
|
|
777
1016
|
lines.append("원인 분석 근거가 부족합니다. (추가 데이터 필요)")
|
|
778
1017
|
return lines
|
|
779
1018
|
|
|
1019
|
+
@staticmethod
|
|
1020
|
+
def _format_dataset_change(item: Any) -> str:
|
|
1021
|
+
if isinstance(item, dict):
|
|
1022
|
+
field = item.get("field") or "field"
|
|
1023
|
+
before = item.get("before")
|
|
1024
|
+
after = item.get("after")
|
|
1025
|
+
if before is not None or after is not None:
|
|
1026
|
+
return f"{field}: {before} → {after}"
|
|
1027
|
+
return str(item)
|
|
1028
|
+
|
|
1029
|
+
def _summarize_comparison_evidence(self, evidence: list[dict[str, Any]]) -> list[str]:
|
|
1030
|
+
summary: dict[str, dict[str, int]] = {"A": {}, "B": {}}
|
|
1031
|
+
refs: dict[str, list[str]] = {"A": [], "B": []}
|
|
1032
|
+
for item in evidence:
|
|
1033
|
+
run_label = item.get("run_label") or "A"
|
|
1034
|
+
if run_label not in summary:
|
|
1035
|
+
continue
|
|
1036
|
+
for metric in item.get("failed_metrics") or []:
|
|
1037
|
+
summary[run_label][metric] = summary[run_label].get(metric, 0) + 1
|
|
1038
|
+
evidence_id = item.get("evidence_id")
|
|
1039
|
+
if evidence_id:
|
|
1040
|
+
refs[run_label].append(str(evidence_id))
|
|
1041
|
+
|
|
1042
|
+
lines: list[str] = []
|
|
1043
|
+
for label in ("A", "B"):
|
|
1044
|
+
if not summary[label]:
|
|
1045
|
+
continue
|
|
1046
|
+
top = sorted(summary[label].items(), key=lambda x: x[1], reverse=True)[:3]
|
|
1047
|
+
metrics_text = ", ".join([f"{metric}({count}건)" for metric, count in top])
|
|
1048
|
+
refs_text = self._format_evidence_refs(refs[label], max_items=2)
|
|
1049
|
+
suffix = f" {refs_text}" if refs_text else ""
|
|
1050
|
+
lines.append(f"실행 {label} 실패 지표 상위: {metrics_text}{suffix}")
|
|
1051
|
+
return lines
|
|
1052
|
+
|
|
780
1053
|
def _merge_recommendations(self, context: dict[str, Any]) -> list[str]:
|
|
781
1054
|
recommendations: list[str] = []
|
|
782
1055
|
for item in context.get("recommendations") or []:
|
|
@@ -794,8 +1067,12 @@ class LLMReportModule(BaseAnalysisModule):
|
|
|
794
1067
|
"summary_bullets": ["<요약 bullet 1>", "<요약 bullet 2>", "<요약 bullet 3>"],
|
|
795
1068
|
"change_summary": ["<변경 사항 1>", "<변경 사항 2>"],
|
|
796
1069
|
"statistical_notes": ["<통계적 신뢰도 1>"],
|
|
1070
|
+
"reasons": ["<변화 원인/근거 1>"],
|
|
1071
|
+
"meaning": ["<결과 의미 1>"],
|
|
1072
|
+
"dataset_deltas": ["<데이터셋 차이 1>"],
|
|
797
1073
|
"root_causes": ["<원인 분석 1>"],
|
|
798
1074
|
"recommendations": ["<개선 제안 1>"],
|
|
1075
|
+
"improvement_plan": ["<개선 방향 1>", "<검증 방법 1>", "<리스크 1>"],
|
|
799
1076
|
"next_steps": ["<다음 단계 1>"],
|
|
800
1077
|
"appendix": ["<부록 항목 1>"],
|
|
801
1078
|
}
|
|
@@ -803,7 +1080,11 @@ class LLMReportModule(BaseAnalysisModule):
|
|
|
803
1080
|
template = {
|
|
804
1081
|
"summary_sentence": "<한 문장 결론>",
|
|
805
1082
|
"summary_bullets": ["<요약 bullet 1>", "<요약 bullet 2>", "<요약 bullet 3>"],
|
|
1083
|
+
"reasons": ["<원인/근거 1>"],
|
|
1084
|
+
"meaning": ["<결과 의미 1>"],
|
|
1085
|
+
"dataset_deltas": ["<데이터셋 차이 1>"],
|
|
806
1086
|
"recommendations": ["<개선 제안 1>"],
|
|
1087
|
+
"improvement_plan": ["<개선 방향 1>", "<검증 방법 1>", "<리스크 1>"],
|
|
807
1088
|
"next_steps": ["<다음 단계 1>"],
|
|
808
1089
|
"appendix": ["<부록 항목 1>"],
|
|
809
1090
|
}
|
|
@@ -812,8 +1093,12 @@ class LLMReportModule(BaseAnalysisModule):
|
|
|
812
1093
|
"summary_sentence": "<한 문장 결론>",
|
|
813
1094
|
"summary_bullets": ["<요약 bullet 1>", "<요약 bullet 2>", "<요약 bullet 3>"],
|
|
814
1095
|
"insights": ["<증거 기반 인사이트 1>"],
|
|
1096
|
+
"reasons": ["<원인/근거 1>"],
|
|
1097
|
+
"meaning": ["<결과 의미 1>"],
|
|
1098
|
+
"dataset_deltas": ["<데이터셋 차이 1>"],
|
|
815
1099
|
"root_causes": ["<원인 가설 1>"],
|
|
816
1100
|
"recommendations": ["<개선 제안 1>"],
|
|
1101
|
+
"improvement_plan": ["<개선 방향 1>", "<검증 방법 1>", "<리스크 1>"],
|
|
817
1102
|
"next_steps": ["<다음 단계 1>"],
|
|
818
1103
|
"appendix": ["<부록 항목 1>"],
|
|
819
1104
|
}
|
|
@@ -852,8 +1137,12 @@ class LLMReportModule(BaseAnalysisModule):
|
|
|
852
1137
|
"summary_sentence": self._coerce_str(payload.get("summary_sentence")),
|
|
853
1138
|
"summary_bullets": self._coerce_list(payload.get("summary_bullets")),
|
|
854
1139
|
"insights": self._coerce_list(payload.get("insights")),
|
|
1140
|
+
"reasons": self._coerce_list(payload.get("reasons")),
|
|
1141
|
+
"meaning": self._coerce_list(payload.get("meaning")),
|
|
1142
|
+
"dataset_deltas": self._coerce_list(payload.get("dataset_deltas")),
|
|
855
1143
|
"root_causes": self._coerce_list(payload.get("root_causes")),
|
|
856
1144
|
"recommendations": self._coerce_list(payload.get("recommendations")),
|
|
1145
|
+
"improvement_plan": self._coerce_list(payload.get("improvement_plan")),
|
|
857
1146
|
"next_steps": self._coerce_list(payload.get("next_steps")),
|
|
858
1147
|
"change_summary": self._coerce_list(payload.get("change_summary")),
|
|
859
1148
|
"statistical_notes": self._coerce_list(payload.get("statistical_notes")),
|
|
@@ -865,18 +1154,35 @@ class LLMReportModule(BaseAnalysisModule):
|
|
|
865
1154
|
"summary_sentence",
|
|
866
1155
|
"summary_bullets",
|
|
867
1156
|
"insights",
|
|
1157
|
+
"reasons",
|
|
1158
|
+
"meaning",
|
|
1159
|
+
"dataset_deltas",
|
|
868
1160
|
"root_causes",
|
|
869
1161
|
"recommendations",
|
|
1162
|
+
"improvement_plan",
|
|
1163
|
+
"next_steps",
|
|
1164
|
+
],
|
|
1165
|
+
"summary": [
|
|
1166
|
+
"summary_sentence",
|
|
1167
|
+
"summary_bullets",
|
|
1168
|
+
"reasons",
|
|
1169
|
+
"meaning",
|
|
1170
|
+
"dataset_deltas",
|
|
1171
|
+
"recommendations",
|
|
1172
|
+
"improvement_plan",
|
|
870
1173
|
"next_steps",
|
|
871
1174
|
],
|
|
872
|
-
"summary": ["summary_sentence", "summary_bullets", "recommendations", "next_steps"],
|
|
873
1175
|
"comparison": [
|
|
874
1176
|
"summary_sentence",
|
|
875
1177
|
"summary_bullets",
|
|
876
1178
|
"change_summary",
|
|
877
1179
|
"statistical_notes",
|
|
1180
|
+
"reasons",
|
|
1181
|
+
"meaning",
|
|
1182
|
+
"dataset_deltas",
|
|
878
1183
|
"root_causes",
|
|
879
1184
|
"recommendations",
|
|
1185
|
+
"improvement_plan",
|
|
880
1186
|
"next_steps",
|
|
881
1187
|
],
|
|
882
1188
|
}
|
|
@@ -917,6 +1223,8 @@ class LLMReportModule(BaseAnalysisModule):
|
|
|
917
1223
|
comparison_scorecard = assets.get("comparison_scorecard", [])
|
|
918
1224
|
quality_summary = assets.get("quality_summary", {})
|
|
919
1225
|
artifact_manifest = assets.get("artifact_manifest", [])
|
|
1226
|
+
artifact_highlights = assets.get("artifact_highlights", [])
|
|
1227
|
+
artifact_hint_lines = [f"아티팩트 근거: {item}" for item in artifact_highlights[:4]]
|
|
920
1228
|
|
|
921
1229
|
lines = [title, "", "## 요약"]
|
|
922
1230
|
summary_sentence = payload.get("summary_sentence")
|
|
@@ -950,6 +1258,47 @@ class LLMReportModule(BaseAnalysisModule):
|
|
|
950
1258
|
lines.append(f"- {note}")
|
|
951
1259
|
lines.append("")
|
|
952
1260
|
|
|
1261
|
+
lines.append("## 원인/근거")
|
|
1262
|
+
reasons = self._select_section_or_fallback(
|
|
1263
|
+
payload.get("reasons") or [],
|
|
1264
|
+
self._build_comparison_root_causes(context.get("change_summary"), evidence)
|
|
1265
|
+
+ artifact_hint_lines,
|
|
1266
|
+
)
|
|
1267
|
+
for item in reasons:
|
|
1268
|
+
lines.append(f"- {item}")
|
|
1269
|
+
lines.append("")
|
|
1270
|
+
|
|
1271
|
+
lines.append("## 결과 의미")
|
|
1272
|
+
meanings = self._select_section_or_fallback(
|
|
1273
|
+
payload.get("meaning") or [],
|
|
1274
|
+
self._build_result_meaning(
|
|
1275
|
+
scorecard,
|
|
1276
|
+
assets.get("signal_group_summary") or {},
|
|
1277
|
+
evidence,
|
|
1278
|
+
)
|
|
1279
|
+
+ artifact_hint_lines,
|
|
1280
|
+
)
|
|
1281
|
+
for item in meanings:
|
|
1282
|
+
lines.append(f"- {item}")
|
|
1283
|
+
lines.append("")
|
|
1284
|
+
|
|
1285
|
+
lines.append("## 데이터셋 차이")
|
|
1286
|
+
dataset_deltas = self._select_section_or_fallback(
|
|
1287
|
+
payload.get("dataset_deltas") or [],
|
|
1288
|
+
self._build_dataset_deltas(
|
|
1289
|
+
report_type,
|
|
1290
|
+
self._build_run_summary(context.get("run")),
|
|
1291
|
+
[self._build_run_summary(run) for run in (context.get("runs") or [])[:2]],
|
|
1292
|
+
context.get("change_summary"),
|
|
1293
|
+
scorecard,
|
|
1294
|
+
evidence,
|
|
1295
|
+
)
|
|
1296
|
+
+ artifact_hint_lines,
|
|
1297
|
+
)
|
|
1298
|
+
for item in dataset_deltas:
|
|
1299
|
+
lines.append(f"- {item}")
|
|
1300
|
+
lines.append("")
|
|
1301
|
+
|
|
953
1302
|
lines.append("## 원인 분석")
|
|
954
1303
|
root_causes = payload.get("root_causes") or self._build_comparison_root_causes(
|
|
955
1304
|
context.get("change_summary"),
|
|
@@ -963,6 +1312,46 @@ class LLMReportModule(BaseAnalysisModule):
|
|
|
963
1312
|
lines.extend(self._render_scorecard_table(scorecard))
|
|
964
1313
|
lines.append("")
|
|
965
1314
|
|
|
1315
|
+
lines.append("## 원인/근거")
|
|
1316
|
+
reasons = self._select_section_or_fallback(
|
|
1317
|
+
payload.get("reasons") or [],
|
|
1318
|
+
self._build_fallback_insights(scorecard, evidence) + artifact_hint_lines,
|
|
1319
|
+
)
|
|
1320
|
+
for item in reasons:
|
|
1321
|
+
lines.append(f"- {item}")
|
|
1322
|
+
lines.append("")
|
|
1323
|
+
|
|
1324
|
+
lines.append("## 결과 의미")
|
|
1325
|
+
meanings = self._select_section_or_fallback(
|
|
1326
|
+
payload.get("meaning") or [],
|
|
1327
|
+
self._build_result_meaning(
|
|
1328
|
+
scorecard,
|
|
1329
|
+
assets.get("signal_group_summary") or {},
|
|
1330
|
+
evidence,
|
|
1331
|
+
)
|
|
1332
|
+
+ artifact_hint_lines,
|
|
1333
|
+
)
|
|
1334
|
+
for item in meanings:
|
|
1335
|
+
lines.append(f"- {item}")
|
|
1336
|
+
lines.append("")
|
|
1337
|
+
|
|
1338
|
+
lines.append("## 데이터셋 차이")
|
|
1339
|
+
dataset_deltas = self._select_section_or_fallback(
|
|
1340
|
+
payload.get("dataset_deltas") or [],
|
|
1341
|
+
self._build_dataset_deltas(
|
|
1342
|
+
report_type,
|
|
1343
|
+
self._build_run_summary(context.get("run")),
|
|
1344
|
+
[self._build_run_summary(run) for run in (context.get("runs") or [])[:2]],
|
|
1345
|
+
context.get("change_summary"),
|
|
1346
|
+
scorecard,
|
|
1347
|
+
evidence,
|
|
1348
|
+
)
|
|
1349
|
+
+ artifact_hint_lines,
|
|
1350
|
+
)
|
|
1351
|
+
for item in dataset_deltas:
|
|
1352
|
+
lines.append(f"- {item}")
|
|
1353
|
+
lines.append("")
|
|
1354
|
+
|
|
966
1355
|
if report_type != "summary":
|
|
967
1356
|
lines.append("## 데이터 품질/신뢰도")
|
|
968
1357
|
lines.extend(
|
|
@@ -1008,6 +1397,16 @@ class LLMReportModule(BaseAnalysisModule):
|
|
|
1008
1397
|
else:
|
|
1009
1398
|
lines.append("- 추가 데이터 및 LLM 분석을 통해 상세 원인을 도출하세요.")
|
|
1010
1399
|
|
|
1400
|
+
lines.append("")
|
|
1401
|
+
lines.append("## 개선 방향")
|
|
1402
|
+
improvement_plan = self._select_section_or_fallback(
|
|
1403
|
+
payload.get("improvement_plan") or [],
|
|
1404
|
+
self._build_improvement_plan(scorecard, evidence, recommendations)
|
|
1405
|
+
+ artifact_hint_lines,
|
|
1406
|
+
)
|
|
1407
|
+
for item in improvement_plan:
|
|
1408
|
+
lines.append(f"- {item}")
|
|
1409
|
+
|
|
1011
1410
|
lines.append("")
|
|
1012
1411
|
lines.append("## 다음 단계")
|
|
1013
1412
|
next_steps = payload.get("next_steps") or [
|
|
@@ -1033,12 +1432,16 @@ class LLMReportModule(BaseAnalysisModule):
|
|
|
1033
1432
|
"변경 사항 요약": ["변경 사항 요약", "변경사항 요약", "변경 요약"],
|
|
1034
1433
|
"지표 비교 스코어카드": ["지표 비교 스코어카드", "비교 스코어카드", "지표 비교표"],
|
|
1035
1434
|
"통계적 신뢰도": ["통계적 신뢰도", "통계 신뢰도", "통계적 검증"],
|
|
1435
|
+
"원인/근거": ["원인/근거", "원인 및 근거", "근거"],
|
|
1436
|
+
"결과 의미": ["결과 의미", "의미", "영향"],
|
|
1437
|
+
"데이터셋 차이": ["데이터셋 차이", "데이터셋 변경", "데이터셋 기준"],
|
|
1036
1438
|
"원인 분석": ["원인 분석", "원인", "원인 가설"],
|
|
1037
1439
|
"지표 스코어카드": ["지표 스코어카드", "스코어카드", "지표 요약"],
|
|
1038
1440
|
"데이터 품질/신뢰도": ["데이터 품질/신뢰도", "데이터 품질", "신뢰도"],
|
|
1039
1441
|
"증거 기반 인사이트": ["증거 기반 인사이트", "증거 인사이트", "증거 기반", "증거"],
|
|
1040
1442
|
"원인 가설": ["원인 가설", "원인 분석", "원인"],
|
|
1041
1443
|
"개선 제안": ["개선 제안", "개선안", "개선 사항"],
|
|
1444
|
+
"개선 방향": ["개선 방향", "개선 계획", "개선 로드맵"],
|
|
1042
1445
|
"다음 단계": ["다음 단계", "후속 단계", "다음 액션"],
|
|
1043
1446
|
"부록(산출물)": ["부록(산출물)", "부록", "산출물"],
|
|
1044
1447
|
}
|
|
@@ -1048,25 +1451,37 @@ class LLMReportModule(BaseAnalysisModule):
|
|
|
1048
1451
|
"변경 사항 요약",
|
|
1049
1452
|
"지표 비교 스코어카드",
|
|
1050
1453
|
"통계적 신뢰도",
|
|
1454
|
+
"원인/근거",
|
|
1455
|
+
"결과 의미",
|
|
1456
|
+
"데이터셋 차이",
|
|
1051
1457
|
"원인 분석",
|
|
1052
1458
|
"개선 제안",
|
|
1459
|
+
"개선 방향",
|
|
1053
1460
|
"다음 단계",
|
|
1054
1461
|
"부록(산출물)",
|
|
1055
1462
|
],
|
|
1056
1463
|
"summary": [
|
|
1057
1464
|
"요약",
|
|
1058
1465
|
"지표 스코어카드",
|
|
1466
|
+
"원인/근거",
|
|
1467
|
+
"결과 의미",
|
|
1468
|
+
"데이터셋 차이",
|
|
1059
1469
|
"개선 제안",
|
|
1470
|
+
"개선 방향",
|
|
1060
1471
|
"다음 단계",
|
|
1061
1472
|
"부록(산출물)",
|
|
1062
1473
|
],
|
|
1063
1474
|
"analysis": [
|
|
1064
1475
|
"요약",
|
|
1065
1476
|
"지표 스코어카드",
|
|
1477
|
+
"원인/근거",
|
|
1478
|
+
"결과 의미",
|
|
1479
|
+
"데이터셋 차이",
|
|
1066
1480
|
"데이터 품질/신뢰도",
|
|
1067
1481
|
"증거 기반 인사이트",
|
|
1068
1482
|
"원인 가설",
|
|
1069
1483
|
"개선 제안",
|
|
1484
|
+
"개선 방향",
|
|
1070
1485
|
"다음 단계",
|
|
1071
1486
|
"부록(산출물)",
|
|
1072
1487
|
],
|
|
@@ -1230,6 +1645,12 @@ class LLMReportModule(BaseAnalysisModule):
|
|
|
1230
1645
|
except (TypeError, ValueError):
|
|
1231
1646
|
return "-"
|
|
1232
1647
|
|
|
1648
|
+
@staticmethod
|
|
1649
|
+
def _round_if_number(value: Any, precision: int = 4) -> float | None:
|
|
1650
|
+
if isinstance(value, (int, float)):
|
|
1651
|
+
return round(float(value), precision)
|
|
1652
|
+
return None
|
|
1653
|
+
|
|
1233
1654
|
def _render_scorecard_table(self, scorecard: list[dict[str, Any]]) -> list[str]:
|
|
1234
1655
|
if not scorecard:
|
|
1235
1656
|
return ["- 스코어카드 데이터가 없습니다."]
|
|
@@ -1313,6 +1734,7 @@ class LLMReportModule(BaseAnalysisModule):
|
|
|
1313
1734
|
priority_highlights = self._build_priority_highlights(context.get("priority_summary"))
|
|
1314
1735
|
prompt_change_summary = self._summarize_prompt_changes(context.get("change_summary"))
|
|
1315
1736
|
artifact_manifest = self._build_artifact_manifest(context.get("artifact_nodes") or [])
|
|
1737
|
+
artifact_highlights = self._build_artifact_highlights(context)
|
|
1316
1738
|
signal_group_summary = self._build_signal_group_summary(scorecard)
|
|
1317
1739
|
risk_metrics = self._build_risk_metrics(scorecard)
|
|
1318
1740
|
significant_changes = self._build_significant_changes(comparison_scorecard)
|
|
@@ -1363,6 +1785,7 @@ class LLMReportModule(BaseAnalysisModule):
|
|
|
1363
1785
|
"signal_group_summary": signal_group_summary,
|
|
1364
1786
|
"risk_metrics": risk_metrics,
|
|
1365
1787
|
"artifact_manifest": artifact_manifest,
|
|
1788
|
+
"artifact_highlights": artifact_highlights,
|
|
1366
1789
|
}
|
|
1367
1790
|
|
|
1368
1791
|
summary_json = json.dumps(summary_payload, ensure_ascii=False, indent=2)
|
|
@@ -1384,6 +1807,9 @@ class LLMReportModule(BaseAnalysisModule):
|
|
|
1384
1807
|
"6) 개선안마다 기대되는 영향 지표, 검증 방법(실험/재평가), 리스크를 함께 서술\n"
|
|
1385
1808
|
"7) 신뢰도/타당성 제약(표본 수, 커버리지, 유의성, 데이터 변경)을 명시\n"
|
|
1386
1809
|
"8) appendix는 선택이며 비어 있으면 산출물 목록을 자동 추가\n"
|
|
1810
|
+
"9) 금지: 모호한 조언(예: '개선 필요', '점검하세요')만 반복\n"
|
|
1811
|
+
"10) 각 항목은 반드시 '지표명 + 근거 + 영향/개선 방향'을 포함\n"
|
|
1812
|
+
"11) artifact_highlights에서 최소 2개 이상을 인용해 근거를 강화\n"
|
|
1387
1813
|
)
|
|
1388
1814
|
if report_type == "comparison":
|
|
1389
1815
|
requirements = (
|
|
@@ -1391,35 +1817,42 @@ class LLMReportModule(BaseAnalysisModule):
|
|
|
1391
1817
|
"1) 출력 언어: 한국어\n"
|
|
1392
1818
|
"2) 핵심 주장/원인/개선안에는 evidence_id를 [A1]/[B1] 형식으로 인용\n"
|
|
1393
1819
|
"3) JSON 필수 키: summary_sentence, summary_bullets, change_summary, "
|
|
1394
|
-
"statistical_notes,
|
|
1820
|
+
"statistical_notes, reasons, meaning, dataset_deltas, root_causes, "
|
|
1821
|
+
"recommendations, improvement_plan, next_steps\n"
|
|
1395
1822
|
"4) summary_bullets는 3개 권장(지표/변경 사항/사용자 영향)\n"
|
|
1396
1823
|
"5) change_summary에는 데이터셋/설정/프롬프트 변경을 명시\n"
|
|
1397
1824
|
"6) statistical_notes에는 유의한 변화 및 한계(표본/유의성) 포함\n"
|
|
1398
|
-
"7)
|
|
1825
|
+
"7) meaning은 결과가 사용자/업무에 주는 의미를 서술\n"
|
|
1826
|
+
"8) dataset_deltas는 데이터셋 기준 차이를 명확히 기재\n"
|
|
1827
|
+
"9) improvement_plan은 개선 방향+기대 지표+검증 방법+리스크 포함\n"
|
|
1828
|
+
"10) 근거가 부족하면 '추가 데이터 필요'라고 명시\n"
|
|
1399
1829
|
)
|
|
1400
1830
|
elif report_type == "summary":
|
|
1401
1831
|
requirements = (
|
|
1402
1832
|
"요구사항:\n"
|
|
1403
1833
|
"1) 출력 언어: 한국어\n"
|
|
1404
1834
|
"2) 핵심 주장/개선안에는 evidence_id를 [E1] 형식으로 인용\n"
|
|
1405
|
-
"3) JSON 필수 키: summary_sentence, summary_bullets, "
|
|
1406
|
-
"recommendations, next_steps\n"
|
|
1835
|
+
"3) JSON 필수 키: summary_sentence, summary_bullets, reasons, meaning, "
|
|
1836
|
+
"dataset_deltas, recommendations, improvement_plan, next_steps\n"
|
|
1407
1837
|
"4) summary_bullets는 3개 권장\n"
|
|
1408
1838
|
"5) risk_metrics를 활용해 상위 위험 지표 3개를 명확히 언급\n"
|
|
1409
|
-
"6)
|
|
1839
|
+
"6) meaning과 dataset_deltas는 간단히라도 포함\n"
|
|
1840
|
+
"7) improvement_plan은 개선 방향+검증 방법을 포함\n"
|
|
1841
|
+
"8) 근거가 부족하면 '추가 데이터 필요'라고 명시\n"
|
|
1410
1842
|
)
|
|
1411
1843
|
else:
|
|
1412
1844
|
requirements = (
|
|
1413
1845
|
"요구사항:\n"
|
|
1414
1846
|
"1) 출력 언어: 한국어\n"
|
|
1415
1847
|
"2) 핵심 주장/원인/개선안에는 evidence_id를 [E1] 형식으로 인용\n"
|
|
1416
|
-
"3) JSON 필수 키: summary_sentence, summary_bullets, insights, "
|
|
1417
|
-
"root_causes, recommendations, next_steps\n"
|
|
1848
|
+
"3) JSON 필수 키: summary_sentence, summary_bullets, insights, reasons, meaning, "
|
|
1849
|
+
"dataset_deltas, root_causes, recommendations, improvement_plan, next_steps\n"
|
|
1418
1850
|
"4) insights/root_causes에는 evidence_id를 포함\n"
|
|
1419
1851
|
"5) summary_bullets는 3개 권장(지표/원인/사용자 영향)\n"
|
|
1420
1852
|
"6) signal_group_summary로 축별 약점/강점을 분해\n"
|
|
1421
1853
|
"7) 사용자 영향은 신뢰/이해/인지부하 관점으로 1~2문장\n"
|
|
1422
|
-
"8)
|
|
1854
|
+
"8) meaning/dataset_deltas/improvement_plan은 반드시 포함\n"
|
|
1855
|
+
"9) 근거가 부족하면 '추가 데이터 필요'라고 명시\n"
|
|
1423
1856
|
)
|
|
1424
1857
|
|
|
1425
1858
|
return (
|
|
@@ -1568,6 +2001,28 @@ class LLMReportModule(BaseAnalysisModule):
|
|
|
1568
2001
|
report_lines.append(f"- {note}")
|
|
1569
2002
|
report_lines.append("")
|
|
1570
2003
|
|
|
2004
|
+
report_lines.append("## 원인/근거")
|
|
2005
|
+
for item in self._build_comparison_root_causes(change_summary, evidence):
|
|
2006
|
+
report_lines.append(f"- {item}")
|
|
2007
|
+
report_lines.append("")
|
|
2008
|
+
|
|
2009
|
+
report_lines.append("## 결과 의미")
|
|
2010
|
+
for item in self._build_result_meaning(scorecard, {}, evidence):
|
|
2011
|
+
report_lines.append(f"- {item}")
|
|
2012
|
+
report_lines.append("")
|
|
2013
|
+
|
|
2014
|
+
report_lines.append("## 데이터셋 차이")
|
|
2015
|
+
for item in self._build_dataset_deltas(
|
|
2016
|
+
report_type,
|
|
2017
|
+
run_summary,
|
|
2018
|
+
comparison_runs,
|
|
2019
|
+
change_summary,
|
|
2020
|
+
scorecard,
|
|
2021
|
+
evidence,
|
|
2022
|
+
):
|
|
2023
|
+
report_lines.append(f"- {item}")
|
|
2024
|
+
report_lines.append("")
|
|
2025
|
+
|
|
1571
2026
|
report_lines.append("## 원인 분석")
|
|
1572
2027
|
for cause in self._build_comparison_root_causes(change_summary, evidence):
|
|
1573
2028
|
report_lines.append(f"- {cause}")
|
|
@@ -1599,6 +2054,28 @@ class LLMReportModule(BaseAnalysisModule):
|
|
|
1599
2054
|
report_lines.append("- 증거 기반 인사이트가 없습니다. (추가 데이터 필요)")
|
|
1600
2055
|
report_lines.append("")
|
|
1601
2056
|
|
|
2057
|
+
report_lines.append("## 원인/근거")
|
|
2058
|
+
for item in self._build_fallback_insights(scorecard, evidence):
|
|
2059
|
+
report_lines.append(f"- {item}")
|
|
2060
|
+
report_lines.append("")
|
|
2061
|
+
|
|
2062
|
+
report_lines.append("## 결과 의미")
|
|
2063
|
+
for item in self._build_result_meaning(scorecard, {}, evidence):
|
|
2064
|
+
report_lines.append(f"- {item}")
|
|
2065
|
+
report_lines.append("")
|
|
2066
|
+
|
|
2067
|
+
report_lines.append("## 데이터셋 차이")
|
|
2068
|
+
for item in self._build_dataset_deltas(
|
|
2069
|
+
report_type,
|
|
2070
|
+
run_summary,
|
|
2071
|
+
comparison_runs,
|
|
2072
|
+
context.get("change_summary"),
|
|
2073
|
+
scorecard,
|
|
2074
|
+
evidence,
|
|
2075
|
+
):
|
|
2076
|
+
report_lines.append(f"- {item}")
|
|
2077
|
+
report_lines.append("")
|
|
2078
|
+
|
|
1602
2079
|
report_lines.append("## 원인 가설")
|
|
1603
2080
|
root_causes = self._build_root_cause_hypotheses(scorecard, evidence)
|
|
1604
2081
|
if root_causes:
|
|
@@ -1617,6 +2094,11 @@ class LLMReportModule(BaseAnalysisModule):
|
|
|
1617
2094
|
else:
|
|
1618
2095
|
report_lines.append("- 추가 데이터 및 LLM 분석을 통해 상세 원인을 도출하세요.")
|
|
1619
2096
|
|
|
2097
|
+
report_lines.append("")
|
|
2098
|
+
report_lines.append("## 개선 방향")
|
|
2099
|
+
for item in self._build_improvement_plan(scorecard, evidence, recommendations):
|
|
2100
|
+
report_lines.append(f"- {item}")
|
|
2101
|
+
|
|
1620
2102
|
report_lines.append("")
|
|
1621
2103
|
report_lines.append("## 다음 단계")
|
|
1622
2104
|
report_lines.append("- 우선순위 케이스를 대상으로 실험/재평가를 진행하세요.")
|