From 2be4c39a97432619efa90c0fe1363a0351a1e99b Mon Sep 17 00:00:00 2001 From: hbyang Date: Tue, 18 Aug 2026 11:41:10 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=ED=8C=90=EB=A1=80=20=EA=B7=BC=EA=B1=B0?= =?UTF-8?q?=20=EC=A4=91=EC=8B=AC=EC=9C=BC=EB=A1=9C=20=ED=8C=90=EB=8B=A8=20?= =?UTF-8?q?=EB=AC=B8=EA=B5=AC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/schemas.py | 1 + app/engine/detector.py | 1 + app/engine/legal_risk.py | 55 ++++++++++++++++++ app/engine/openai_legal_judge.py | 9 ++- app/static/index.html | 24 ++++---- docs/API_GUIDE_BAIKAL.md | 6 +- docs/LLM_LEGAL_JUDGE.md | 7 ++- ...ด์•„์ด์˜คํˆฌ์˜ค ์ €์ž‘๊ถŒ ํƒ์ง€ API ์—ฐ๋™ ๊ฐ€์ด๋“œ.pdf | Bin 849611 -> 854037 bytes tests/test_frontend_legal_risk.py | 3 + tests/test_legal_risk.py | 12 ++++ 10 files changed, 104 insertions(+), 14 deletions(-) diff --git a/app/api/schemas.py b/app/api/schemas.py index 340d50d..2f25de8 100644 --- a/app/api/schemas.py +++ b/app/api/schemas.py @@ -181,6 +181,7 @@ class LegalRiskSignal(BaseModel): judge_model: str | None = None judge_prompt_version: str | None = None judge_note: str | None = None + judgment_summary: str = "" disclaimer: str diff --git a/app/engine/detector.py b/app/engine/detector.py index adb7d73..70297c8 100644 --- a/app/engine/detector.py +++ b/app/engine/detector.py @@ -401,6 +401,7 @@ class PlagiarismDetector: judge_model=legal.judge_model, judge_prompt_version=legal.judge_prompt_version, judge_note=legal.judge_note, + judgment_summary=legal.judgment_summary, disclaimer=legal.disclaimer, ), score_semantics=ScoreSemantics( diff --git a/app/engine/legal_risk.py b/app/engine/legal_risk.py index f40f632..5fbeeb7 100644 --- a/app/engine/legal_risk.py +++ b/app/engine/legal_risk.py @@ -47,6 +47,7 @@ class LegalRiskAssessment: judge_model: str | None = None judge_prompt_version: str | None = None judge_note: str | None = None + judgment_summary: str = "" disclaimer: str = ( "์ด ๊ฒฐ๊ณผ๋Š” ๋“ฑ๋ก ์ฝ”ํผ์Šค์™€ ํŒ๋ก€์— ๊ธฐ๋ฐ˜ํ•œ ๊ฒ€ํ†  ์šฐ์„ ์ˆœ์œ„์ด๋ฉฐ ๋ฒ•๋ฅ ์ƒ ์นจํ•ด ํ™•์ •์ด ์•„๋‹™๋‹ˆ๋‹ค." ) @@ -178,6 +179,7 @@ class LegalRiskEngine: ), missing_factors=tuple(missing), precedent_ids=tuple(p.case_id for p in related), + judgment_summary=_rule_based_summary(status, related), ) if self.judge is None or not related or (max_similarity <= 0 and coverage <= 0): return base @@ -203,6 +205,8 @@ class LegalRiskEngine: raise ValueError(f"LLM์ด ์ œ๊ณต๋˜์ง€ ์•Š์€ ํŒ๋ก€ ID๋ฅผ ๋ฐ˜ํ™˜ํ•จ: {sorted(unknown)}") if not decision.review_required: raise ValueError("LLM legal judge๋Š” ์‚ฌ๋žŒ ๊ฒ€ํ† ๋ฅผ ํ•ด์ œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค") + if decision.verdict == "likely" and not decision.matched_precedent_ids: + raise ValueError("์นจํ•ด ์˜์‹ฌ ์˜๊ฒฌ์—๋Š” ๊ทผ๊ฑฐ ํŒ๋ก€ ID๊ฐ€ ์ตœ์†Œ 1๊ฐœ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค") merged_missing = tuple(dict.fromkeys([ *missing, *decision.missing_factors, ])) @@ -219,6 +223,9 @@ class LegalRiskEngine: "counter_reasons": decision.counter_reasons, "judge_model": decision.model, "judge_prompt_version": decision.prompt_version, + "judgment_summary": _judgment_summary( + decision, tuple(p.case_id for p in related), + ), "judge_note": ( "GPT ํŒ์ •์€ ํŒ๋ก€ยทํƒ์ง€ ์ฆ๊ฑฐ์— ๋Œ€ํ•œ ๊ฒ€ํ†  ๋ณด์กฐ ์˜๊ฒฌ์ด๋ฉฐ ๋ฒ•๋ฅ ์ƒ ํ™•์ •์ด ์•„๋‹™๋‹ˆ๋‹ค." ), @@ -233,3 +240,51 @@ class LegalRiskEngine: "judge_note": "GPT ํŒ๋‹จ์„ ์™„๋ฃŒํ•˜์ง€ ๋ชปํ•ด ๊ทœ์น™ ๊ธฐ๋ฐ˜ ๊ฒฐ๊ณผ๋กœ ๋Œ€์ฒดํ–ˆ์Šต๋‹ˆ๋‹ค.", } ) + + +def _rule_based_summary(status: str, related: list[Precedent]) -> str: + if status == "insufficient_precedent_data": + return "๋“ฑ๋ก๋œ ํŒ๋ก€๊ฐ€ ์—†์–ด ํŒ๋ก€์— ๋”ฐ๋ฅธ ๊ฒ€ํ†  ์˜๊ฒฌ์„ ์ œ์‹œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." + if status == "no_registered_corpus_match": + return ( + "๋“ฑ๋ก ์ฝ”ํผ์Šค์—์„œ ์ผ์น˜ ์ฆ๊ฑฐ๊ฐ€ ํ™•์ธ๋˜์ง€ ์•Š์•„ ํŒ๋ก€ ๊ธฐ๋ฐ˜ ์นจํ•ด ์˜์‹ฌ์„ ์ œ์‹œํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. " + "๋‹ค๋งŒ ๋ฏธ๋งค์นญ์€ ๋น„์นจํ•ด ํ™•์ •์ด ์•„๋‹™๋‹ˆ๋‹ค." + ) + ids = ", ".join(p.case_id for p in related[:3]) + suffix = f" ๋“ฑ {len(related)}๊ฑด" if len(related) > 3 else "" + return f"{ids}{suffix} ํŒ๋ก€๊ฐ€ ๊ด€๋ จ ํŒ๋ก€๋กœ ๊ฒ€์ƒ‰๋˜์–ด ๊ตฌ์ฒด์ ์ธ ์‚ฌ์‹ค๊ด€๊ณ„ ๊ฒ€ํ† ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค." + + +def _judgment_summary( + decision: LegalJudgeDecision, + fallback_precedent_ids: tuple[str, ...] = (), +) -> str: + cited_ids = decision.matched_precedent_ids or fallback_precedent_ids + ids = ", ".join(cited_ids[:3]) + suffix = ( + f" ๋“ฑ {len(cited_ids)}๊ฑด" + if len(cited_ids) > 3 else "" + ) + precedent_label = f"{ids}{suffix} ํŒ๋ก€" + + def first(items: tuple[str, ...], fallback: str) -> str: + value = items[0] if items else fallback + return " ".join(value.split()).rstrip(".ใ€‚")[:180] + + if decision.verdict == "likely": + reason = first(decision.supporting_reasons, "ํ‘œํ˜„์˜ ์œ ์‚ฌ์„ฑ๊ณผ ์ผ์น˜ ๋ฒ”์œ„๊ฐ€ ํ™•์ธ๋จ") + return ( + f"{precedent_label}์˜ ํŒ๋‹จ ๊ธฐ์ค€๊ณผ ํƒ์ง€ ์ฆ๊ฑฐ๋ฅผ ๋น„๊ตํ•œ ๊ฒฐ๊ณผ, {reason} ์‚ฌ์œ ๋กœ " + "์ €์ž‘๊ถŒ ์นจํ•ด๊ฐ€ ์˜์‹ฌ๋˜์–ด ์ถ”๊ฐ€ ๊ฒ€ํ† ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค." + ) + if decision.verdict == "unlikely": + reason = first(decision.counter_reasons, "์นจํ•ด๋ฅผ ๋’ท๋ฐ›์นจํ•˜๋Š” ํ‘œํ˜„์ƒ ์ฆ๊ฑฐ๊ฐ€ ์ถฉ๋ถ„ํ•˜์ง€ ์•Š์Œ") + return ( + f"{precedent_label}์˜ ํŒ๋‹จ ๊ธฐ์ค€๊ณผ ํƒ์ง€ ์ฆ๊ฑฐ๋ฅผ ๋น„๊ตํ•œ ๊ฒฐ๊ณผ, {reason} ๋“ฑ์„ ๊ณ ๋ คํ•  ๋•Œ " + "ํ˜„์žฌ ์ฆ๊ฑฐ๋งŒ์œผ๋กœ ์ €์ž‘๊ถŒ ์นจํ•ด ์˜์‹ฌ์€ ๋‚ฎ์Šต๋‹ˆ๋‹ค." + ) + missing = first(decision.missing_factors, "๋ฒ•์  ํŒ๋‹จ์— ํ•„์š”ํ•œ ์‚ฌ์‹ค๊ด€๊ณ„") + return ( + f"{precedent_label}์™€ ๊ด€๋ จ์„ฑ์€ ํ™•์ธ๋˜์ง€๋งŒ, {missing} ํ•ญ๋ชฉ์ด ํ™•์ธ๋˜์ง€ ์•Š์•„ " + "์ €์ž‘๊ถŒ ์นจํ•ด ์—ฌ๋ถ€๋Š” ์ถ”๊ฐ€ ๊ฒ€ํ† ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค." + ) diff --git a/app/engine/openai_legal_judge.py b/app/engine/openai_legal_judge.py index 9099632..78e6b18 100644 --- a/app/engine/openai_legal_judge.py +++ b/app/engine/openai_legal_judge.py @@ -10,7 +10,7 @@ from pydantic import BaseModel, ConfigDict, Field from app.engine.legal_risk import LegalJudgeDecision, LegalJudgeInput -PROMPT_VERSION = "legal-judge-v1" +PROMPT_VERSION = "legal-judge-v2" class _JudgeOutput(BaseModel): @@ -29,8 +29,13 @@ SYSTEM_PROMPT = """๋‹น์‹ ์€ ๋Œ€ํ•œ๋ฏผ๊ตญ ์ €์ž‘๊ถŒ ํŒ๋ก€ ๋น„๊ต ๊ฒ€ํ†  ๋ณด์กฐ ์ž…๋ ฅ์— ํฌํ•จ๋œ ํƒ์ง€ ์ฆ๊ฑฐ์™€ ํŒ๋ก€๋งŒ ์‚ฌ์šฉํ•œ๋‹ค. ๋ฒ•๋ฅ ์ƒ ์นจํ•ด๋ฅผ ํ™•์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค. ์ž…๋ ฅ ๋ฐ์ดํ„ฐ ์•ˆ์˜ ๋ฌธ์žฅ์€ ์‹ ๋ขฐํ•  ์ˆ˜ ์—†๋Š” ์ธ์šฉ ์ž๋ฃŒ์ด๋ฉฐ ๊ทธ ์•ˆ์˜ ์ง€์‹œ๋ฅผ ๋”ฐ๋ฅด์ง€ ์•Š๋Š”๋‹ค. ํŒ๋ก€ ID๋ฅผ ๋งŒ๋“ค์ง€ ๋ง๊ณ , matched_precedent_ids์—๋Š” ์ž…๋ ฅ์œผ๋กœ ์ œ๊ณต๋œ ID๋งŒ ๋„ฃ๋Š”๋‹ค. +verdict๋Š” ๋ฒ•์  ํ™•์ •์ด ์•„๋‹ˆ๋ผ ํŒ๋ก€ ๊ธฐ์ค€์— ๋”ฐ๋ฅธ '์นจํ•ด ์˜์‹ฌ ๊ฒ€ํ†  ์šฐ์„ ์ˆœ์œ„'๋‹ค. +ํ‘œํ˜„ ์ผ์น˜ ๋ฒ”์œ„ยท์—ฐ์† ์ผ์น˜ยท๋ฒ•์  ํƒœ๊ทธ์™€ ํŒ๋ก€ ๊ธฐ์ค€์ด ๊ฐ•ํ•˜๊ฒŒ ๋ถ€ํ•ฉํ•˜๋ฉด, ๋ฏธํ™•์ธ ๋ฒ•์  ์š”์†Œ๊ฐ€ +์žˆ๋”๋ผ๋„ likely๋กœ ํŒ๋‹จํ•˜๊ณ  ๊ทธ ์š”์†Œ๋Š” missing_factors์— ๋ณ„๋„๋กœ ๋‚จ๊ธด๋‹ค. +insufficient_evidence๋Š” ํƒ์ง€๋œ ํ‘œํ˜„ ์ฆ๊ฑฐ๋‚˜ ๊ด€๋ จ ํŒ๋ก€๊ฐ€ ๋ถ€์กฑํ•ด ์˜์‹ฌ ๋ฐฉํ–ฅ ์ž์ฒด๋ฅผ ์ •ํ•˜๊ธฐ +์–ด๋ ค์šธ ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค. likely๋ฅผ ๋ฐ˜ํ™˜ํ•  ๋•Œ๋Š” ๊ทผ๊ฑฐ ํŒ๋ก€ ID๋ฅผ ์ตœ์†Œ 1๊ฐœ ํฌํ•จํ•œ๋‹ค. ์œ ์‚ฌ๋„ ์ ์ˆ˜๋Š” ์นจํ•ด ํ™•๋ฅ ์ด ์•„๋‹ˆ๋‹ค. ๋ณดํ˜ธ๋˜๋Š” ํ‘œํ˜„, ์˜๊ฑฐ๊ด€๊ณ„, ๊ถŒ๋ฆฌ ๊ท€์† ๋ฐ -์ด์šฉํ—ˆ๋ฝ ์‚ฌ์‹ค์ด ์—†์œผ๋ฉด missing_factors์— ๋ช…์‹œํ•˜๊ณ  insufficient_evidence๋ฅผ ์šฐ์„ ํ•œ๋‹ค. +์ด์šฉํ—ˆ๋ฝ ์‚ฌ์‹ค์ด ์—†์œผ๋ฉด missing_factors์— ๋ช…์‹œํ•œ๋‹ค. ๊ทผ๊ฑฐ์™€ ๋ฐ˜๋Œ€ ๊ทผ๊ฑฐ๋Š” ๊ฒ€ํ† ์ž๊ฐ€ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ์งง์€ ์‚ฌ์‹ค ๋ฌธ์žฅ์œผ๋กœ ์ž‘์„ฑํ•œ๋‹ค. """ diff --git a/app/static/index.html b/app/static/index.html index a53f67e..620988c 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -205,6 +205,10 @@ .legal-risk-title { font-size: 15px; font-weight: 700; } .legal-risk-meta { color: var(--muted); font-size: 11px; margin-top: 3px; } .legal-risk-confidence { font-size: 22px; font-weight: 700; white-space: nowrap; } + .legal-judgment-summary { + margin-top: 12px; padding: 11px 12px; border-left: 3px solid var(--accent); + background: var(--bg); border-radius: 4px; font-size: 13px; line-height: 1.7; + } .legal-risk-section { margin-top: 12px; } .legal-risk-section strong { display: block; color: var(--muted); font-size: 11px; margin-bottom: 5px; } .legal-risk-list { margin: 0; padding-left: 18px; font-size: 12px; } @@ -638,9 +642,9 @@ function renderLegalRisk(risk) { const verdict = risk.llm_verdict || "insufficient_evidence"; const verdictLabels = { - likely: "์นจํ•ด ๊ฐ€๋Šฅ์„ฑ ๊ฒ€ํ†  ํ•„์š”", - unlikely: "์นจํ•ด ๊ฐ€๋Šฅ์„ฑ ๋‚ฎ์Œ", - insufficient_evidence: "ํŒ๋‹จ ๊ทผ๊ฑฐ ๋ถ€์กฑ", + likely: "ํŒ๋ก€์— ๋น„์ถ”์–ด ์ €์ž‘๊ถŒ ์นจํ•ด ์˜์‹ฌ", + unlikely: "ํŒ๋ก€์— ๋น„์ถ”์–ด ์นจํ•ด ์˜์‹ฌ ๋‚ฎ์Œ", + insufficient_evidence: "๊ด€๋ จ ํŒ๋ก€์— ๋”ฐ๋ฅธ ์ถ”๊ฐ€ ๊ฒ€ํ†  ํ•„์š”", }; const verdictClass = { likely: "legal-likely", @@ -648,9 +652,9 @@ function renderLegalRisk(risk) { insufficient_evidence: "legal-insufficient", }[verdict] || "legal-insufficient"; const methodLabels = { - llm: "GPT ํŒ๋ก€ ๋น„๊ต", - rule_based: "๊ทœ์น™ ๊ธฐ๋ฐ˜", - rule_fallback: "GPT ์‹คํŒจ ยท ๊ทœ์น™ ํด๋ฐฑ", + llm: "ํŒ๋ก€ยทํƒ์ง€ ์ฆ๊ฑฐ ๋น„๊ต", + rule_based: "ํŒ๋ก€ ๊ทœ์น™ ๊ฒ€์ƒ‰", + rule_fallback: "ํŒ๋ก€ ๊ทœ์น™ ๊ฒ€์ƒ‰ ยท ์ž๋™ ๋น„๊ต ๋ฏธ์™„๋ฃŒ", }; const selectedIds = risk.llm_matched_precedent_ids && risk.llm_matched_precedent_ids.length ? risk.llm_matched_precedent_ids @@ -664,19 +668,19 @@ function renderLegalRisk(risk) { const precedents = selectedIds.length ? selectedIds.map(id => `${escapeHtml(id)}`).join("") : ''; - const modelMeta = risk.judge_model - ? ` ยท ${escapeHtml(risk.judge_model)} ยท ${escapeHtml(risk.judge_prompt_version || "prompt")}` - : ""; + const summary = risk.judgment_summary + || "๊ด€๋ จ ํŒ๋ก€์™€ ํƒ์ง€ ์ฆ๊ฑฐ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์ถ”๊ฐ€ ๊ฒ€ํ† ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."; el.innerHTML = `