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.
Files changed (168) hide show
  1. structverify/__init__.py +83 -0
  2. structverify/adaptation/__init__.py +0 -0
  3. structverify/adaptation/adapter_trainer.py +341 -0
  4. structverify/adaptation/feedback_store.py +31 -0
  5. structverify/adaptation/kosis_crawler.py +317 -0
  6. structverify/adaptation/sample_builder.py +149 -0
  7. structverify/adaptation/synthetic_generator.py +320 -0
  8. structverify/adaptation/update_embeddings.py +178 -0
  9. structverify/agent/__init__.py +21 -0
  10. structverify/agent/builder_agent.py +226 -0
  11. structverify/agent/conformance_agent.py +171 -0
  12. structverify/agent/dependency_planner.py +151 -0
  13. structverify/agent/indexing_agent.py +153 -0
  14. structverify/agent/indexing_planner.py +169 -0
  15. structverify/agent/integration_example.py +182 -0
  16. structverify/agent/loop.py +1165 -0
  17. structverify/agent/memory.py +207 -0
  18. structverify/agent/planner.py +817 -0
  19. structverify/agent/prompts/__init__.py +15 -0
  20. structverify/agent/prompts/planner_prompts.py +219 -0
  21. structverify/agent/prompts/reflect_prompts.py +387 -0
  22. structverify/agent/reflect.py +227 -0
  23. structverify/agent/runtime_agent.py +1272 -0
  24. structverify/agent/schemas.py +262 -0
  25. structverify/agent/source_profiler.py +229 -0
  26. structverify/agent/tools/__init__.py +64 -0
  27. structverify/agent/tools/base.py +222 -0
  28. structverify/agent/tools/calculate.py +244 -0
  29. structverify/agent/tools/catalog_search.py +859 -0
  30. structverify/agent/tools/deep_explore.py +293 -0
  31. structverify/agent/tools/explore_catalog.py +423 -0
  32. structverify/agent/tools/fetch_evidence.py +922 -0
  33. structverify/agent/tools/finish.py +423 -0
  34. structverify/agent/tools/meta_explore.py +267 -0
  35. structverify/agent/tools/query_rewriter.py +134 -0
  36. structverify/agent/tools/read_original.py +144 -0
  37. structverify/agent/tools/replan.py +365 -0
  38. structverify/agent/workspace.py +958 -0
  39. structverify/api.py +804 -0
  40. structverify/config/default.yaml +350 -0
  41. structverify/core/__init__.py +0 -0
  42. structverify/core/config_loader.py +30 -0
  43. structverify/core/pipeline.py +280 -0
  44. structverify/core/schemas.py +362 -0
  45. structverify/detection/__init__.py +26 -0
  46. structverify/detection/_config.py +163 -0
  47. structverify/detection/_llm.py +24 -0
  48. structverify/detection/candidate/__init__.py +1 -0
  49. structverify/detection/candidate/heuristic.py +60 -0
  50. structverify/detection/candidate/llm.py +51 -0
  51. structverify/detection/candidate_scorer.py +81 -0
  52. structverify/detection/claim_detector.py +164 -0
  53. structverify/detection/claims/__init__.py +1 -0
  54. structverify/detection/claims/worthiness.py +142 -0
  55. structverify/detection/domain/__init__.py +1 -0
  56. structverify/detection/domain/classify.py +84 -0
  57. structverify/detection/domain/preview.py +36 -0
  58. structverify/detection/domain/registry.py +99 -0
  59. structverify/detection/domain_classifier.py +75 -0
  60. structverify/detection/prompts/__init__.py +1 -0
  61. structverify/detection/prompts/candidate.py +38 -0
  62. structverify/detection/prompts/claim_worthiness.py +48 -0
  63. structverify/detection/prompts/domain.py +41 -0
  64. structverify/detection/prompts/schema.py +508 -0
  65. structverify/detection/prompts_loader.py +167 -0
  66. structverify/detection/schema/__init__.py +1 -0
  67. structverify/detection/schema/expand.py +83 -0
  68. structverify/detection/schema/induce.py +441 -0
  69. structverify/detection/schema/regenerate.py +162 -0
  70. structverify/detection/schema/temporal_hints.py +130 -0
  71. structverify/detection/schema/validate.py +193 -0
  72. structverify/detection/schema_inductor.py +112 -0
  73. structverify/detection/synthetic_generator.py +270 -0
  74. structverify/explanation/__init__.py +0 -0
  75. structverify/explanation/_config.py +18 -0
  76. structverify/explanation/_llm.py +25 -0
  77. structverify/explanation/explainer.py +183 -0
  78. structverify/explanation/fallback.py +29 -0
  79. structverify/explanation/formatters.py +75 -0
  80. structverify/explanation/prompts/__init__.py +1 -0
  81. structverify/explanation/prompts/match.py +27 -0
  82. structverify/explanation/prompts/mismatch.py +20 -0
  83. structverify/explanation/prompts/multihop.py +16 -0
  84. structverify/explanation/prompts/unverifiable.py +17 -0
  85. structverify/graph/__init__.py +0 -0
  86. structverify/graph/claim_graph.py +226 -0
  87. structverify/graph/document_graph.py +487 -0
  88. structverify/graph/graph_builder.py +238 -0
  89. structverify/graph/graph_multihop.py +335 -0
  90. structverify/graph/graph_store.py +281 -0
  91. structverify/graph/provenance.py +52 -0
  92. structverify/memory/__init__.py +44 -0
  93. structverify/memory/agent_memory.py +142 -0
  94. structverify/memory/embedder.py +69 -0
  95. structverify/memory/exemplar_store.py +241 -0
  96. structverify/memory/normalizer.py +91 -0
  97. structverify/memory/schema.py +119 -0
  98. structverify/memory/storage/__init__.py +29 -0
  99. structverify/memory/storage/jsonl_store.py +117 -0
  100. structverify/memory/working_memory.py +370 -0
  101. structverify/preprocessing/Dockerfile.scraper +27 -0
  102. structverify/preprocessing/__init__.py +0 -0
  103. structverify/preprocessing/extractor.py +574 -0
  104. structverify/preprocessing/pdf/__init__.py +16 -0
  105. structverify/preprocessing/pdf/fields.py +95 -0
  106. structverify/preprocessing/pdf/markdown.py +107 -0
  107. structverify/preprocessing/pdf/models.py +34 -0
  108. structverify/preprocessing/pdf/ocr.py +172 -0
  109. structverify/preprocessing/pdf/pipeline.py +74 -0
  110. structverify/preprocessing/pdf/reader.py +119 -0
  111. structverify/preprocessing/pdf/scoring.py +61 -0
  112. structverify/preprocessing/scraper_sandbox.py +561 -0
  113. structverify/preprocessing/segmenter.py +48 -0
  114. structverify/preprocessing/sir_builder.py +240 -0
  115. structverify/progress.py +591 -0
  116. structverify/retrieval/__init__.py +0 -0
  117. structverify/retrieval/base.py +208 -0
  118. structverify/retrieval/base_connector.py +85 -0
  119. structverify/retrieval/catalog_ranker.py +300 -0
  120. structverify/retrieval/catalog_search.py +583 -0
  121. structverify/retrieval/chunking.py +92 -0
  122. structverify/retrieval/custom_csv_source.py +386 -0
  123. structverify/retrieval/custom_db_source.py +396 -0
  124. structverify/retrieval/custom_docs_source.py +152 -0
  125. structverify/retrieval/dimension_resolver.py +281 -0
  126. structverify/retrieval/evidence_subgraph.py +63 -0
  127. structverify/retrieval/kosis_connector.py +1192 -0
  128. structverify/retrieval/kosis_relevance.py +142 -0
  129. structverify/retrieval/kosis_source.py +1541 -0
  130. structverify/retrieval/query_builder.py +72 -0
  131. structverify/retrieval/registry.py +133 -0
  132. structverify/retrieval/relevance_judge.py +141 -0
  133. structverify/retrieval/row_matcher.py +267 -0
  134. structverify/storage/__init__.py +0 -0
  135. structverify/storage/db_manager.py +157 -0
  136. structverify/storage/dwh_manager.py +92 -0
  137. structverify/storage/init_db.py +99 -0
  138. structverify/storage/raw_storage.py +29 -0
  139. structverify/training/__init__.py +26 -0
  140. structverify/training/curator.py +124 -0
  141. structverify/training/dataset.py +134 -0
  142. structverify/training/doctor.py +99 -0
  143. structverify/training/evalgate.py +96 -0
  144. structverify/training/generate.py +101 -0
  145. structverify/training/loop.py +116 -0
  146. structverify/training/recipe/train_mlx.py +99 -0
  147. structverify/training/recipe/train_qlora.py +104 -0
  148. structverify/training/tasks.py +79 -0
  149. structverify/utils/__init__.py +0 -0
  150. structverify/utils/embedding_client.py +248 -0
  151. structverify/utils/llm_client.py +809 -0
  152. structverify/utils/logger.py +81 -0
  153. structverify/verification/__init__.py +0 -0
  154. structverify/verification/_config.py +45 -0
  155. structverify/verification/adapters.py +405 -0
  156. structverify/verification/conformance.py +117 -0
  157. structverify/verification/decide_verdict.py +216 -0
  158. structverify/verification/decide_verdict_agent.py +454 -0
  159. structverify/verification/growth_diff.py +267 -0
  160. structverify/verification/row_match.py +345 -0
  161. structverify/verification/units.py +64 -0
  162. structverify/verification/verdict_thresholds.py +232 -0
  163. structverify/verification/verifier.py +84 -0
  164. structverify-0.3.0.dist-info/METADATA +903 -0
  165. structverify-0.3.0.dist-info/RECORD +168 -0
  166. structverify-0.3.0.dist-info/WHEEL +5 -0
  167. structverify-0.3.0.dist-info/licenses/LICENSE +21 -0
  168. structverify-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,99 @@
