8월 7일까지 워킹트리에만 남아 있던 미커밋 작업을 커밋한다. 여러 사본 폴더(worktree·clone)에 흩어져 있던 중간 스냅샷을 정리하기 전에 원본을 git 이력으로 고정하는 것이 목적이다. - contracts/routes/services: measurement, outcome_trajectory, rupture_repair, deliberate_practice, calibration_transfer, supervision_research, multimodal_alliance, continuous_improvement 계열 신규 모듈과 테스트 - infra/db/init: 07~16 마이그레이션(측정 기반~calibration transfer 실행) - apps/web: 세션 리뷰 카드·관리 화면·E2E 스펙 추가 - docs/ops: G0~G8 라이브 통합·배포·롤백 증거 문서와 evidence JSON/PNG - scripts: smoke·ledger·릴리스 에이전트·NAS 프리뷰 운영 스크립트 engine.public 로그 .bak과 apps/web/test-results 산출물은 커밋에서 제외했다.
224 lines
8.7 KiB
Python
224 lines
8.7 KiB
Python
"""Run the real G8 scheduled worker against the configured dev engine and DB."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import asyncio
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
from typing import Any
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
API_ROOT = REPO_ROOT / "apps" / "api"
|
|
if str(API_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(API_ROOT))
|
|
os.chdir(API_ROOT)
|
|
|
|
from app import db # noqa: E402
|
|
from app.engine_client import engine_client # noqa: E402
|
|
from app.services import continuous_improvement_agentic as agentic # noqa: E402
|
|
from app.services import continuous_improvement_producer as producer # noqa: E402
|
|
|
|
|
|
class SmokeFailure(RuntimeError):
|
|
pass
|
|
|
|
|
|
class CountingEngine:
|
|
def __init__(self) -> None:
|
|
self.call_count = 0
|
|
self.stages: list[str] = []
|
|
self.responses: list[dict[str, str]] = []
|
|
|
|
async def generate(self, request: Any) -> Any:
|
|
self.call_count += 1
|
|
self.stages.append(str(request.metadata.get("agentic_stage", "unknown")))
|
|
response = await engine_client.generate(
|
|
request,
|
|
timeout=producer.settings.continuous_improvement_producer_engine_timeout_seconds,
|
|
)
|
|
self.responses.append(
|
|
{"provider": str(response.provider), "model": str(response.model)}
|
|
)
|
|
return response
|
|
|
|
|
|
def _sha256_text(value: str) -> str:
|
|
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
|
|
|
|
|
async def _read_evidence_row(job_id: Any) -> dict[str, Any]:
|
|
async with db.acquire(ai_view="research", ai_context=True) as conn:
|
|
row = await conn.fetchrow(
|
|
"""
|
|
SELECT j.job_id, j.job_key, j.status, j.attempt_count,
|
|
j.data_classification, j.source_fingerprint,
|
|
j.last_error_code, j.last_error_message,
|
|
j.result_submission_id, j.result_qualification_id,
|
|
p.pipeline_id, p.generation_model, p.prompt_version,
|
|
p.prompt_sha256, p.payload_sha256, p.state AS pipeline_state,
|
|
p.clinical_claim_allowed,
|
|
q.gate_state, q.catalog_entry_id,
|
|
b.variant_count, b.variant_pass_rate, b.qualified,
|
|
(SELECT count(*) FROM app.ci_red_team_review r
|
|
WHERE r.pipeline_id = p.pipeline_id) AS reviewer_count,
|
|
(SELECT count(*) FROM app.ci_catalog_entry c
|
|
WHERE c.qualification_id = q.qualification_id) AS catalog_count,
|
|
(SELECT count(*) FROM audit.ci_human_approval_event a
|
|
WHERE a.target_kind = 'content_qualification'
|
|
AND a.target_id = q.qualification_id) AS approval_count,
|
|
(SELECT jsonb_agg(jsonb_build_object(
|
|
'source_id', s.source_id,
|
|
'version', s.source_version,
|
|
'content_sha256', s.content_sha256,
|
|
'provenance_uri', s.provenance_uri,
|
|
'usage_status', s.usage_status
|
|
) ORDER BY s.source_id)
|
|
FROM app.ci_source_artifact s
|
|
WHERE s.source_record_id = ANY(p.source_record_ids)) AS sources
|
|
FROM app.ci_agentic_job j
|
|
LEFT JOIN app.ci_content_pipeline p
|
|
ON p.submission_id = j.result_submission_id
|
|
LEFT JOIN app.ci_content_qualification q
|
|
ON q.qualification_id = j.result_qualification_id
|
|
LEFT JOIN app.ci_content_benchmark b
|
|
ON b.pipeline_id = p.pipeline_id
|
|
WHERE j.job_id = $1
|
|
""",
|
|
job_id,
|
|
)
|
|
if row is None:
|
|
raise SmokeFailure("scheduled agentic job is not visible")
|
|
return dict(row)
|
|
|
|
|
|
async def _run(out_path: Path) -> dict[str, Any]:
|
|
await db.init_pool()
|
|
await engine_client.startup()
|
|
try:
|
|
readiness = await engine_client.health_detail()
|
|
if not readiness.get("ok"):
|
|
raise SmokeFailure(f"engine is not ready: {readiness.get('detail')}")
|
|
|
|
spec = producer.load_repo_approved_job()
|
|
job_id = await producer.ensure_repo_approved_job()
|
|
claimed = await producer.claim_next_agentic_job(job_id=job_id)
|
|
if claimed is None:
|
|
raise SmokeFailure("repo-approved job was not available for a fresh model run")
|
|
|
|
first_engine = CountingEngine()
|
|
first = await producer.execute_claimed_agentic_job(
|
|
claimed,
|
|
engine=first_engine,
|
|
)
|
|
row = await _read_evidence_row(job_id)
|
|
|
|
evidence: dict[str, Any] = {
|
|
"evidence_version": "g8-agentic-producer-live-v1",
|
|
"environment": "dev",
|
|
"engine_ready": {
|
|
"ok": bool(readiness.get("ok")),
|
|
"detail": str(readiness.get("detail", "")),
|
|
},
|
|
"repo_source": {
|
|
"job_key": spec.job_key,
|
|
"data_classification": spec.data_classification,
|
|
"source_ids": [item.artifact.source_id for item in spec.source_packs],
|
|
"usage_statuses": [
|
|
item.artifact.usage_status for item in spec.source_packs
|
|
],
|
|
"content_hashes_verified": all(
|
|
_sha256_text(item.content) == item.artifact.content_sha256
|
|
for item in spec.source_packs
|
|
),
|
|
"raw_transcript_included": False,
|
|
"pii_included": False,
|
|
},
|
|
"first_run": {
|
|
"status": first.status,
|
|
"engine_call_count": first_engine.call_count,
|
|
"stages": sorted(first_engine.stages),
|
|
"provider_models": first_engine.responses,
|
|
"error_code": first.error_code,
|
|
},
|
|
"durable_db": row,
|
|
"safety_boundary": {
|
|
"human_approval_required": row.get("gate_state")
|
|
== "pending_human_approval",
|
|
"catalog_promoted": int(row.get("catalog_count") or 0) > 0,
|
|
"human_approval_event_count": int(row.get("approval_count") or 0),
|
|
"clinical_claim_allowed": bool(row.get("clinical_claim_allowed")),
|
|
},
|
|
}
|
|
|
|
if first.status == "completed":
|
|
ids = producer._job_ids(job_id)
|
|
replay_engine = CountingEngine()
|
|
async with db.acquire(ai_view="research", ai_context=True) as conn:
|
|
replay = await agentic.run_agentic_content_pipeline(
|
|
conn=conn,
|
|
engine=replay_engine,
|
|
submission_id=ids["submission"],
|
|
pipeline_id=ids["pipeline"],
|
|
benchmark_record_id=ids["benchmark_record"],
|
|
qualification_id=ids["qualification"],
|
|
source_packs=spec.source_packs,
|
|
content_kind=spec.content_kind,
|
|
difficulty_level=spec.difficulty_level,
|
|
variant_count=spec.variant_count,
|
|
prompt_version=spec.prompt_version,
|
|
trigger_kind=spec.trigger_kind,
|
|
)
|
|
evidence["idempotent_replay"] = {
|
|
"reported": replay.idempotent_replay,
|
|
"engine_call_count": replay_engine.call_count,
|
|
"same_submission_id": str(replay.submission_id)
|
|
== str(row.get("result_submission_id")),
|
|
"same_qualification_id": str(replay.qualification_id)
|
|
== str(row.get("result_qualification_id")),
|
|
}
|
|
else:
|
|
evidence["storage_rejected"] = {
|
|
"pipeline_absent": row.get("pipeline_id") is None,
|
|
"qualification_absent": row.get("result_qualification_id") is None,
|
|
"catalog_count": int(row.get("catalog_count") or 0),
|
|
}
|
|
|
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
out_path.write_text(
|
|
json.dumps(evidence, ensure_ascii=False, indent=2, default=str) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
return evidence
|
|
finally:
|
|
await engine_client.shutdown()
|
|
await db.close_pool()
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
"--out",
|
|
type=Path,
|
|
required=True,
|
|
help="JSON evidence path",
|
|
)
|
|
args = parser.parse_args()
|
|
requested_out = args.out
|
|
out_path = (
|
|
requested_out
|
|
if requested_out.is_absolute()
|
|
else REPO_ROOT / requested_out
|
|
).resolve()
|
|
evidence = asyncio.run(_run(out_path))
|
|
print(json.dumps(evidence, ensure_ascii=False, indent=2, default=str))
|
|
first_status = evidence["first_run"]["status"]
|
|
return 0 if first_status == "completed" else 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|