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
structverify/__init__.py
ADDED
|
@@ -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
|