structverify 0.3.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.
- structverify/__init__.py +83 -0
- structverify/adaptation/__init__.py +0 -0
- structverify/adaptation/adapter_trainer.py +341 -0
- structverify/adaptation/feedback_store.py +31 -0
- structverify/adaptation/kosis_crawler.py +317 -0
- structverify/adaptation/sample_builder.py +149 -0
- structverify/adaptation/synthetic_generator.py +320 -0
- structverify/adaptation/update_embeddings.py +178 -0
- structverify/agent/__init__.py +21 -0
- structverify/agent/builder_agent.py +226 -0
- structverify/agent/conformance_agent.py +171 -0
- structverify/agent/dependency_planner.py +151 -0
- structverify/agent/indexing_agent.py +153 -0
- structverify/agent/indexing_planner.py +169 -0
- structverify/agent/integration_example.py +182 -0
- structverify/agent/loop.py +1165 -0
- structverify/agent/memory.py +207 -0
- structverify/agent/planner.py +817 -0
- structverify/agent/prompts/__init__.py +15 -0
- structverify/agent/prompts/planner_prompts.py +219 -0
- structverify/agent/prompts/reflect_prompts.py +387 -0
- structverify/agent/reflect.py +227 -0
- structverify/agent/runtime_agent.py +1272 -0
- structverify/agent/schemas.py +262 -0
- structverify/agent/source_profiler.py +229 -0
- structverify/agent/tools/__init__.py +64 -0
- structverify/agent/tools/base.py +222 -0
- structverify/agent/tools/calculate.py +244 -0
- structverify/agent/tools/catalog_search.py +859 -0
- structverify/agent/tools/deep_explore.py +293 -0
- structverify/agent/tools/explore_catalog.py +423 -0
- structverify/agent/tools/fetch_evidence.py +922 -0
- structverify/agent/tools/finish.py +423 -0
- structverify/agent/tools/meta_explore.py +267 -0
- structverify/agent/tools/query_rewriter.py +134 -0
- structverify/agent/tools/read_original.py +144 -0
- structverify/agent/tools/replan.py +365 -0
- structverify/agent/workspace.py +958 -0
- structverify/api.py +804 -0
- structverify/config/default.yaml +350 -0
- structverify/core/__init__.py +0 -0
- structverify/core/config_loader.py +30 -0
- structverify/core/pipeline.py +280 -0
- structverify/core/schemas.py +362 -0
- structverify/detection/__init__.py +26 -0
- structverify/detection/_config.py +163 -0
- structverify/detection/_llm.py +24 -0
- structverify/detection/candidate/__init__.py +1 -0
- structverify/detection/candidate/heuristic.py +60 -0
- structverify/detection/candidate/llm.py +51 -0
- structverify/detection/candidate_scorer.py +81 -0
- structverify/detection/claim_detector.py +164 -0
- structverify/detection/claims/__init__.py +1 -0
- structverify/detection/claims/worthiness.py +142 -0
- structverify/detection/domain/__init__.py +1 -0
- structverify/detection/domain/classify.py +84 -0
- structverify/detection/domain/preview.py +36 -0
- structverify/detection/domain/registry.py +99 -0
- structverify/detection/domain_classifier.py +75 -0
- structverify/detection/prompts/__init__.py +1 -0
- structverify/detection/prompts/candidate.py +38 -0
- structverify/detection/prompts/claim_worthiness.py +48 -0
- structverify/detection/prompts/domain.py +41 -0
- structverify/detection/prompts/schema.py +508 -0
- structverify/detection/prompts_loader.py +167 -0
- structverify/detection/schema/__init__.py +1 -0
- structverify/detection/schema/expand.py +83 -0
- structverify/detection/schema/induce.py +441 -0
- structverify/detection/schema/regenerate.py +162 -0
- structverify/detection/schema/temporal_hints.py +130 -0
- structverify/detection/schema/validate.py +193 -0
- structverify/detection/schema_inductor.py +112 -0
- structverify/detection/synthetic_generator.py +270 -0
- structverify/explanation/__init__.py +0 -0
- structverify/explanation/_config.py +18 -0
- structverify/explanation/_llm.py +25 -0
- structverify/explanation/explainer.py +183 -0
- structverify/explanation/fallback.py +29 -0
- structverify/explanation/formatters.py +75 -0
- structverify/explanation/prompts/__init__.py +1 -0
- structverify/explanation/prompts/match.py +27 -0
- structverify/explanation/prompts/mismatch.py +20 -0
- structverify/explanation/prompts/multihop.py +16 -0
- structverify/explanation/prompts/unverifiable.py +17 -0
- structverify/graph/__init__.py +0 -0
- structverify/graph/claim_graph.py +226 -0
- structverify/graph/document_graph.py +487 -0
- structverify/graph/graph_builder.py +238 -0
- structverify/graph/graph_multihop.py +335 -0
- structverify/graph/graph_store.py +281 -0
- structverify/graph/provenance.py +52 -0
- structverify/memory/__init__.py +44 -0
- structverify/memory/agent_memory.py +142 -0
- structverify/memory/embedder.py +69 -0
- structverify/memory/exemplar_store.py +241 -0
- structverify/memory/normalizer.py +91 -0
- structverify/memory/schema.py +119 -0
- structverify/memory/storage/__init__.py +29 -0
- structverify/memory/storage/jsonl_store.py +117 -0
- structverify/memory/working_memory.py +370 -0
- structverify/preprocessing/Dockerfile.scraper +27 -0
- structverify/preprocessing/__init__.py +0 -0
- structverify/preprocessing/extractor.py +574 -0
- structverify/preprocessing/pdf/__init__.py +16 -0
- structverify/preprocessing/pdf/fields.py +95 -0
- structverify/preprocessing/pdf/markdown.py +107 -0
- structverify/preprocessing/pdf/models.py +34 -0
- structverify/preprocessing/pdf/ocr.py +172 -0
- structverify/preprocessing/pdf/pipeline.py +74 -0
- structverify/preprocessing/pdf/reader.py +119 -0
- structverify/preprocessing/pdf/scoring.py +61 -0
- structverify/preprocessing/scraper_sandbox.py +561 -0
- structverify/preprocessing/segmenter.py +48 -0
- structverify/preprocessing/sir_builder.py +240 -0
- structverify/progress.py +591 -0
- structverify/retrieval/__init__.py +0 -0
- structverify/retrieval/base.py +208 -0
- structverify/retrieval/base_connector.py +85 -0
- structverify/retrieval/catalog_ranker.py +300 -0
- structverify/retrieval/catalog_search.py +583 -0
- structverify/retrieval/chunking.py +92 -0
- structverify/retrieval/custom_csv_source.py +386 -0
- structverify/retrieval/custom_db_source.py +396 -0
- structverify/retrieval/custom_docs_source.py +152 -0
- structverify/retrieval/dimension_resolver.py +281 -0
- structverify/retrieval/evidence_subgraph.py +63 -0
- structverify/retrieval/kosis_connector.py +1192 -0
- structverify/retrieval/kosis_relevance.py +142 -0
- structverify/retrieval/kosis_source.py +1541 -0
- structverify/retrieval/query_builder.py +72 -0
- structverify/retrieval/registry.py +133 -0
- structverify/retrieval/relevance_judge.py +141 -0
- structverify/retrieval/row_matcher.py +267 -0
- structverify/storage/__init__.py +0 -0
- structverify/storage/db_manager.py +157 -0
- structverify/storage/dwh_manager.py +92 -0
- structverify/storage/init_db.py +99 -0
- structverify/storage/raw_storage.py +29 -0
- structverify/training/__init__.py +26 -0
- structverify/training/curator.py +124 -0
- structverify/training/dataset.py +134 -0
- structverify/training/doctor.py +99 -0
- structverify/training/evalgate.py +96 -0
- structverify/training/generate.py +101 -0
- structverify/training/loop.py +116 -0
- structverify/training/recipe/train_mlx.py +99 -0
- structverify/training/recipe/train_qlora.py +104 -0
- structverify/training/tasks.py +79 -0
- structverify/utils/__init__.py +0 -0
- structverify/utils/embedding_client.py +248 -0
- structverify/utils/llm_client.py +809 -0
- structverify/utils/logger.py +81 -0
- structverify/verification/__init__.py +0 -0
- structverify/verification/_config.py +45 -0
- structverify/verification/adapters.py +405 -0
- structverify/verification/conformance.py +117 -0
- structverify/verification/decide_verdict.py +216 -0
- structverify/verification/decide_verdict_agent.py +454 -0
- structverify/verification/growth_diff.py +267 -0
- structverify/verification/row_match.py +345 -0
- structverify/verification/units.py +64 -0
- structverify/verification/verdict_thresholds.py +232 -0
- structverify/verification/verifier.py +84 -0
- structverify-0.3.0.dist-info/METADATA +903 -0
- structverify-0.3.0.dist-info/RECORD +168 -0
- structverify-0.3.0.dist-info/WHEEL +5 -0
- structverify-0.3.0.dist-info/licenses/LICENSE +21 -0
- structverify-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
# 수정자: 박재윤
|
|
3
|
+
# 수정 날짜: 2026-04-26
|
|
4
|
+
# 수정 내용: __init__ psycopg2 연결 구현, save_claims INSERT 구현
|
|
5
|
+
|
|
6
|
+
# [DONE] __init__ DB 연결 초기화
|
|
7
|
+
# [DONE] save_claims 배치 INSERT 구현
|
|
8
|
+
# [DONE] save_results 배치 INSERT 구현 (claimed_value, true_value, deviation 계산 포함)
|
|
9
|
+
# [DONE] save_claims ON CONFLICT (claim_id) DO NOTHING 추가 (중복 실행 방지)
|
|
10
|
+
# [DONE] save_claims/save_results 매번 새 연결로 변경 (long-running 연결 끊김 방지)
|
|
11
|
+
# [DONE] save_claims domain 파라미터 추가
|
|
12
|
+
# [DONE] save_claims/save_results 재실행 시 기존 데이터 삭제 후 재삽입
|
|
13
|
+
# [DONE] save_claims source_type 파라미터 추가
|
|
14
|
+
# [TODO] save_document 구현
|
|
15
|
+
# [TODO] save_feedback 구현
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
import os
|
|
19
|
+
from structverify.core.schemas import SIRDocument, Claim, VerificationResult
|
|
20
|
+
from structverify.utils.logger import get_logger
|
|
21
|
+
|
|
22
|
+
logger = get_logger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DBManager:
|
|
26
|
+
def __init__(self, config: dict | None = None):
|
|
27
|
+
self.config = config or {}
|
|
28
|
+
from dotenv import load_dotenv
|
|
29
|
+
load_dotenv()
|
|
30
|
+
|
|
31
|
+
def _get_conn(self):
|
|
32
|
+
import psycopg2
|
|
33
|
+
return psycopg2.connect(
|
|
34
|
+
host=os.getenv("POSTGRES_HOST"),
|
|
35
|
+
port=os.getenv("POSTGRES_PORT"),
|
|
36
|
+
dbname=os.getenv("POSTGRES_DB"),
|
|
37
|
+
user=os.getenv("POSTGRES_USER"),
|
|
38
|
+
password=os.getenv("POSTGRES_PASSWORD")
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
async def save_document(self, doc: SIRDocument) -> None:
|
|
42
|
+
"""
|
|
43
|
+
SIR 문서를 PostgreSQL에 저장한다.
|
|
44
|
+
|
|
45
|
+
TODO [박재윤]: documents 테이블 INSERT 구현
|
|
46
|
+
INSERT INTO documents (doc_id, source_type, source_uri, sir_json, extracted_at)
|
|
47
|
+
VALUES ($1, $2, $3, $4::jsonb, $5)
|
|
48
|
+
ON CONFLICT (doc_id) DO NOTHING
|
|
49
|
+
|
|
50
|
+
- doc_id: str(doc.doc_id)
|
|
51
|
+
- source_type: doc.source_type.value
|
|
52
|
+
- sir_json: doc.model_dump_json() 또는 json.dumps
|
|
53
|
+
- 비동기 세션 사용 (async with self.AsyncSession() as session)
|
|
54
|
+
"""
|
|
55
|
+
logger.warning(f"DB 저장 stub: doc {doc.doc_id}")
|
|
56
|
+
|
|
57
|
+
async def save_claims(self, claims: list[Claim], domain: str = None, source_type: str = None) -> None:
|
|
58
|
+
# [v3] - 박재윤: 매번 새 연결 + 재실행 시 기존 데이터 삭제 후 재삽입
|
|
59
|
+
# [v4] - 박재윤: source_type 파라미터 추가
|
|
60
|
+
conn = self._get_conn()
|
|
61
|
+
cur = conn.cursor()
|
|
62
|
+
|
|
63
|
+
if claims:
|
|
64
|
+
request_id = str(claims[0].doc_id)
|
|
65
|
+
# [v3] - 박재윤: 같은 기사 재실행 시 기존 데이터 삭제 후 재삽입
|
|
66
|
+
cur.execute(
|
|
67
|
+
"DELETE FROM results WHERE claim_id IN (SELECT claim_id FROM claims WHERE request_id = %s)",
|
|
68
|
+
(request_id,)
|
|
69
|
+
)
|
|
70
|
+
cur.execute("DELETE FROM claims WHERE request_id = %s", (request_id,))
|
|
71
|
+
cur.execute(
|
|
72
|
+
"INSERT INTO requests (request_id, source_type, domain, submitted_at) VALUES (%s, %s, %s, NOW()) ON CONFLICT (request_id) DO UPDATE SET source_type = EXCLUDED.source_type, domain = EXCLUDED.domain, submitted_at = NOW()",
|
|
73
|
+
(request_id, source_type, domain)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
for claim in claims:
|
|
77
|
+
indicator = claim.schema.indicator if claim.schema else None
|
|
78
|
+
value = claim.schema.value if claim.schema else None
|
|
79
|
+
unit = claim.schema.unit if claim.schema else None
|
|
80
|
+
time_ref = claim.schema.time_period if claim.schema else None
|
|
81
|
+
|
|
82
|
+
cur.execute("""
|
|
83
|
+
INSERT INTO claims (claim_id, request_id, field_name, field_value,
|
|
84
|
+
unit, is_approximate, modifier, parent_path,
|
|
85
|
+
time_reference, context)
|
|
86
|
+
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
87
|
+
""", (
|
|
88
|
+
str(claim.claim_id), str(claim.doc_id),
|
|
89
|
+
indicator, value, unit,
|
|
90
|
+
False, None, None,
|
|
91
|
+
time_ref, claim.claim_text
|
|
92
|
+
))
|
|
93
|
+
|
|
94
|
+
conn.commit()
|
|
95
|
+
cur.close()
|
|
96
|
+
conn.close()
|
|
97
|
+
logger.info(f"Claims 저장 완료: {len(claims)}건")
|
|
98
|
+
|
|
99
|
+
async def save_results(self, results: list[VerificationResult], claims: list[Claim] = None) -> None:
|
|
100
|
+
# [v3] - 박재윤: 매번 새 연결 + 재실행 시 기존 데이터 삭제 후 재삽입
|
|
101
|
+
conn = self._get_conn()
|
|
102
|
+
cur = conn.cursor()
|
|
103
|
+
|
|
104
|
+
if results:
|
|
105
|
+
# 기존 results 삭제 (재실행 시 중복 방지)
|
|
106
|
+
claim_ids = [str(r.claim_id) for r in results]
|
|
107
|
+
cur.execute(
|
|
108
|
+
"DELETE FROM results WHERE claim_id = ANY(%s)", (claim_ids,)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
claim_value_map = {}
|
|
112
|
+
if claims:
|
|
113
|
+
for c in claims:
|
|
114
|
+
if c.schema and c.schema.value is not None:
|
|
115
|
+
claim_value_map[str(c.claim_id)] = c.schema.value
|
|
116
|
+
|
|
117
|
+
for result in results:
|
|
118
|
+
ev = result.evidence
|
|
119
|
+
claimed_value = claim_value_map.get(str(result.claim_id))
|
|
120
|
+
true_value = ev.official_value if ev else None
|
|
121
|
+
|
|
122
|
+
deviation = None
|
|
123
|
+
if claimed_value and true_value and true_value != 0:
|
|
124
|
+
deviation = abs(claimed_value - true_value) / abs(true_value)
|
|
125
|
+
|
|
126
|
+
cur.execute("""
|
|
127
|
+
INSERT INTO results (result_id, claim_id, truth_id,
|
|
128
|
+
claimed_value, true_value, deviation,
|
|
129
|
+
match_status, reason, explanation, judged_at)
|
|
130
|
+
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
131
|
+
""", (
|
|
132
|
+
str(result.result_id),
|
|
133
|
+
str(result.claim_id),
|
|
134
|
+
None,
|
|
135
|
+
claimed_value,
|
|
136
|
+
true_value,
|
|
137
|
+
deviation,
|
|
138
|
+
result.verdict.value,
|
|
139
|
+
result.mismatch_type.value if result.mismatch_type else None,
|
|
140
|
+
result.explanation,
|
|
141
|
+
result.created_at,
|
|
142
|
+
))
|
|
143
|
+
|
|
144
|
+
conn.commit()
|
|
145
|
+
cur.close()
|
|
146
|
+
conn.close()
|
|
147
|
+
logger.info(f"Results 저장 완료: {len(results)}건")
|
|
148
|
+
|
|
149
|
+
async def save_feedback(self, event) -> None:
|
|
150
|
+
"""
|
|
151
|
+
TODO [박재윤]: feedback_events 테이블 INSERT 구현
|
|
152
|
+
INSERT INTO feedback_events
|
|
153
|
+
(event_id, claim_id, feedback_type, original_verdict,
|
|
154
|
+
corrected_verdict, reviewer_note, created_at)
|
|
155
|
+
VALUES ...
|
|
156
|
+
"""
|
|
157
|
+
logger.warning(f"DB 저장 stub: feedback {event}")
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
storage/dwh_manager.py — DWH 매니저 (Snowflake / BigQuery / ClickHouse)
|
|
3
|
+
|
|
4
|
+
검증 결과 로그, 모델 성능, 비용 데이터를 분석 계층에 적재한다.
|
|
5
|
+
|
|
6
|
+
[박재윤]
|
|
7
|
+
- Snowflake connector 연결 및 executemany INSERT 구현 담당
|
|
8
|
+
- verification_logs, model_metrics, llm_cost_logs 테이블 적재
|
|
9
|
+
- init_snowflake.sql의 테이블 구조에 맞춰 구현
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
import os
|
|
13
|
+
from typing import Any
|
|
14
|
+
from structverify.utils.logger import get_logger
|
|
15
|
+
|
|
16
|
+
logger = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DWHManager:
|
|
20
|
+
def __init__(self, config: dict | None = None):
|
|
21
|
+
self.config = config or {}
|
|
22
|
+
self.provider = self.config.get("provider", "snowflake")
|
|
23
|
+
|
|
24
|
+
def _get_snowflake_conn(self):
|
|
25
|
+
"""
|
|
26
|
+
Snowflake 연결 생성
|
|
27
|
+
|
|
28
|
+
TODO [박재윤]: snowflake.connector.connect() 구현
|
|
29
|
+
import snowflake.connector
|
|
30
|
+
cfg = self.config.get("snowflake", {})
|
|
31
|
+
return snowflake.connector.connect(
|
|
32
|
+
account=os.environ.get(cfg.get("account_env", "SNOWFLAKE_ACCOUNT")),
|
|
33
|
+
user=os.environ.get(cfg.get("user_env", "SNOWFLAKE_USER")),
|
|
34
|
+
password=os.environ.get(cfg.get("password_env", "SNOWFLAKE_PASSWORD")),
|
|
35
|
+
warehouse=cfg.get("warehouse", "STRUCTVERIFY_WH"),
|
|
36
|
+
database=cfg.get("database", "STRUCTVERIFY_DB"),
|
|
37
|
+
schema=cfg.get("schema", "PUBLIC"),
|
|
38
|
+
)
|
|
39
|
+
"""
|
|
40
|
+
logger.warning("Snowflake 연결 stub")
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
async def load_verification_logs(self, records: list[dict[str, Any]]) -> None:
|
|
44
|
+
"""
|
|
45
|
+
검증 결과 로그를 DWH에 적재한다.
|
|
46
|
+
|
|
47
|
+
TODO [박재윤]: Snowflake executemany 배치 INSERT 구현
|
|
48
|
+
conn = self._get_snowflake_conn()
|
|
49
|
+
cursor = conn.cursor()
|
|
50
|
+
sql =
|
|
51
|
+
INSERT INTO verification_logs
|
|
52
|
+
(result_id, claim_id, verdict, confidence, mismatch_type,
|
|
53
|
+
domain, model_version, latency_ms, created_at)
|
|
54
|
+
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
55
|
+
rows = [
|
|
56
|
+
(r["result_id"], r["claim_id"], r["verdict"], r["confidence"],
|
|
57
|
+
r.get("mismatch_type"), r.get("domain"), r.get("model_version"),
|
|
58
|
+
r.get("latency_ms"), r.get("created_at"))
|
|
59
|
+
for r in records
|
|
60
|
+
]
|
|
61
|
+
cursor.executemany(sql, rows)
|
|
62
|
+
conn.commit()
|
|
63
|
+
cursor.close()
|
|
64
|
+
conn.close()
|
|
65
|
+
|
|
66
|
+
TODO [박재윤]: BigQuery/ClickHouse 분기 구현 (provider 설정에 따라)
|
|
67
|
+
"""
|
|
68
|
+
logger.warning(f"DWH 적재 stub ({self.provider}): {len(records)} records")
|
|
69
|
+
|
|
70
|
+
async def load_model_metrics(self, metrics: dict[str, Any]) -> None:
|
|
71
|
+
"""
|
|
72
|
+
모델 성능 지표를 DWH에 적재한다.
|
|
73
|
+
|
|
74
|
+
TODO [박재윤]: model_metrics 테이블 INSERT 구현
|
|
75
|
+
INSERT INTO model_metrics
|
|
76
|
+
(job_id, domain, model_version, eval_score, sample_count, created_at)
|
|
77
|
+
VALUES (...)
|
|
78
|
+
"""
|
|
79
|
+
logger.warning(f"DWH 모델 지표 stub: {metrics}")
|
|
80
|
+
|
|
81
|
+
async def load_llm_costs(self, cost_records: list[dict[str, Any]]) -> None:
|
|
82
|
+
"""
|
|
83
|
+
LLM 호출 비용 데이터를 DWH에 적재한다.
|
|
84
|
+
|
|
85
|
+
TODO [박재윤]: llm_cost_logs 테이블 INSERT 구현
|
|
86
|
+
INSERT INTO llm_cost_logs
|
|
87
|
+
(call_id, provider, model, input_tokens, output_tokens,
|
|
88
|
+
cost_usd, task_type, created_at)
|
|
89
|
+
VALUES (...)
|
|
90
|
+
- Langfuse 연동 시 자동 수집 가능
|
|
91
|
+
"""
|
|
92
|
+
logger.warning(f"DWH 비용 stub: {len(cost_records)} records")
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# 아래 스크립 트 파일 CreateTable, DbConnect,openapiyes/noSave.test.py 전부 베이스라인 코드에 침투예정.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
# 수정자: 박재윤
|
|
5
|
+
# 수정 날짜: 2026-04-26
|
|
6
|
+
# 수정 내용: requests, claims, truths, results 테이블 생성 스크립트 구현
|
|
7
|
+
|
|
8
|
+
# [DONE] CREATE TABLE 4개 구현
|
|
9
|
+
# [DONE] pgvector extension 추가
|
|
10
|
+
# [DONE] kosis_stat_catalog 테이블 추가
|
|
11
|
+
"""
|
|
12
|
+
import psycopg2
|
|
13
|
+
from dotenv import load_dotenv
|
|
14
|
+
import os
|
|
15
|
+
|
|
16
|
+
load_dotenv()
|
|
17
|
+
|
|
18
|
+
conn = psycopg2.connect(
|
|
19
|
+
host=os.getenv("POSTGRES_HOST"),
|
|
20
|
+
port=os.getenv("POSTGRES_PORT"),
|
|
21
|
+
dbname=os.getenv("POSTGRES_DB"),
|
|
22
|
+
user=os.getenv("POSTGRES_USER"),
|
|
23
|
+
password=os.getenv("POSTGRES_PASSWORD")
|
|
24
|
+
)
|
|
25
|
+
print("PostgreSQL 연결 확인")
|
|
26
|
+
print(f"Host: {os.getenv('POSTGRES_HOST')}, Port: {os.getenv('POSTGRES_PORT')}, DB: {os.getenv('POSTGRES_DB')}, User: {os.getenv('POSTGRES_USER')}")
|
|
27
|
+
|
|
28
|
+
cur = conn.cursor()
|
|
29
|
+
cur.execute("SELECT version();")
|
|
30
|
+
version = cur.fetchone()
|
|
31
|
+
print(f"PostgreSQL 버전: {version[0]}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
cur.execute("""
|
|
35
|
+
CREATE EXTENSION IF NOT EXISTS vector;
|
|
36
|
+
|
|
37
|
+
CREATE TABLE IF NOT EXISTS requests (
|
|
38
|
+
request_id VARCHAR PRIMARY KEY,
|
|
39
|
+
source_type VARCHAR,
|
|
40
|
+
domain VARCHAR,
|
|
41
|
+
raw_data JSONB,
|
|
42
|
+
submitted_at TIMESTAMP DEFAULT NOW()
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
CREATE TABLE IF NOT EXISTS claims (
|
|
46
|
+
claim_id VARCHAR PRIMARY KEY,
|
|
47
|
+
request_id VARCHAR REFERENCES requests(request_id),
|
|
48
|
+
field_name VARCHAR,
|
|
49
|
+
field_value FLOAT,
|
|
50
|
+
unit VARCHAR,
|
|
51
|
+
is_approximate BOOLEAN DEFAULT FALSE,
|
|
52
|
+
modifier VARCHAR,
|
|
53
|
+
parent_path VARCHAR,
|
|
54
|
+
time_reference VARCHAR,
|
|
55
|
+
context TEXT
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE TABLE IF NOT EXISTS truths (
|
|
59
|
+
truth_id VARCHAR PRIMARY KEY,
|
|
60
|
+
request_id VARCHAR REFERENCES requests(request_id),
|
|
61
|
+
field_name VARCHAR,
|
|
62
|
+
field_value FLOAT,
|
|
63
|
+
unit VARCHAR,
|
|
64
|
+
parent_path VARCHAR,
|
|
65
|
+
time_reference VARCHAR,
|
|
66
|
+
source VARCHAR
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
CREATE TABLE IF NOT EXISTS results (
|
|
70
|
+
result_id VARCHAR PRIMARY KEY,
|
|
71
|
+
claim_id VARCHAR REFERENCES claims(claim_id),
|
|
72
|
+
truth_id VARCHAR REFERENCES truths(truth_id),
|
|
73
|
+
claimed_value FLOAT,
|
|
74
|
+
true_value FLOAT,
|
|
75
|
+
deviation FLOAT,
|
|
76
|
+
match_status VARCHAR,
|
|
77
|
+
reason TEXT,
|
|
78
|
+
explanation TEXT,
|
|
79
|
+
judged_at TIMESTAMP DEFAULT NOW()
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
CREATE TABLE IF NOT EXISTS kosis_stat_catalog (
|
|
83
|
+
stat_id VARCHAR PRIMARY KEY,
|
|
84
|
+
stat_name VARCHAR,
|
|
85
|
+
org_id VARCHAR,
|
|
86
|
+
org_name VARCHAR,
|
|
87
|
+
category_path VARCHAR,
|
|
88
|
+
keywords TEXT[],
|
|
89
|
+
embedding vector(1024),
|
|
90
|
+
raw_meta_json JSONB,
|
|
91
|
+
fetched_at TIMESTAMP DEFAULT NOW()
|
|
92
|
+
);
|
|
93
|
+
""")
|
|
94
|
+
|
|
95
|
+
conn.commit()
|
|
96
|
+
cur.close()
|
|
97
|
+
conn.close()
|
|
98
|
+
|
|
99
|
+
print("테이블 5개 생성 완료")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
storage/raw_storage.py — 원본 파일 저장 (S3 / MinIO)
|
|
3
|
+
|
|
4
|
+
원본 PDF, HTML, DOCX 파일을 절대 수정/삭제하지 않고 보존한다.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
from structverify.utils.logger import get_logger
|
|
8
|
+
|
|
9
|
+
logger = get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RawStorage:
|
|
13
|
+
def __init__(self, config: dict | None = None):
|
|
14
|
+
self.config = config or {}
|
|
15
|
+
# TODO: boto3 S3 client 또는 MinIO client 초기화
|
|
16
|
+
|
|
17
|
+
async def save(self, doc_id: str, data: bytes, filename: str) -> str:
|
|
18
|
+
"""
|
|
19
|
+
원본 파일을 S3/MinIO에 저장하고 key를 반환한다.
|
|
20
|
+
TODO: s3_client.put_object(Bucket=bucket, Key=key, Body=data) 구현
|
|
21
|
+
"""
|
|
22
|
+
key = f"raw/{doc_id}/{filename}"
|
|
23
|
+
logger.warning(f"Raw Storage 저장 stub: {key}")
|
|
24
|
+
return key
|
|
25
|
+
|
|
26
|
+
async def get(self, key: str) -> bytes:
|
|
27
|
+
"""TODO: s3_client.get_object() 구현"""
|
|
28
|
+
logger.warning(f"Raw Storage 조회 stub: {key}")
|
|
29
|
+
return b""
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""structverify.training — 감독 학습 루프 (계획 기능).
|
|
2
|
+
|
|
3
|
+
OSS 설계: 차별화되는 부분(데이터 준비·품질·이상감지·자가평가)은 이 모듈이 소유하고,
|
|
4
|
+
실제 파인튜닝은 표준 데이터셋(chat jsonl)으로 넘겨 생태계(TRL/unsloth/axolotl)에 위임한다.
|
|
5
|
+
|
|
6
|
+
코어(GPU 불필요, 여기서 동작): dataset export · DataCurator · TrainDoctor · EvalGate · LearningLoop
|
|
7
|
+
GPU 필요: recipe/train_qlora.py (핸드오프 or [training] extra)
|
|
8
|
+
|
|
9
|
+
from structverify.training import LearningLoop, EvalGate
|
|
10
|
+
loop = LearningLoop(engine).add_seed()
|
|
11
|
+
ds, _ = loop.prepare("train.jsonl") # ① 데이터 + 🕵 품질검사
|
|
12
|
+
loop.train(ds, "./adapter", run=False) # ② GPU 머신에서 실행할 명령 반환
|
|
13
|
+
loop.diagnose("./adapter/trainer_state.json")# 🩺 로그 진단
|
|
14
|
+
# loop.evaluate(eval_set, tuned_engine=...) # 🚦 자가평가 → 채택/거부
|
|
15
|
+
"""
|
|
16
|
+
from .curator import DataCurator, CurationReport
|
|
17
|
+
from .dataset import build_seed_dataset, export_dataset, read_jsonl, write_jsonl
|
|
18
|
+
from .doctor import TrainDoctor, Diagnosis
|
|
19
|
+
from .evalgate import EvalGate, EvalResult, GateDecision
|
|
20
|
+
from .loop import LearningLoop
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"LearningLoop", "DataCurator", "TrainDoctor", "EvalGate",
|
|
24
|
+
"CurationReport", "Diagnosis", "EvalResult", "GateDecision",
|
|
25
|
+
"build_seed_dataset", "export_dataset", "read_jsonl", "write_jsonl",
|
|
26
|
+
]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""structverify.training.curator — DataCurator (학습셋 품질 검사, 코어·GPU 불필요).
|
|
2
|
+
|
|
3
|
+
export/seed 데이터셋을 학습 전에 점검:
|
|
4
|
+
· 포맷 유효성 (messages 3-turn, 빈 응답 없음)
|
|
5
|
+
· 태스크별 출력 검증 (schema/verdict = JSON 파싱, sql = 읽기전용 SELECT)
|
|
6
|
+
· 중복 제거 (같은 user→assistant)
|
|
7
|
+
· 태스크 분포(편중) 리포트
|
|
8
|
+
나쁜 샘플은 *사유와 함께* 격리(quarantine)하고, 통과한 것만 clean 셋으로 반환.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
_FORBIDDEN_SQL = re.compile(
|
|
18
|
+
r"\b(insert|update|delete|drop|alter|create|merge|truncate|grant|revoke)\b", re.IGNORECASE)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class CurationReport:
|
|
23
|
+
total: int = 0
|
|
24
|
+
kept: int = 0
|
|
25
|
+
dropped: list[dict] = field(default_factory=list) # [{reason, sample}]
|
|
26
|
+
by_task: dict[str, int] = field(default_factory=dict)
|
|
27
|
+
warnings: list[str] = field(default_factory=list)
|
|
28
|
+
|
|
29
|
+
def summary(self) -> str:
|
|
30
|
+
lines = [f"[DataCurator] 총 {self.total} → 통과 {self.kept} · 격리 {len(self.dropped)}"]
|
|
31
|
+
if self.by_task:
|
|
32
|
+
dist = ", ".join(f"{k} {v}" for k, v in sorted(self.by_task.items()))
|
|
33
|
+
lines.append(f" 태스크 분포(통과): {dist}")
|
|
34
|
+
for w in self.warnings:
|
|
35
|
+
lines.append(f" ⚠ {w}")
|
|
36
|
+
for d in self.dropped[:12]:
|
|
37
|
+
lines.append(f" ✗ 격리({d['reason']}): {d['sample'][:70]}")
|
|
38
|
+
if len(self.dropped) > 12:
|
|
39
|
+
lines.append(f" … 외 {len(self.dropped) - 12}건")
|
|
40
|
+
return "\n".join(lines)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _valid_json_output(text: str) -> bool:
|
|
44
|
+
m = re.search(r"\{[\s\S]*\}", text)
|
|
45
|
+
if not m:
|
|
46
|
+
return False
|
|
47
|
+
try:
|
|
48
|
+
json.loads(m.group(0))
|
|
49
|
+
return True
|
|
50
|
+
except json.JSONDecodeError:
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _valid_sql_output(text: str) -> bool:
|
|
55
|
+
s = text.strip().rstrip(";").strip()
|
|
56
|
+
if not re.match(r"(?is)^\s*(with|select)\b", s):
|
|
57
|
+
return False
|
|
58
|
+
if ";" in s or _FORBIDDEN_SQL.search(s):
|
|
59
|
+
return False
|
|
60
|
+
return bool(re.search(r"(?is)\bfrom\b", s))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class DataCurator:
|
|
64
|
+
"""학습셋 품질 게이트. curate(rows) → (clean_rows, CurationReport)."""
|
|
65
|
+
|
|
66
|
+
def curate(self, rows: list[dict], *, min_per_task: int = 3) -> tuple[list[dict], CurationReport]:
|
|
67
|
+
rep = CurationReport(total=len(rows))
|
|
68
|
+
seen: set = set()
|
|
69
|
+
clean: list[dict] = []
|
|
70
|
+
for row in rows:
|
|
71
|
+
reason = self._reject_reason(row)
|
|
72
|
+
key = self._dedup_key(row)
|
|
73
|
+
if reason is None and key in seen:
|
|
74
|
+
reason = "중복"
|
|
75
|
+
if reason is not None:
|
|
76
|
+
rep.dropped.append({"reason": reason, "sample": self._preview(row)})
|
|
77
|
+
continue
|
|
78
|
+
seen.add(key)
|
|
79
|
+
clean.append(row)
|
|
80
|
+
t = str(row.get("task", "?"))
|
|
81
|
+
rep.by_task[t] = rep.by_task.get(t, 0) + 1
|
|
82
|
+
rep.kept = len(clean)
|
|
83
|
+
# 편중 경고
|
|
84
|
+
for t, n in rep.by_task.items():
|
|
85
|
+
if n < min_per_task:
|
|
86
|
+
rep.warnings.append(f"태스크 '{t}' 예시 {n}개 — {min_per_task}개 미만(과소). 보강 권장")
|
|
87
|
+
if not rep.by_task:
|
|
88
|
+
rep.warnings.append("통과한 예시가 없음 — 포맷 확인 필요")
|
|
89
|
+
return clean, rep
|
|
90
|
+
|
|
91
|
+
# ── 내부 ──
|
|
92
|
+
@staticmethod
|
|
93
|
+
def _msgs(row: dict) -> list[dict]:
|
|
94
|
+
return row.get("messages") or []
|
|
95
|
+
|
|
96
|
+
def _reject_reason(self, row: dict) -> str | None:
|
|
97
|
+
msgs = self._msgs(row)
|
|
98
|
+
if len(msgs) < 2:
|
|
99
|
+
return "messages 부족"
|
|
100
|
+
roles = [m.get("role") for m in msgs]
|
|
101
|
+
if "user" not in roles or "assistant" not in roles:
|
|
102
|
+
return "user/assistant 누락"
|
|
103
|
+
out = next((m.get("content", "") for m in msgs if m.get("role") == "assistant"), "")
|
|
104
|
+
if not str(out).strip():
|
|
105
|
+
return "빈 응답"
|
|
106
|
+
task = row.get("task")
|
|
107
|
+
if task in ("schema", "verdict") and not _valid_json_output(out):
|
|
108
|
+
return "JSON 출력 아님"
|
|
109
|
+
if task == "sql" and not _valid_sql_output(out):
|
|
110
|
+
return "안전한 SELECT 아님"
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def _dedup_key(row: dict):
|
|
115
|
+
msgs = row.get("messages") or []
|
|
116
|
+
u = next((m.get("content") for m in msgs if m.get("role") == "user"), "")
|
|
117
|
+
a = next((m.get("content") for m in msgs if m.get("role") == "assistant"), "")
|
|
118
|
+
return (str(u).strip(), str(a).strip())
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def _preview(row: dict) -> str:
|
|
122
|
+
msgs = row.get("messages") or []
|
|
123
|
+
u = next((m.get("content") for m in msgs if m.get("role") == "user"), "")
|
|
124
|
+
return str(u).replace("\n", " ")[:80]
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""structverify.training.dataset — 학습 데이터셋 준비 (코어, GPU 불필요).
|
|
2
|
+
|
|
3
|
+
두 경로:
|
|
4
|
+
· export_dataset(reports) — 실제 검증 결과(확정 정답) → verdict/schema 예시로 변환
|
|
5
|
+
· build_seed_dataset() — 시작용 seed 예시(아래 SEED) 를 jsonl로 (도메인 확장의 출발점)
|
|
6
|
+
|
|
7
|
+
표준 chat 포맷(messages) jsonl → TRL/unsloth/axolotl 어디서든 학습. 우리 방식에 안 가둠.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from typing import Any, Iterable
|
|
13
|
+
|
|
14
|
+
from .tasks import build_example
|
|
15
|
+
|
|
16
|
+
# ── SEED 예시 ────────────────────────────────────────────────────────────────
|
|
17
|
+
# StructVerify가 코드로 하드닝한 동작들을 그대로 모델에 각인시키는 실제 예시:
|
|
18
|
+
# · 한국어 큰수 파싱("1,500만"→15,000,000) · 순매출=net 공식 · 비율/증감율 SQL
|
|
19
|
+
# · 단위 일관성(원시값+기본단위) · 판정 근거 톤
|
|
20
|
+
_SEED_SPECS: list[dict] = [
|
|
21
|
+
# ---- schema: claim → 구조화 추출 (한국어 숫자·단위 파싱 각인) ----
|
|
22
|
+
dict(task="schema", claim_text="2023년 말 기준 당사의 누적 고객 수는 150만 명에 이르렀습니다.",
|
|
23
|
+
output='{"indicator": "누적 고객 수", "value": 1500000, "unit": "명", "time_period": "2023", "population": null}'),
|
|
24
|
+
dict(task="schema", claim_text="누적 주문 건수는 1,500만 건을 돌파하였습니다.",
|
|
25
|
+
output='{"indicator": "누적 주문 건수", "value": 15000000, "unit": "건", "time_period": null, "population": null}'),
|
|
26
|
+
dict(task="schema", claim_text="당사와 거래하는 공급업체는 총 10만 개사입니다.",
|
|
27
|
+
output='{"indicator": "공급업체 수", "value": 100000, "unit": "개", "time_period": null, "population": null}'),
|
|
28
|
+
dict(task="schema", claim_text="누적 총 순매출은 21,791억 달러를 기록했습니다.",
|
|
29
|
+
output='{"indicator": "총 순매출", "value": 21791, "unit": "억 달러", "time_period": null, "population": null}'),
|
|
30
|
+
dict(task="schema", claim_text="건당 평균 주문 금액은 151,121달러입니다.",
|
|
31
|
+
output='{"indicator": "평균 주문 금액", "value": 151121, "unit": "달러", "time_period": null, "population": null}'),
|
|
32
|
+
dict(task="schema", claim_text="1997년 순매출은 3,304억 달러였습니다.",
|
|
33
|
+
output='{"indicator": "순매출", "value": 3304, "unit": "억 달러", "time_period": "1997", "population": null}'),
|
|
34
|
+
dict(task="schema", claim_text="유럽(EUROPE)이 4,369억 달러로 가장 높은 매출을 기록했습니다.",
|
|
35
|
+
output='{"indicator": "지역별 순매출", "value": 4369, "unit": "억 달러", "time_period": null, "population": "유럽"}'),
|
|
36
|
+
dict(task="schema", claim_text="아메리카 권역 순매출은 전체 순매출의 약 20%를 차지합니다.",
|
|
37
|
+
output='{"indicator": "순매출 비율", "value": 20, "unit": "%", "time_period": null, "population": "아메리카"}'),
|
|
38
|
+
dict(task="schema", claim_text="1998년에는 전년 대비 약 41% 감소하였습니다.",
|
|
39
|
+
output='{"indicator": "순매출 증감율", "value": -41, "unit": "%", "time_period": "1998", "population": null}'),
|
|
40
|
+
dict(task="schema", claim_text="쉬었음 청년이 23만 8천명으로 집계됐다.",
|
|
41
|
+
output='{"indicator": "쉬었음 청년 수", "value": 238000, "unit": "명", "time_period": null, "population": "청년"}'),
|
|
42
|
+
dict(task="schema", claim_text="출생아 수는 2만 171명이었다.",
|
|
43
|
+
output='{"indicator": "출생아 수", "value": 20171, "unit": "명", "time_period": null, "population": null}'),
|
|
44
|
+
# ---- sql: 지표 → 집계/파생 SQL (net 공식·최소테이블·비율·증감율 각인) ----
|
|
45
|
+
dict(task="sql", indicator="총 고객수", claim_text="누적 고객 수는 150만 명입니다.",
|
|
46
|
+
schema_hint="CUSTOMER(c_custkey, c_name, c_mktsegment, c_nationkey)",
|
|
47
|
+
output="SELECT COUNT(DISTINCT c_custkey) FROM CUSTOMER"),
|
|
48
|
+
dict(task="sql", indicator="총 순매출(순=할인반영)", claim_text="누적 총 순매출은 21,791억 달러입니다.",
|
|
49
|
+
schema_hint="LINEITEM(l_orderkey, l_extendedprice, l_discount, l_quantity)",
|
|
50
|
+
output="SELECT SUM(l_extendedprice*(1-l_discount)) FROM LINEITEM"),
|
|
51
|
+
dict(task="sql", indicator="1996년 주문 건수", claim_text="1996년 주문 건수는 약 228만 건입니다.",
|
|
52
|
+
schema_hint="ORDERS(o_orderkey, o_orderdate, o_totalprice, o_custkey)",
|
|
53
|
+
output="SELECT COUNT(*) FROM ORDERS WHERE YEAR(o_orderdate)=1996"),
|
|
54
|
+
dict(task="sql", indicator="MACHINERY 세그먼트 고객 수", claim_text="기계 세그먼트 고객은 30만 명입니다.",
|
|
55
|
+
schema_hint="CUSTOMER(c_custkey, c_mktsegment) ※불필요한 JOIN 금지",
|
|
56
|
+
output="SELECT COUNT(DISTINCT c_custkey) FROM CUSTOMER WHERE c_mktsegment='MACHINERY'"),
|
|
57
|
+
dict(task="sql", indicator="지역별 순매출(유럽)", claim_text="유럽 순매출은 4,369억 달러입니다.",
|
|
58
|
+
schema_hint="LINEITEM·ORDERS·CUSTOMER·NATION·REGION (l_orderkey=o_orderkey, o_custkey=c_custkey, c_nationkey=n_nationkey, n_regionkey=r_regionkey)",
|
|
59
|
+
output=("SELECT SUM(l_extendedprice*(1-l_discount)) FROM LINEITEM "
|
|
60
|
+
"JOIN ORDERS ON l_orderkey=o_orderkey JOIN CUSTOMER ON o_custkey=c_custkey "
|
|
61
|
+
"JOIN NATION ON c_nationkey=n_nationkey JOIN REGION ON n_regionkey=r_regionkey "
|
|
62
|
+
"WHERE r_name='EUROPE'")),
|
|
63
|
+
dict(task="sql", indicator="순매출 비율(아메리카/전체)", claim_text="아메리카는 전체 순매출의 약 20%입니다.",
|
|
64
|
+
schema_hint="LINEITEM·ORDERS·CUSTOMER·NATION·REGION",
|
|
65
|
+
output=("SELECT (SELECT SUM(l_extendedprice*(1-l_discount)) FROM LINEITEM "
|
|
66
|
+
"JOIN ORDERS ON l_orderkey=o_orderkey JOIN CUSTOMER ON o_custkey=c_custkey "
|
|
67
|
+
"JOIN NATION ON c_nationkey=n_nationkey JOIN REGION ON n_regionkey=r_regionkey "
|
|
68
|
+
"WHERE r_name='AMERICA') / (SELECT SUM(l_extendedprice*(1-l_discount)) FROM LINEITEM) * 100")),
|
|
69
|
+
dict(task="sql", indicator="순매출 증감율(1998 vs 1997)", claim_text="1998년은 1997년 대비 약 41% 감소했습니다.",
|
|
70
|
+
schema_hint="LINEITEM·ORDERS (l_orderkey=o_orderkey, o_orderdate) ※FILTER 금지, 서브쿼리 2개",
|
|
71
|
+
output=("SELECT (a.v - b.v)/b.v*100 FROM "
|
|
72
|
+
"(SELECT SUM(l_extendedprice*(1-l_discount)) v FROM LINEITEM JOIN ORDERS ON l_orderkey=o_orderkey WHERE YEAR(o_orderdate)=1998) a, "
|
|
73
|
+
"(SELECT SUM(l_extendedprice*(1-l_discount)) v FROM LINEITEM JOIN ORDERS ON l_orderkey=o_orderkey WHERE YEAR(o_orderdate)=1997) b")),
|
|
74
|
+
# ---- verdict: claim + 근거 → 판정 (허용오차·근거 톤 각인) ----
|
|
75
|
+
dict(task="verdict", claim_text="누적 고객 수는 150만 명입니다.", evidence_value=1500000, evidence_unit="명",
|
|
76
|
+
output='{"verdict": "match", "reason": "주장 150만 명이 공식 수치 1,500,000명과 정확히 일치합니다."}'),
|
|
77
|
+
dict(task="verdict", claim_text="건당 평균 주문 금액은 20만 달러입니다.", evidence_value=145274, evidence_unit="달러",
|
|
78
|
+
output='{"verdict": "mismatch", "reason": "주장 200,000달러는 실제 145,274달러와 약 37.7% 차이로 불일치합니다."}'),
|
|
79
|
+
dict(task="verdict", claim_text="유럽 순매출은 전체의 약 35%를 차지합니다.", evidence_value=20.05, evidence_unit="%",
|
|
80
|
+
output='{"verdict": "mismatch", "reason": "주장 35%는 실제 비율 20.05%와 크게 어긋나 불일치합니다."}'),
|
|
81
|
+
dict(task="verdict", claim_text="1997년 순매출은 3,304억 달러였습니다.", evidence_value=3304, evidence_unit="억 달러",
|
|
82
|
+
output='{"verdict": "match", "reason": "주장 3,304억 달러가 공식 수치와 0.0% 오차로 일치합니다."}'),
|
|
83
|
+
dict(task="verdict", claim_text="1998년 순매출은 3,300억 달러 수준을 유지했습니다.", evidence_value=1937, evidence_unit="억 달러",
|
|
84
|
+
output='{"verdict": "mismatch", "reason": "주장 3,300억 달러는 실제 1,937억 달러와 70% 차이로 불일치합니다."}'),
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def build_seed_dataset(out: str = "seed_dataset.jsonl") -> int:
|
|
89
|
+
"""SEED 예시를 chat 포맷 jsonl로 저장. 반환: 예시 수."""
|
|
90
|
+
rows = [build_example(**spec) for spec in _SEED_SPECS]
|
|
91
|
+
return write_jsonl(rows, out)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def export_dataset(reports: Iterable[Any], out: str = "train.jsonl",
|
|
95
|
+
include: tuple[str, ...] = ("verdict",)) -> int:
|
|
96
|
+
"""실제 검증 결과(Report들) → 학습 예시. 확정된(=사람이 승인한) 결과만 넣는 것을 권장.
|
|
97
|
+
|
|
98
|
+
각 Result(claim/verdict/value/unit/reason)를 verdict 태스크 예시로 변환.
|
|
99
|
+
(schema/sql 예시는 파이프라인 중간 산출을 별도 수집해야 하므로 seed/수동 확장 권장.)
|
|
100
|
+
"""
|
|
101
|
+
rows: list[dict] = []
|
|
102
|
+
for report in reports:
|
|
103
|
+
for r in report:
|
|
104
|
+
v = str(getattr(r, "verdict", "") or "")
|
|
105
|
+
if v not in ("match", "mismatch", "unverifiable"):
|
|
106
|
+
continue
|
|
107
|
+
if "verdict" in include:
|
|
108
|
+
reason = str(getattr(r, "reason", "") or "")[:200]
|
|
109
|
+
rows.append(build_example(
|
|
110
|
+
"verdict",
|
|
111
|
+
claim_text=str(getattr(r, "claim", "") or "").replace("\n", " ").strip(),
|
|
112
|
+
evidence_value=getattr(r, "value", None),
|
|
113
|
+
evidence_unit=getattr(r, "unit", "") or "",
|
|
114
|
+
output=json.dumps({"verdict": v, "reason": reason}, ensure_ascii=False),
|
|
115
|
+
))
|
|
116
|
+
return write_jsonl(rows, out)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ── jsonl I/O ────────────────────────────────────────────────────────────────
|
|
120
|
+
def write_jsonl(rows: list[dict], path: str) -> int:
|
|
121
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
122
|
+
for row in rows:
|
|
123
|
+
f.write(json.dumps(row, ensure_ascii=False) + "\n")
|
|
124
|
+
return len(rows)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def read_jsonl(path: str) -> list[dict]:
|
|
128
|
+
rows: list[dict] = []
|
|
129
|
+
with open(path, encoding="utf-8") as f:
|
|
130
|
+
for line in f:
|
|
131
|
+
line = line.strip()
|
|
132
|
+
if line:
|
|
133
|
+
rows.append(json.loads(line))
|
|
134
|
+
return rows
|