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,83 @@
1
+ """StructVerify Lab v2.0 — 도메인 독립형 LLM 기반 사실검증 플랫폼
2
+
3
+ Graph + JSON 하이브리드 저장 · 2-Agent 아키텍처 · 도메인 적응형 학습 루프
4
+
5
+ 공개 API:
6
+ verify_text(text, config) — 텍스트 일회성 검증
7
+ verify_document(source, source_type, config) — URL/PDF/DOCX/TEXT 일회성 검증
8
+ VerificationEngine(config) — config를 주입해 두고 재사용 (객체형)
9
+ VerificationPipeline(config) — 저수준 파이프라인
10
+
11
+ [DONE] 김예슬
12
+ - 외부 사용자에게 보여줄 진입점 먼저 고정하고, 내부 모듈은 점진적으로 구현
13
+ - verify_text() / verify_document() → 내부에서 VerificationPipeline 호출
14
+ - VerificationEngine: 같은 config로 여러 번 검증하는 객체형 진입점
15
+ - 확인 사항: (1) import 가능 (2) verify_text() 실행 시 VerificationPipeline.run() 호출
16
+
17
+ [v2 - 김예슬] 지연 로딩(PEP 562):
18
+ `import structverify` 시점에는 무거운 pipeline/deps(trafilatura·httpx·bs4 등)를
19
+ 끌어오지 않는다. 실제로 verify_text 등을 *사용*할 때 처음 로드된다.
20
+ → import/패키지 탐지가 가볍고, 선택적 deps 없이도 import 자체는 성공.
21
+ """
22
+
23
+ # High-level ergonomic API (structverify.api) — the "point-and-call" surface.
24
+ _API_SYMBOLS = frozenset({
25
+ "verify", # one-shot fact verification → Report
26
+ "Verifier", # reusable fact-verification engine
27
+ "Ruleset", # bring-your-own rulebook → conformance checking
28
+ "DataSource", # company data (CSV/docs) as ground truth
29
+ "Report", # document-level result (iterable of Result)
30
+ "Result", # single verified claim
31
+ "Verdict", # single conformance decision
32
+ "build_config", # provider/key → full engine config
33
+ "configure_logging", # console/file logging setup
34
+ })
35
+
36
+ # Low-level entry points (structverify.core.pipeline).
37
+ _PIPELINE_SYMBOLS = frozenset({
38
+ "verify_text",
39
+ "verify_document",
40
+ "VerificationEngine",
41
+ "VerificationPipeline",
42
+ })
43
+
44
+ # Live progress (structverify.progress) — local web dashboard + terminal bar.
45
+ _PROGRESS_SYMBOLS = frozenset({
46
+ "progress_dashboard",
47
+ })
48
+
49
+ # Supervised learning loop (structverify.training) — light core (no torch);
50
+ # actual QLoRA training is delegated to the [training] extra / recipe.
51
+ _TRAINING_SYMBOLS = frozenset({
52
+ "LearningLoop", "DataCurator", "TrainDoctor", "EvalGate",
53
+ "build_seed_dataset", "export_dataset",
54
+ })
55
+
56
+ __all__ = sorted(_API_SYMBOLS | _PIPELINE_SYMBOLS | _PROGRESS_SYMBOLS | _TRAINING_SYMBOLS)
57
+ __version__ = "0.3.0"
58
+
59
+
60
+ def __getattr__(name: str):
61
+ # PEP 562 — resolve public symbols lazily so ``import structverify`` stays
62
+ # light (no heavy pipeline/deps pulled until a symbol is actually used).
63
+ if name in _API_SYMBOLS:
64
+ from structverify import api
65
+
66
+ return getattr(api, name)
67
+ if name in _PIPELINE_SYMBOLS:
68
+ from structverify.core import pipeline
69
+
70
+ return getattr(pipeline, name)
71
+ if name in _PROGRESS_SYMBOLS:
72
+ from structverify import progress
73
+
74
+ return getattr(progress, name)
75
+ if name in _TRAINING_SYMBOLS:
76
+ from structverify import training
77
+
78
+ return getattr(training, name)
79
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
80
+
81
+
82
+ def __dir__():
83
+ return sorted(__all__)
File without changes
@@ -0,0 +1,341 @@
1
+ """
2
+ adaptation/adapter_trainer.py — NCP CLOVA Studio Tuning API 기반 학습 관리
3
+
4
+ HCX 모델은 로컬 PEFT가 아니라 NCP CLOVA Studio Tuning API를 사용합니다.
5
+ 학습 데이터 → NCP Object Storage 업로드 → Tuning API 호출 → polling → 평가 → 배포
6
+
7
+ [김예슬 - 2026-04-24]
8
+ - train():
9
+ · 학습 샘플 → NCP Tuning API JSONL 포맷 변환 (_samples_to_jsonl)
10
+ · NCP Object Storage 업로드 (_upload_to_object_storage)
11
+ · POST /tuning/v2/tasks 호출 (_call_tuning_api)
12
+ · 완료까지 polling (_poll_tuning_status)
13
+ - evaluate():
14
+ · 벤치마크 JSONL 로드 → Tuning된 모델로 추론 → F1 계산
15
+ - deploy():
16
+ · model_versions 테이블 INSERT (MLflow는 추후 연동)
17
+ · config 파일 갱신으로 runtime_agent에 핫스왑 알림
18
+ - _samples_to_jsonl(): NCP PEFT 학습 포맷으로 변환
19
+ · {"text": "### 지시: ...\n\n### 질문: ...\n\n### 답변: ..."}
20
+
21
+ [NCP Tuning API]
22
+ POST https://clovastudio.apigw.ntruss.com/tuning/v2/tasks
23
+ tuningType: "PEFT"
24
+ taskType: "GENERATION"
25
+
26
+ [참고] KnowLA (NAACL 2024), AdaptLLM (ICLR 2024)
27
+ """
28
+ from __future__ import annotations
29
+
30
+ import asyncio
31
+ import json
32
+ import os
33
+ import time
34
+ from typing import Any
35
+
36
+ import httpx
37
+
38
+ from structverify.utils.logger import get_logger
39
+
40
+ logger = get_logger(__name__)
41
+
42
+ NCP_TUNING_URL = "https://clovastudio.apigw.ntruss.com/tuning/v2/tasks"
43
+ NCP_TUNING_STATUS_URL = "https://clovastudio.apigw.ntruss.com/tuning/v2/tasks/{task_id}"
44
+ NCP_OBS_ENDPOINT = "https://kr.object.ncloudstorage.com"
45
+
46
+
47
+ class AdapterTrainer:
48
+ """NCP CLOVA Studio Tuning API 기반 Adapter 학습 관리자."""
49
+
50
+ def __init__(self, config: dict | None = None):
51
+ self.config = config or {}
52
+ adapt_cfg = self.config.get("adaptation", {})
53
+ self.eval_min_score = float(adapt_cfg.get("eval_min_score", 0.85))
54
+ self.epochs = int(adapt_cfg.get("train_epochs", 8))
55
+ self.lr = float(adapt_cfg.get("learning_rate", 1e-5))
56
+ self.bucket = self.config.get("storage", {}).get("bucket", "structverify-training")
57
+
58
+ self.api_key = os.environ.get("CLOVASTUDIO_API_KEY", "")
59
+ self.ncp_access_key = os.environ.get("NCP_ACCESS_KEY", "")
60
+ self.ncp_secret_key = os.environ.get("NCP_SECRET_KEY", "")
61
+
62
+ async def train(self, domain: str, samples: list[dict[str, Any]]) -> str | None:
63
+ """
64
+ 학습 샘플 → NCP Tuning API → adapter 반환.
65
+
66
+ 흐름:
67
+ 1) JSONL 변환
68
+ 2) NCP Object Storage 업로드
69
+ 3) Tuning API 호출
70
+ 4) 완료 polling
71
+ """
72
+ if not samples:
73
+ logger.warning("학습 샘플 없음 — train() 중단")
74
+ return None
75
+
76
+ logger.info(f"[Trainer] 학습 시작: domain={domain}, samples={len(samples)}")
77
+
78
+ # 1) JSONL 변환
79
+ jsonl_path = f"/tmp/structverify_train_{domain}_{int(time.time())}.jsonl"
80
+ _samples_to_jsonl(samples, jsonl_path)
81
+ logger.info(f"[Trainer] JSONL 변환: {jsonl_path}")
82
+
83
+ # 2) Object Storage 업로드
84
+ remote_path = await _upload_to_object_storage(
85
+ local_path=jsonl_path,
86
+ bucket=self.bucket,
87
+ access_key=self.ncp_access_key,
88
+ secret_key=self.ncp_secret_key,
89
+ domain=domain,
90
+ )
91
+ if not remote_path:
92
+ logger.error("[Trainer] Object Storage 업로드 실패")
93
+ return None
94
+
95
+ # 3) Tuning API 호출
96
+ task_name = f"structverify_{domain}_{int(time.time())}"
97
+ task_id = await _call_tuning_api(
98
+ api_key=self.api_key,
99
+ task_name=task_name,
100
+ model="HCX-003",
101
+ ncp_file_path=remote_path,
102
+ bucket=self.bucket,
103
+ access_key=self.ncp_access_key,
104
+ secret_key=self.ncp_secret_key,
105
+ epochs=self.epochs,
106
+ learning_rate=self.lr,
107
+ )
108
+ if not task_id:
109
+ logger.error("[Trainer] Tuning API 호출 실패")
110
+ return None
111
+
112
+ # 4) 완료 polling
113
+ adapter_path = await _poll_tuning_status(self.api_key, task_id)
114
+ if adapter_path:
115
+ logger.info(f"[Trainer] 학습 완료: {adapter_path}")
116
+ return adapter_path
117
+
118
+ async def evaluate(self, adapter_path: str, benchmark: str) -> float:
119
+ """
120
+ 학습된 Adapter 성능 평가.
121
+
122
+ benchmark JSONL 로드 → 각 샘플에 대해 LLM 추론 → 정확도 계산.
123
+ """
124
+ if not os.path.exists(benchmark):
125
+ logger.warning(f"[Trainer] 벤치마크 파일 없음: {benchmark} → 기본 점수 0.0")
126
+ return 0.0
127
+
128
+ # 벤치마크 로드
129
+ samples: list[dict] = []
130
+ with open(benchmark, encoding="utf-8") as f:
131
+ for line in f:
132
+ line = line.strip()
133
+ if line:
134
+ try:
135
+ samples.append(json.loads(line))
136
+ except json.JSONDecodeError:
137
+ pass
138
+
139
+ if not samples:
140
+ logger.warning("[Trainer] 벤치마크 샘플 없음")
141
+ return 0.0
142
+
143
+ # TODO: adapter_path 모델로 실제 추론 후 정확도 계산
144
+ # 현재는 샘플 개수 기반 더미 점수
145
+ # 실제 구현 시:
146
+ # from structverify.utils.llm_client import LLMClient
147
+ # llm = LLMClient(config={"adapter_path": adapter_path, ...})
148
+ # correct = 0
149
+ # for s in samples:
150
+ # pred = await llm.generate_json(s["input"])
151
+ # if pred.get("label") == s["expected_label"]:
152
+ # correct += 1
153
+ # return correct / len(samples)
154
+ logger.warning(f"[Trainer] evaluate() stub: {len(samples)}개 샘플 → 0.0")
155
+ return 0.0
156
+
157
+ async def deploy(self, adapter_path: str, domain: str) -> bool:
158
+ """
159
+ 평가 통과 Adapter 배포.
160
+
161
+ 1) model_versions 테이블에 등록 (TODO 박재윤)
162
+ 2) domain-packs/{domain}/model.yaml 업데이트
163
+ → runtime_agent가 다음 요청부터 새 adapter 사용
164
+ """
165
+ logger.info(f"[Trainer] Adapter 배포: {adapter_path} → {domain}")
166
+
167
+ # domain-packs/{domain}/ 디렉토리에 model 정보 기록
168
+ pack_dir = os.path.join("domain-packs", domain)
169
+ model_yaml = os.path.join(pack_dir, "model.yaml")
170
+ os.makedirs(pack_dir, exist_ok=True)
171
+
172
+ import yaml
173
+ model_info = {
174
+ "adapter_path": adapter_path,
175
+ "domain": domain,
176
+ "deployed_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
177
+ "base_model": "HCX-003",
178
+ "tuning_type": "PEFT",
179
+ }
180
+ with open(model_yaml, "w", encoding="utf-8") as f:
181
+ yaml.dump(model_info, f, allow_unicode=True)
182
+
183
+ logger.info(f"[Trainer] 배포 완료: {model_yaml}")
184
+
185
+ # TODO [박재윤]: model_versions 테이블 INSERT
186
+ # await db.execute(
187
+ # "INSERT INTO model_versions (domain, adapter_path, deployed_at) VALUES (...)"
188
+ # )
189
+
190
+ return True
191
+
192
+
193
+ # ── 내부 헬퍼 ─────────────────────────────────────────────────────────────
194
+
195
+ def _samples_to_jsonl(samples: list[dict[str, Any]], output_path: str) -> None:
196
+ """
197
+ 학습 샘플 → NCP PEFT 학습 포맷 JSONL 변환.
198
+
199
+ NCP Tuning API 학습 포맷:
200
+ {"text": "### 지시: {instruction}\n\n### 질문: {input}\n\n### 답변: {output}"}
201
+ """
202
+ with open(output_path, "w", encoding="utf-8") as f:
203
+ for sample in samples:
204
+ instruction = sample.get("instruction", "")
205
+ inp = sample.get("input", "")
206
+ out = sample.get("output", "")
207
+
208
+ if instruction:
209
+ text = f"### 지시: {instruction}\n\n### 질문: {inp}\n\n### 답변: {out}"
210
+ else:
211
+ text = f"### 질문: {inp}\n\n### 답변: {out}"
212
+
213
+ f.write(json.dumps({"text": text}, ensure_ascii=False) + "\n")
214
+
215
+
216
+ async def _upload_to_object_storage(
217
+ local_path: str,
218
+ bucket: str,
219
+ access_key: str,
220
+ secret_key: str,
221
+ domain: str,
222
+ ) -> str | None:
223
+ """
224
+ NCP Object Storage (S3 호환)에 학습 데이터 업로드.
225
+
226
+ NCP Object Storage는 S3 호환 API 사용:
227
+ endpoint: https://kr.object.ncloudstorage.com
228
+ """
229
+ if not access_key or not secret_key:
230
+ logger.warning("[Trainer] NCP_ACCESS_KEY / NCP_SECRET_KEY 미설정 → stub")
231
+ return f"training/{domain}/{os.path.basename(local_path)}"
232
+
233
+ try:
234
+ import boto3
235
+ s3 = boto3.client(
236
+ "s3",
237
+ endpoint_url=NCP_OBS_ENDPOINT,
238
+ aws_access_key_id=access_key,
239
+ aws_secret_access_key=secret_key,
240
+ )
241
+ remote_path = f"training/{domain}/{os.path.basename(local_path)}"
242
+ s3.upload_file(local_path, bucket, remote_path)
243
+ logger.info(f"[Trainer] Object Storage 업로드: s3://{bucket}/{remote_path}")
244
+ return remote_path
245
+ except Exception as e:
246
+ logger.error(f"[Trainer] Object Storage 업로드 실패: {e}")
247
+ return None
248
+
249
+
250
+ async def _call_tuning_api(
251
+ api_key: str,
252
+ task_name: str,
253
+ model: str,
254
+ ncp_file_path: str,
255
+ bucket: str,
256
+ access_key: str,
257
+ secret_key: str,
258
+ epochs: int = 8,
259
+ learning_rate: float = 1e-5,
260
+ ) -> str | None:
261
+ """
262
+ NCP CLOVA Studio Tuning API 호출.
263
+
264
+ POST https://clovastudio.apigw.ntruss.com/tuning/v2/tasks
265
+ """
266
+ if not api_key:
267
+ logger.warning("[Trainer] CLOVASTUDIO_API_KEY 미설정 → stub")
268
+ return f"stub_task_{int(time.time())}"
269
+
270
+ headers = {
271
+ "Authorization": f"Bearer {api_key}",
272
+ "X-NCP-CLOVASTUDIO-REQUEST-ID": f"train-{int(time.time())}",
273
+ }
274
+ payload = {
275
+ "name": task_name,
276
+ "model": model,
277
+ "tuningType": "PEFT",
278
+ "taskType": "GENERATION",
279
+ "trainEpochs": str(epochs),
280
+ "learningRate": str(learning_rate),
281
+ "trainingDatasetFilePath": ncp_file_path,
282
+ "trainingDatasetBucket": bucket,
283
+ "trainingDatasetAccessKey": access_key,
284
+ "trainingDatasetSecretKey": secret_key,
285
+ }
286
+
287
+ try:
288
+ async with httpx.AsyncClient(timeout=30) as client:
289
+ resp = await client.post(NCP_TUNING_URL, data=payload, headers=headers)
290
+ resp.raise_for_status()
291
+ data = resp.json()
292
+ task_id = data.get("result", {}).get("taskId")
293
+ logger.info(f"[Trainer] Tuning 작업 생성: task_id={task_id}")
294
+ return task_id
295
+ except Exception as e:
296
+ logger.error(f"[Trainer] Tuning API 호출 실패: {e}")
297
+ return None
298
+
299
+
300
+ async def _poll_tuning_status(
301
+ api_key: str,
302
+ task_id: str,
303
+ poll_interval: int = 60,
304
+ max_wait: int = 7200,
305
+ ) -> str | None:
306
+ """
307
+ Tuning 작업 완료까지 polling.
308
+
309
+ 상태: READY / RUNNING / SUCCEEDED / FAILED / CANCELED
310
+ """
311
+ if task_id.startswith("stub_"):
312
+ logger.warning(f"[Trainer] stub task_id — polling 스킵")
313
+ return None
314
+
315
+ headers = {"Authorization": f"Bearer {api_key}"}
316
+ url = NCP_TUNING_STATUS_URL.format(task_id=task_id)
317
+ elapsed = 0
318
+
319
+ while elapsed < max_wait:
320
+ try:
321
+ async with httpx.AsyncClient(timeout=10) as client:
322
+ resp = await client.get(url, headers=headers)
323
+ resp.raise_for_status()
324
+ data = resp.json()
325
+ status = data.get("result", {}).get("status", "")
326
+ logger.info(f"[Trainer] Tuning 상태: {task_id} → {status} ({elapsed}s)")
327
+
328
+ if status == "SUCCEEDED":
329
+ return task_id
330
+ if status in ("FAILED", "CANCELED"):
331
+ logger.error(f"[Trainer] Tuning 실패: {status}")
332
+ return None
333
+
334
+ except Exception as e:
335
+ logger.warning(f"[Trainer] 상태 조회 실패 (재시도): {e}")
336
+
337
+ await asyncio.sleep(poll_interval)
338
+ elapsed += poll_interval
339
+
340
+ logger.error(f"[Trainer] Tuning 타임아웃: {max_wait}s 초과")
341
+ return None
@@ -0,0 +1,31 @@
1
+ """
2
+ adaptation/feedback_store.py — Feedback Store (Human Review / 실패 사례)
3
+
4
+ [참고] Feedback Adaptation for RAG (arXiv 2604.06647)
5
+ 피드백 수집 → 학습 데이터 변환 → 모델 개선 비동기 루프
6
+ """
7
+ from __future__ import annotations
8
+ from structverify.core.schemas import FeedbackEvent
9
+ from structverify.utils.logger import get_logger
10
+
11
+ logger = get_logger(__name__)
12
+
13
+
14
+ class FeedbackStore:
15
+ def __init__(self, config: dict | None = None):
16
+ self.config = config or {}
17
+ self._events: list[FeedbackEvent] = [] # TODO: PostgreSQL로 교체
18
+
19
+ async def save(self, event: FeedbackEvent) -> None:
20
+ """피드백 이벤트 저장 — TODO: DB INSERT 구현"""
21
+ self._events.append(event)
22
+
23
+ async def count_by_domain(self) -> int:
24
+ """도메인별 미처리 피드백 수 — TODO: DB COUNT 쿼리"""
25
+ return len(self._events)
26
+
27
+ async def get_pending(self) -> list[FeedbackEvent]:
28
+ """미처리 피드백 조회 — TODO: DB SELECT WHERE status=pending"""
29
+ pending = list(self._events)
30
+ self._events.clear()
31
+ return pending