1
+ """structverify.training.doctor — TrainDoctor (학습 로그 이상 감지, 코어·GPU 불필요).
2
+
3
+ 표준 HuggingFace `trainer_state.json`(log_history: step/loss/learning_rate/eval_loss)을 읽어
4
+ 발산·NaN·spike·정체·overfit을 휴리스틱으로 감지하고, 자연어 진단 + 처방을 낸다.
5
+ 표준 포맷만 보므로 TRL/unsloth/axolotl 어떤 트레이너로 학습하든 동작(백엔드 무관).
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import math
11
+ from dataclasses import dataclass, field
12
+ from typing import Any
13
+
14
+
15
+ @dataclass
16
+ class Diagnosis:
17
+ healthy: bool = True
18
+ issues: list[str] = field(default_factory=list) # [severity] 설명
19
+ prescriptions: list[str] = field(default_factory=list)
20
+ stats: dict[str, Any] = field(default_factory=dict)
21
+
22
+ def summary(self) -> str:
23
+ head = "🩺 TrainDoctor — 정상" if self.healthy else "🩺 TrainDoctor — 이상 감지"
24
+ lines = [head]
25
+ s = self.stats
26
+ if s:
27
+ lines.append(f" steps={s.get('steps')} · loss {s.get('first_loss')}→{s.get('last_loss')} "
28
+ f"· min={s.get('min_loss')}")
29
+ for i in self.issues:
30
+ lines.append(f" ⚠ {i}")
31
+ for p in self.prescriptions:
32
+ lines.append(f" → {p}")
33
+ return "\n".join(lines)
34
+
35
+
36
+ def _load_history(path_or_state: Any) -> list[dict]:
37
+ if isinstance(path_or_state, dict):
38
+ return path_or_state.get("log_history", [])
39
+ with open(path_or_state, encoding="utf-8") as f:
40
+ return json.load(f).get("log_history", [])
41
+
42
+
43
+ class TrainDoctor:
44
+ def diagnose(self, trainer_state: Any) -> Diagnosis:
45
+ hist = _load_history(trainer_state)
46
+ losses = [(h.get("step"), h["loss"]) for h in hist if "loss" in h]
47
+ evals = [(h.get("step"), h["eval_loss"]) for h in hist if "eval_loss" in h]
48
+ d = Diagnosis()
49
+ if len(losses) < 2:
50
+ d.healthy = False
51
+ d.issues.append("[정보부족] loss 기록이 2개 미만 — 학습이 거의 안 돎")
52
+ d.prescriptions.append("스텝 수·로깅 간격(logging_steps) 확인")
53
+ return d
54
+
55
+ vals = [v for _, v in losses]
56
+ d.stats = {
57
+ "steps": losses[-1][0], "first_loss": round(vals[0], 4),
58
+ "last_loss": round(vals[-1], 4), "min_loss": round(min(vals), 4),
59
+ }
60
+
61
+ # NaN/inf
62
+ if any(math.isnan(v) or math.isinf(v) for v in vals):
63
+ d.healthy = False
64
+ d.issues.append("[치명] loss에 NaN/inf — 발산")
65
+ d.prescriptions.append("learning_rate를 1/5로 낮추고 warmup·grad clipping(max_grad_norm=1.0) 적용")
66
+ return d
67
+
68
+ # 발산: 마지막이 최소보다 크게 상승
69
+ min_v = min(vals)
70
+ if vals[-1] > min_v * 1.6 and vals[-1] > vals[0]:
71
+ d.healthy = False
72
+ d.issues.append(f"[심각] loss 발산 — 최소 {min_v:.3f} 후 {vals[-1]:.3f}로 상승")
73
+ d.prescriptions.append("learning_rate 낮추기(÷2~5), 최소 지점 체크포인트 사용")
74
+
75
+ # spike: 국소 급등(직전 대비 2배↑)
76
+ spikes = [losses[i][0] for i in range(1, len(vals))
77
+ if vals[i] > vals[i - 1] * 2.0 and vals[i - 1] > 1e-6]
78
+ if spikes:
79
+ d.healthy = False
80
+ d.issues.append(f"[주의] loss spike @ step {spikes[:3]} — 특정 배치 의심")
81
+ d.prescriptions.append("해당 구간 데이터 재검토(DataCurator), batch shuffle·grad clipping 확인")
82
+
83
+ # 정체: 후반 개선 미미
84
+ tail = vals[max(0, len(vals) - 5):]
85
+ if len(tail) >= 3 and (max(tail) - min(tail)) < 0.01 and min_v > 0.5:
86
+ d.issues.append("[정보] 후반 loss 정체 — 이미 수렴했거나 lr 과소")
87
+ d.prescriptions.append("수렴이면 조기 종료, 아니면 lr↑ 또는 데이터 증강")
88
+
89
+ # overfit: eval_loss가 상승 전환
90
+ if len(evals) >= 2:
91
+ ev = [v for _, v in evals]
92
+ if ev[-1] > min(ev) * 1.15:
93
+ d.healthy = False
94
+ d.issues.append(f"[주의] overfit — eval_loss 최소 {min(ev):.3f} 후 {ev[-1]:.3f}로 상승")
95
+ d.prescriptions.append("early stopping, LoRA rank↓ 또는 dropout↑, 데이터 늘리기")
96
+
97
+ if d.healthy and not d.issues:
98
+ d.issues.append("[정상] 발산·spike·overfit 없음")
99
+ return d
@@ -0,0 +1,96 @@
1
+ """structverify.training.evalgate — EvalGate (학습된 모델을 *검증 엔진으로* 자가평가).
2
+
3
+ StructVerify만 할 수 있는 셀프-루프: 라벨된 eval 셋(확정 정답)으로 base vs 파인튜닝 모델의
4
+ 검증 정확도를 재고, 좋아졌을 때만 어댑터 채택(회귀 방지). GPU 불필요(엔진이 알아서 추론).
5
+
6
+ eval 셋 포맷(라벨): [{"claim": "...", "expected": "match|mismatch|unverifiable"}, ...]
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Any
12
+
13
+
14
+ @dataclass
15
+ class EvalResult:
16
+ n: int = 0
17
+ correct: int = 0
18
+ by_verdict: dict[str, dict] = field(default_factory=dict) # expected → {n, correct}
19
+
20
+ @property
21
+ def accuracy(self) -> float:
22
+ return self.correct / self.n if self.n else 0.0
23
+
24
+ def summary(self) -> str:
25
+ parts = [f"정확도 {self.accuracy:.1%} ({self.correct}/{self.n})"]
26
+ for k, v in sorted(self.by_verdict.items()):
27
+ parts.append(f"{k} {v['correct']}/{v['n']}")
28
+ return " · ".join(parts)
29
+
30
+
31
+ @dataclass
32
+ class GateDecision:
33
+ before: EvalResult
34
+ after: EvalResult
35
+ margin: float
36
+ accepted: bool
37
+ reason: str
38
+
39
+ def summary(self) -> str:
40
+ mark = "✅ 채택" if self.accepted else "⛔ 거부"
41
+ return (f"🚦 EvalGate — {mark}\n"
42
+ f" before: {self.before.summary()}\n"
43
+ f" after : {self.after.summary()}\n"
44
+ f" Δ정확도 {self.after.accuracy - self.before.accuracy:+.1%} (margin {self.margin:+.1%}) — {self.reason}")
45
+
46
+
47
+ def _claim_verdict(report: Any) -> str:
48
+ """Report → 대표 판정. mismatch > match > unverifiable 우선(거짓을 놓치지 않게)."""
49
+ verds = [str(getattr(r, "verdict", "") or "") for r in report]
50
+ for pref in ("mismatch", "match", "unverifiable"):
51
+ if pref in verds:
52
+ return pref
53
+ return verds[0] if verds else "unverifiable"
54
+
55
+
56
+ class EvalGate:
57
+ """검증 엔진(Verifier) 재사용해 라벨 셋에 대한 정확도 측정 + 채택 판정."""
58
+
59
+ def __init__(self, engine: Any):
60
+ self.engine = engine
61
+
62
+ def score(self, labeled: list[dict], engine: Any | None = None) -> EvalResult:
63
+ eng = engine or self.engine
64
+ res = EvalResult()
65
+ for row in labeled:
66
+ claim = str(row.get("claim", "")).strip()
67
+ exp = str(row.get("expected", "")).strip()
68
+ if not claim or not exp:
69
+ continue
70
+ try:
71
+ report = eng.verify(claim)
72
+ got = _claim_verdict(report)
73
+ except Exception: # noqa: BLE001 — 개별 실패는 오답 처리
74
+ got = "unverifiable"
75
+ b = res.by_verdict.setdefault(exp, {"n": 0, "correct": 0})
76
+ res.n += 1
77
+ b["n"] += 1
78
+ if got == exp:
79
+ res.correct += 1
80
+ b["correct"] += 1
81
+ return res
82
+
83
+ def evaluate(self, labeled: list[dict], *, tuned_engine: Any,
84
+ margin: float = 0.0) -> GateDecision:
85
+ """base(self.engine) vs tuned_engine 정확도 비교 → 채택/거부.
86
+
87
+ tuned_engine = 어댑터 로드된 Verifier (provider=local + adapter 서빙).
88
+ margin: after가 before보다 이만큼은 나아야 채택(기본 0 = 안 나빠지면 채택).
89
+ """
90
+ before = self.score(labeled, self.engine)
91
+ after = self.score(labeled, tuned_engine)
92
+ gain = after.accuracy - before.accuracy
93
+ accepted = gain >= margin
94
+ reason = ("개선됨" if gain > 0 else "동일" if gain == 0 else "악화") + \
95
+ (" → 채택" if accepted else " → 거부(회귀 방지)")
96
+ return GateDecision(before, after, margin, accepted, reason)
@@ -0,0 +1,101 @@
1
+ """structverify.training.generate — 합성 학습 데이터 생성 (증류/부트스트랩).
2
+
3
+ 초기엔 학습 데이터가 없다. 강한 클라우드 모델로 (주장→추출), (주장+근거→판정) 예시를
4
+ 대량 생성해 작은 로컬 모델에 증류(distill)한다. 회사 지표 목록을 주면 그 도메인에 맞춰 생성.
5
+
6
+ from structverify.training import generate_dataset
7
+ n = generate_dataset(llm_config, out="gen.jsonl",
8
+ indicators=["매출","고객수","평균주문금액", ...], domain="유통", n=80)
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import re
14
+ from typing import Any
15
+
16
+ from .tasks import build_example
17
+
18
+
19
+ def _extract_json_array(text: str) -> list[dict]:
20
+ m = re.search(r"\[[\s\S]*\]", text)
21
+ if not m:
22
+ return []
23
+ try:
24
+ data = json.loads(m.group(0))
25
+ return data if isinstance(data, list) else []
26
+ except json.JSONDecodeError:
27
+ return []
28
+
29
+
30
+ _SCHEMA_GEN = """너는 학습 데이터 생성기다. {domain} 회사의 수치 주장 문장 {k}개를 만들고,
31
+ 각 문장에서 검증 대상 수치를 추출한 결과를 함께 제시하라.
32
+
33
+ 다양하게: 지표({inds} 등), 단위(명·건·개·억 달러·%·원), 시점(연도 유/무),
34
+ 그리고 **한국어 숫자 표기를 반드시 섞어라**(예: "1,500만"=15000000, "23만 8천"=238000, "2조 1,791억").
35
+
36
+ JSON 배열로만:
37
+ [{{"claim": "문장", "extraction": {{"indicator":"지표", "value": 숫자, "unit":"단위",
38
+ "time_period":"시점 또는 null", "population":"대상/지역 또는 null"}}}}, ...]"""
39
+
40
+ _VERDICT_GEN = """너는 학습 데이터 생성기다. {domain} 회사 맥락의 (주장, 근거 데이터, 판정) 쌍 {k}개.
41
+ 근거값과 주장이 *일치(match)* 하는 것과 *어긋나는(mismatch)* 것을 대략 반반 섞어라.
42
+ 근거는 원시값(달러/명/건) 또는 %.
43
+
44
+ JSON 배열로만:
45
+ [{{"claim":"주장 문장", "evidence_value": 숫자, "evidence_unit":"단위",
46
+ "verdict":"match 또는 mismatch", "reason":"한 줄 근거"}}, ...]"""
47
+
48
+
49
+ async def _agen(llm, prompt: str) -> list[dict]:
50
+ try:
51
+ raw = await llm.generate(prompt=prompt, system_prompt="JSON 배열만 출력.", model_tier="light")
52
+ except Exception: # noqa: BLE001
53
+ return []
54
+ return _extract_json_array(raw)
55
+
56
+
57
+ async def agenerate_dataset(llm_config: dict, out: str = "gen.jsonl", *,
58
+ indicators: list[str] | None = None, domain: str = "회사",
59
+ n: int = 80, batch: int = 8) -> int:
60
+ """비동기 생성. schema:verdict ≈ 6:4 비율로 n개 근처까지."""
61
+ from structverify.utils.llm_client import LLMClient
62
+ from .dataset import write_jsonl
63
+ llm = LLMClient(config=llm_config)
64
+ inds = ", ".join((indicators or ["매출", "고객수", "주문건수", "평균주문금액", "순매출", "공급업체수"])[:12])
65
+ rows: list[dict] = []
66
+ n_schema, n_verdict = int(n * 0.6), int(n * 0.4)
67
+
68
+ made = 0
69
+ while made < n_schema:
70
+ arr = await _agen(llm, _SCHEMA_GEN.format(domain=domain, k=min(batch, n_schema - made), inds=inds))
71
+ for e in arr:
72
+ ex = e.get("extraction") or {}
73
+ if e.get("claim") and ex.get("indicator") is not None and ex.get("value") is not None:
74
+ rows.append(build_example("schema", claim_text=str(e["claim"]),
75
+ output=json.dumps(ex, ensure_ascii=False)))
76
+ made += 1
77
+ if not arr:
78
+ break
79
+
80
+ made = 0
81
+ while made < n_verdict:
82
+ arr = await _agen(llm, _VERDICT_GEN.format(domain=domain, k=min(batch, n_verdict - made)))
83
+ for e in arr:
84
+ if e.get("claim") and e.get("verdict") in ("match", "mismatch"):
85
+ rows.append(build_example("verdict", claim_text=str(e["claim"]),
86
+ evidence_value=e.get("evidence_value"),
87
+ evidence_unit=e.get("evidence_unit", ""),
88
+ output=json.dumps({"verdict": e["verdict"],
89
+ "reason": str(e.get("reason", ""))[:200]},
90
+ ensure_ascii=False)))
91
+ made += 1
92
+ if not arr:
93
+ break
94
+
95
+ return write_jsonl(rows, out)
96
+
97
+
98
+ def generate_dataset(llm_config: dict, out: str = "gen.jsonl", **kw: Any) -> int:
99
+ """동기 래퍼. 반환: 생성된 예시 수."""
100
+ import asyncio
101
+ return asyncio.run(agenerate_dataset(llm_config, out, **kw))
@@ -0,0 +1,116 @@
1
+ """structverify.training.loop — LearningLoop (감독 학습 오케스트레이션).
2
+
3
+ OSS 원칙대로: *차별화되는 부분(데이터·감독)은 소유*, *학습 자체는 위임*.
4
+ · export/curate/diagnose/evaluate → 코어(여기), GPU 불필요
5
+ · train → 표준 데이터셋을 넘겨 생태계(TRL/unsloth/axolotl/회사 파이프라인)에 위임
6
+
7
+ loop = LearningLoop(engine)
8
+ loop.add_seed(); loop.add_reports(reports) # 데이터 모으기
9
+ ds = loop.prepare("train.jsonl") # ① export + 🕵 DataCurator
10
+ loop.train(ds, "./out", run=False) # ② 레시피/명령 핸드오프 (또는 run=True)
11
+ loop.diagnose("./out/trainer_state.json") # 🩺 TrainDoctor
12
+ gate = loop.evaluate(eval_set, tuned_engine) # 🚦 EvalGate
13
+ if gate.accepted: engine.use_adapter("./out") # 채택
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ import shutil
19
+ from typing import Any
20
+
21
+ from .curator import DataCurator, CurationReport
22
+ from .dataset import build_example, export_dataset, read_jsonl, write_jsonl, _SEED_SPECS
23
+ from .doctor import TrainDoctor, Diagnosis
24
+ from .evalgate import EvalGate, GateDecision
25
+
26
+
27
+ class LearningLoop:
28
+ def __init__(self, engine: Any = None, base_model: str = "unsloth/Qwen2.5-7B-Instruct"):
29
+ self.engine = engine
30
+ self.base_model = base_model
31
+ self._rows: list[dict] = []
32
+ self.curator = DataCurator()
33
+ self.doctor = TrainDoctor()
34
+
35
+ # ── 데이터 모으기 ──
36
+ def add_seed(self) -> "LearningLoop":
37
+ self._rows.extend(build_example(**spec) for spec in _SEED_SPECS)
38
+ return self
39
+
40
+ def add_reports(self, reports, include=("verdict",)) -> "LearningLoop":
41
+ tmp = "_ll_tmp.jsonl"
42
+ export_dataset(reports, tmp, include=include)
43
+ self._rows.extend(read_jsonl(tmp))
44
+ os.remove(tmp)
45
+ return self
46
+
47
+ def add_jsonl(self, path: str) -> "LearningLoop":
48
+ self._rows.extend(read_jsonl(path))
49
+ return self
50
+
51
+ # ── ① 준비: export + DataCurator ──
52
+ def prepare(self, out: str = "train.jsonl", *, verbose: bool = True) -> tuple[str, CurationReport]:
53
+ clean, rep = self.curator.curate(self._rows)
54
+ write_jsonl(clean, out)
55
+ if verbose:
56
+ print(rep.summary())
57
+ print(f"[LearningLoop] clean 데이터셋 저장 → {out} ({rep.kept}건)")
58
+ return out, rep
59
+
60
+ # ── ② 학습: 위임 (레시피 핸드오프 or subprocess 실행) ──
61
+ def train(self, dataset: str, output: str = "./adapter", *, backend: str = "auto",
62
+ run: bool = False, steps: int = 60) -> dict:
63
+ """학습을 위임. run=False(기본)면 실행 명령만 반환(핸드오프).
64
+
65
+ backend: "qlora"(NVIDIA/3060·T4) | "mlx"(Apple Silicon) | "auto"(플랫폼 감지).
66
+ run=True는 해당 extra + 하드웨어가 있을 때만 — 별도 프로세스(샌드박스)로 실행.
67
+ """
68
+ import platform
69
+ if backend == "auto":
70
+ backend = "mlx" if platform.machine() == "arm64" and platform.system() == "Darwin" else "qlora"
71
+ _dir = os.path.join(os.path.dirname(__file__), "recipe")
72
+ if backend == "mlx":
73
+ recipe = os.path.join(_dir, "train_mlx.py")
74
+ model = self.base_model if "mlx" in self.base_model else "mlx-community/Qwen2.5-7B-Instruct-4bit"
75
+ cmd = ["python", "-m", "structverify.training.recipe.train_mlx",
76
+ "--model", model, "--data", os.path.abspath(dataset),
77
+ "--out", os.path.abspath(output), "--iters", str(steps)]
78
+ need = 'pip install "structverify[training-mac]" (Apple Silicon)'
79
+ _probe = "mlx_lm"
80
+ else:
81
+ recipe = os.path.join(_dir, "train_qlora.py")
82
+ cmd = ["python", "-m", "structverify.training.recipe.train_qlora",
83
+ "--model", self.base_model, "--data", os.path.abspath(dataset),
84
+ "--out", os.path.abspath(output), "--max-steps", str(steps)]
85
+ need = 'pip install "structverify[training]" (NVIDIA GPU)'
86
+ _probe = "torch"
87
+ info = {"backend": backend, "recipe": recipe, "command": " ".join(cmd), "output": output}
88
+ if not run:
89
+ print(f"[LearningLoop] 핸드오프({backend}) — 학습 머신에서 아래 실행:")
90
+ print(" " + info["command"])
91
+ return info
92
+ try:
93
+ __import__(_probe)
94
+ except Exception: # noqa: BLE001
95
+ raise RuntimeError(f"run=True에는 학습 백엔드가 필요합니다 → {need}. "
96
+ "또는 run=False로 명령만 받아 별도 실행하세요.")
97
+ import subprocess
98
+ import sys
99
+ print(f"[LearningLoop] 샌드박스 학습 시작 (backend={backend}) …")
100
+ subprocess.run([sys.executable] + cmd[1:], check=True)
101
+ return info
102
+
103
+ # ── 🩺 진단 ──
104
+ def diagnose(self, trainer_state: str, *, verbose: bool = True) -> Diagnosis:
105
+ d = self.doctor.diagnose(trainer_state)
106
+ if verbose:
107
+ print(d.summary())
108
+ return d
109
+
110
+ # ── 🚦 자가평가 ──
111
+ def evaluate(self, eval_set: list[dict], *, tuned_engine: Any,
112
+ margin: float = 0.0, verbose: bool = True) -> GateDecision:
113
+ gate = EvalGate(self.engine).evaluate(eval_set, tuned_engine=tuned_engine, margin=margin)
114
+ if verbose:
115
+ print(gate.summary())
116
+ return gate
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env python
2
+ """StructVerify LoRA 학습 레시피 — Apple Silicon (M1~M4, MLX 백엔드).
3
+
4
+ Mac에는 CUDA가 없어 unsloth/bitsandbytes(QLoRA)가 안 돈다. 대신 Apple 네이티브 **MLX**로
5
+ LoRA 파인튜닝. 36GB 통합메모리면 7B도 여유(4bit MLX 모델이면 더).
6
+
7
+ 준비 (Mac):
8
+ pip install "structverify[training-mac]" # mlx-lm
9
+ # 또는: pip install mlx-lm
10
+
11
+ 실행:
12
+ python -m structverify.training.recipe.train_mlx \
13
+ --model mlx-community/Qwen2.5-7B-Instruct-4bit \
14
+ --data train.jsonl --out ./adapter --iters 100
15
+
16
+ 산출: {out}/ 에 LoRA 어댑터 + trainer_state.json (mlx 로그를 표준 포맷으로 변환 → TrainDoctor 호환)
17
+ """
18
+ import argparse
19
+ import json
20
+ import os
21
+ import re
22
+ import shutil
23
+ import subprocess
24
+ import sys
25
+ import tempfile
26
+
27
+
28
+ def to_mlx_data(chat_jsonl: str, data_dir: str, valid_frac: float = 0.1):
29
+ """우리 chat jsonl → MLX가 먹는 data 폴더(train.jsonl/valid.jsonl, messages 포맷)."""
30
+ os.makedirs(data_dir, exist_ok=True)
31
+ rows = []
32
+ with open(chat_jsonl, encoding="utf-8") as f:
33
+ for line in f:
34
+ line = line.strip()
35
+ if line:
36
+ rows.append({"messages": json.loads(line)["messages"]})
37
+ n_valid = max(1, int(len(rows) * valid_frac))
38
+ valid, train = rows[:n_valid], rows[n_valid:] or rows
39
+ for name, part in (("train", train), ("valid", valid)):
40
+ with open(os.path.join(data_dir, f"{name}.jsonl"), "w", encoding="utf-8") as f:
41
+ for r in part:
42
+ f.write(json.dumps(r, ensure_ascii=False) + "\n")
43
+ return len(train), len(valid)
44
+
45
+
46
+ def parse_mlx_log(text: str) -> list[dict]:
47
+ """mlx_lm.lora stdout("Iter N: Train loss X, ... Val loss Y")→ HF log_history 포맷."""
48
+ hist = []
49
+ for m in re.finditer(r"Iter\s+(\d+):.*?Train loss\s+([\d.]+)", text):
50
+ hist.append({"step": int(m.group(1)), "loss": float(m.group(2))})
51
+ for m in re.finditer(r"Iter\s+(\d+):\s*Val loss\s+([\d.]+)", text):
52
+ hist.append({"step": int(m.group(1)), "eval_loss": float(m.group(2))})
53
+ return sorted(hist, key=lambda h: h.get("step", 0))
54
+
55
+
56
+ def main():
57
+ ap = argparse.ArgumentParser()
58
+ ap.add_argument("--model", default="mlx-community/Qwen2.5-7B-Instruct-4bit",
59
+ help="MLX 모델 (mlx-community 허브). 메모리 빠듯하면 -3B-4bit 로.")
60
+ ap.add_argument("--data", required=True, help="chat jsonl (structverify export)")
61
+ ap.add_argument("--out", default="./adapter")
62
+ ap.add_argument("--iters", type=int, default=100)
63
+ ap.add_argument("--batch", type=int, default=1)
64
+ ap.add_argument("--rank", type=int, default=8)
65
+ ap.add_argument("--lr", type=float, default=1e-4)
66
+ args = ap.parse_args()
67
+
68
+ os.makedirs(args.out, exist_ok=True)
69
+ data_dir = tempfile.mkdtemp(prefix="sv_mlx_")
70
+ ntr, nva = to_mlx_data(args.data, data_dir)
71
+ print(f"[train_mlx] MLX 데이터 변환: train {ntr} · valid {nva} → {data_dir}")
72
+
73
+ cmd = [
74
+ sys.executable, "-m", "mlx_lm", "lora",
75
+ "--model", args.model, "--train",
76
+ "--data", data_dir, "--adapter-path", args.out,
77
+ "--iters", str(args.iters), "--batch-size", str(args.batch),
78
+ "--num-layers", "8", "--learning-rate", str(args.lr),
79
+ "--steps-per-report", "1", # TrainDoctor용 촘촘한 loss
80
+ ]
81
+ print("[train_mlx] 실행:", " ".join(cmd))
82
+ proc = subprocess.run(cmd, capture_output=True, text=True)
83
+ log = proc.stdout + "\n" + proc.stderr
84
+ print(log[-2000:])
85
+ shutil.rmtree(data_dir, ignore_errors=True)
86
+
87
+ # mlx 로그 → 표준 trainer_state.json (TrainDoctor 호환)
88
+ hist = parse_mlx_log(log)
89
+ with open(os.path.join(args.out, "trainer_state.json"), "w", encoding="utf-8") as f:
90
+ json.dump({"log_history": hist}, f, ensure_ascii=False)
91
+ if proc.returncode != 0:
92
+ print(f"⚠ mlx_lm.lora 종료코드 {proc.returncode} — 로그 확인")
93
+ sys.exit(proc.returncode)
94
+ print(f"\n✅ 완료 — 어댑터: {args.out} (trainer_state.json {len(hist)}개 기록)")
95
+ print(" 다음: TrainDoctor 진단, EvalGate 채택 판정")
96
+
97
+
98
+ if __name__ == "__main__":
99
+ main()
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env python
2
+ """StructVerify QLoRA 학습 레시피 — RTX 3060 12GB / Colab T4 검증됨(사양 기준).
3
+
4
+ 이 파일은 GPU 머신에서 실행하는 *샌드박스 레시피*다. 코어 라이브러리는 이 파일을 만들지도,
5
+ 직접 import 하지도 않는다(무거운 deps 격리). 표준 chat jsonl 만 먹으므로 데이터는 어떤
6
+ 경로로 만들어도 됨.
7
+
8
+ 준비 (GPU 머신):
9
+ pip install "structverify[training]" # unsloth·trl·peft·bitsandbytes·torch
10
+ # 또는: pip install unsloth trl peft bitsandbytes accelerate datasets
11
+
12
+ 실행:
13
+ python train_qlora.py --model unsloth/Qwen2.5-7B-Instruct \
14
+ --data train.jsonl --out ./adapter --max-steps 60
15
+
16
+ 산출: {out}/ 에 LoRA 어댑터 + trainer_state.json (→ TrainDoctor가 읽음)
17
+ """
18
+ import argparse
19
+ import json
20
+
21
+
22
+ def load_chat_dataset(path):
23
+ from datasets import Dataset
24
+ rows = []
25
+ with open(path, encoding="utf-8") as f:
26
+ for line in f:
27
+ line = line.strip()
28
+ if line:
29
+ rows.append(json.loads(line))
30
+ # messages 만 남김 (task 필드는 학습에 불필요)
31
+ return Dataset.from_list([{"messages": r["messages"]} for r in rows])
32
+
33
+
34
+ def main():
35
+ ap = argparse.ArgumentParser()
36
+ ap.add_argument("--model", default="unsloth/Qwen2.5-7B-Instruct",
37
+ help="4bit 베이스 모델 (unsloth 허브 권장). 3060 6GB면 3B 계열로.")
38
+ ap.add_argument("--data", required=True, help="chat jsonl (structverify가 export)")
39
+ ap.add_argument("--out", default="./adapter")
40
+ ap.add_argument("--max-steps", type=int, default=60)
41
+ ap.add_argument("--lr", type=float, default=2e-4)
42
+ ap.add_argument("--rank", type=int, default=16)
43
+ ap.add_argument("--seq-len", type=int, default=2048)
44
+ ap.add_argument("--batch", type=int, default=1)
45
+ ap.add_argument("--grad-accum", type=int, default=8)
46
+ args = ap.parse_args()
47
+
48
+ # unsloth: 3060/T4에서 QLoRA 2배 빠름 + 메모리 절약
49
+ from unsloth import FastLanguageModel
50
+ from trl import SFTTrainer, SFTConfig
51
+
52
+ model, tokenizer = FastLanguageModel.from_pretrained(
53
+ model_name=args.model,
54
+ max_seq_length=args.seq_len,
55
+ load_in_4bit=True, # QLoRA — 12GB VRAM에 7B 적재
56
+ )
57
+ model = FastLanguageModel.get_peft_model(
58
+ model,
59
+ r=args.rank,
60
+ target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
61
+ "gate_proj", "up_proj", "down_proj"],
62
+ lora_alpha=args.rank * 2,
63
+ lora_dropout=0.0,
64
+ bias="none",
65
+ use_gradient_checkpointing="unsloth",
66
+ )
67
+
68
+ ds = load_chat_dataset(args.data)
69
+
70
+ def fmt(ex):
71
+ return {"text": tokenizer.apply_chat_template(
72
+ ex["messages"], tokenize=False, add_generation_prompt=False)}
73
+ ds = ds.map(fmt)
74
+
75
+ trainer = SFTTrainer(
76
+ model=model, tokenizer=tokenizer, train_dataset=ds,
77
+ args=SFTConfig(
78
+ output_dir=args.out,
79
+ per_device_train_batch_size=args.batch,
80
+ gradient_accumulation_steps=args.grad_accum,
81
+ warmup_steps=5,
82
+ max_steps=args.max_steps,
83
+ learning_rate=args.lr,
84
+ logging_steps=1, # TrainDoctor용 촘촘한 loss 기록
85
+ max_grad_norm=1.0, # 발산 방지
86
+ optim="adamw_8bit",
87
+ lr_scheduler_type="linear",
88
+ seed=42,
89
+ dataset_text_field="text",
90
+ report_to="none",
91
+ ),
92
+ )
93
+ trainer.train()
94
+
95
+ # 어댑터 + trainer_state.json 저장 (TrainDoctor가 읽음)
96
+ model.save_pretrained(args.out)
97
+ tokenizer.save_pretrained(args.out)
98
+ trainer.state.save_to_json(f"{args.out}/trainer_state.json")
99
+ print(f"\n✅ 완료 — 어댑터: {args.out} (trainer_state.json 포함)")
100
+ print(" 다음: TrainDoctor로 진단, EvalGate로 채택 여부 판정")
101
+
102
+
103
+ if __name__ == "__main__":
104
+ main()