G0~G8 성과·동맹 측정 OS 작업 일괄 고정
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 산출물은 커밋에서 제외했다.
This commit is contained in:
parent
93dd8f82d7
commit
16e791e044
390 changed files with 243188 additions and 499 deletions
809
apps/api/app/routes/continuous_improvement.py
Normal file
809
apps/api/app/routes/continuous_improvement.py
Normal file
|
|
@ -0,0 +1,809 @@
|
|||
"""Standalone secure HTTP boundary for G8 Continuous Improvement OS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
from collections.abc import AsyncIterator
|
||||
from datetime import datetime
|
||||
from typing import Annotated, Literal
|
||||
from uuid import UUID
|
||||
|
||||
import asyncpg
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException, status
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
from .. import db
|
||||
from ..config import Settings, get_settings
|
||||
from ..contracts.continuous_improvement import (
|
||||
AgenticReleaseManifest,
|
||||
ContentBenchmarkQualification,
|
||||
ContentSourceArtifact,
|
||||
GeneratedContentDraft,
|
||||
IndependentRedTeamReview,
|
||||
ModelCalibrationSnapshot,
|
||||
OperationalIncident,
|
||||
)
|
||||
from ..deps import Principal, Role, require_role
|
||||
from ..engine_client import engine_client
|
||||
from ..services import continuous_improvement_store
|
||||
from ..services import continuous_improvement_agentic
|
||||
|
||||
|
||||
router = APIRouter(tags=["continuous-improvement"])
|
||||
INTERNAL_TOKEN_HEADER = "X-Vignette-Continuous-Improvement-Token"
|
||||
MIN_INTERNAL_TOKEN_LENGTH = 32
|
||||
SyntheticDataClassification = Literal["synthetic_replay_red_team_coverage_drift"]
|
||||
|
||||
|
||||
async def _research_db_provider() -> AsyncIterator[asyncpg.Connection]:
|
||||
async with db.acquire(ai_view="research", ai_context=True) as conn:
|
||||
yield conn
|
||||
|
||||
|
||||
def _authenticate_internal(settings: Settings, presented_token: str | None) -> None:
|
||||
configured_token = settings.continuous_improvement_internal_token.get_secret_value()
|
||||
if len(configured_token) < MIN_INTERNAL_TOKEN_LENGTH:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="continuous improvement automation is unavailable",
|
||||
)
|
||||
if presented_token is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="internal authentication required",
|
||||
)
|
||||
if not secrets.compare_digest(presented_token, configured_token):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="internal authentication failed",
|
||||
)
|
||||
|
||||
|
||||
async def continuous_improvement_internal_db(
|
||||
settings: Annotated[Settings, Depends(get_settings)],
|
||||
presented_token: Annotated[str | None, Header(alias=INTERNAL_TOKEN_HEADER)] = None,
|
||||
) -> AsyncIterator[asyncpg.Connection]:
|
||||
"""Fail closed before acquiring the research-view database connection."""
|
||||
|
||||
_authenticate_internal(settings, presented_token)
|
||||
async for conn in _research_db_provider():
|
||||
yield conn
|
||||
|
||||
|
||||
ResearchDB = Annotated[asyncpg.Connection, Depends(continuous_improvement_internal_db)]
|
||||
AdminPrincipal = Annotated[Principal, Depends(require_role(Role.ADMIN))]
|
||||
|
||||
|
||||
async def continuous_improvement_admin_db(
|
||||
principal: AdminPrincipal,
|
||||
) -> AsyncIterator[asyncpg.Connection]:
|
||||
"""Acquire DB state with the effective admin role selected by the role gate."""
|
||||
|
||||
async with db.acquire(
|
||||
role=Role.ADMIN.value,
|
||||
user_id=principal.user_id,
|
||||
cohort_ids=principal.cohort_ids,
|
||||
) as conn:
|
||||
yield conn
|
||||
|
||||
|
||||
AdminDB = Annotated[asyncpg.Connection, Depends(continuous_improvement_admin_db)]
|
||||
|
||||
|
||||
class ContentPipelineRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", protected_namespaces=())
|
||||
|
||||
submission_id: UUID
|
||||
pipeline_id: UUID
|
||||
benchmark_record_id: UUID
|
||||
qualification_id: UUID
|
||||
data_classification: SyntheticDataClassification
|
||||
draft: GeneratedContentDraft
|
||||
sources: list[ContentSourceArtifact] = Field(min_length=1, max_length=100)
|
||||
reviews: list[IndependentRedTeamReview] = Field(min_length=2, max_length=20)
|
||||
benchmark: ContentBenchmarkQualification
|
||||
|
||||
|
||||
class ContentPipelineResponse(BaseModel):
|
||||
submission_id: UUID
|
||||
pipeline_id: UUID
|
||||
qualification_id: UUID
|
||||
candidate_catalog_entry_id: str
|
||||
state: Literal["pending_human_approval"]
|
||||
human_approval_required: Literal[True]
|
||||
catalog_promoted: Literal[False]
|
||||
idempotent_replay: bool
|
||||
clinical_claim_allowed: Literal[False] = False
|
||||
|
||||
|
||||
class AgenticContentPipelineRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", protected_namespaces=())
|
||||
|
||||
submission_id: UUID
|
||||
pipeline_id: UUID
|
||||
benchmark_record_id: UUID
|
||||
qualification_id: UUID
|
||||
data_classification: SyntheticDataClassification
|
||||
source_packs: list[continuous_improvement_agentic.AgenticSourcePack] = Field(
|
||||
min_length=1, max_length=20
|
||||
)
|
||||
content_kind: Literal["case", "rupture", "practice", "benchmark"]
|
||||
difficulty_level: int = Field(ge=1, le=5)
|
||||
variant_count: int = Field(default=3, ge=3, le=12)
|
||||
prompt_version: str = Field(default="1.0.0", min_length=1, max_length=80)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def unique_source_packs(self) -> "AgenticContentPipelineRequest":
|
||||
source_ids = [item.artifact.source_id for item in self.source_packs]
|
||||
if len(source_ids) != len(set(source_ids)):
|
||||
raise ValueError("agentic source pack ids must be unique")
|
||||
return self
|
||||
|
||||
|
||||
class AgenticContentPipelineResponse(ContentPipelineResponse):
|
||||
draft_id: str
|
||||
benchmark_id: str
|
||||
red_team_review_count: int = Field(ge=2)
|
||||
benchmark_variant_count: int = Field(ge=3)
|
||||
agent_calls_executed: int = Field(ge=0)
|
||||
trigger_kind: Literal["source_pack", "operational_incident"]
|
||||
|
||||
|
||||
class IncidentAdversarialPipelineRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", protected_namespaces=())
|
||||
|
||||
submission_id: UUID
|
||||
pipeline_id: UUID
|
||||
benchmark_record_id: UUID
|
||||
qualification_id: UUID
|
||||
data_classification: SyntheticDataClassification
|
||||
difficulty_level: int = Field(default=5, ge=1, le=5)
|
||||
variant_count: int = Field(default=3, ge=3, le=12)
|
||||
prompt_version: str = Field(default="1.0.0", min_length=1, max_length=80)
|
||||
|
||||
|
||||
class GateArtifact(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
artifact_record_id: UUID
|
||||
artifact_id: str = Field(min_length=1, max_length=180)
|
||||
content_sha256: str = Field(pattern=r"^[a-f0-9]{64}$")
|
||||
provenance_uri: str = Field(pattern=r"^(repo|db|audit)://[a-zA-Z0-9_./:-]+$")
|
||||
|
||||
|
||||
class CompleteGateArtifacts(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
baseline: GateArtifact
|
||||
threshold: GateArtifact
|
||||
provenance: list[GateArtifact] = Field(min_length=1, max_length=100)
|
||||
rollback: GateArtifact
|
||||
|
||||
@model_validator(mode="after")
|
||||
def artifact_ids_are_unique(self) -> "CompleteGateArtifacts":
|
||||
values = [
|
||||
self.baseline.artifact_record_id,
|
||||
self.threshold.artifact_record_id,
|
||||
*(item.artifact_record_id for item in self.provenance),
|
||||
self.rollback.artifact_record_id,
|
||||
]
|
||||
if len(values) != len(set(values)):
|
||||
raise ValueError("gate artifact UUIDs must be unique")
|
||||
return self
|
||||
|
||||
|
||||
class ModelChangeGateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", protected_namespaces=())
|
||||
|
||||
submission_id: UUID
|
||||
gate_id: UUID
|
||||
baseline_snapshot_record_id: UUID
|
||||
candidate_snapshot_record_id: UUID
|
||||
data_classification: SyntheticDataClassification
|
||||
baseline: ModelCalibrationSnapshot
|
||||
candidate: ModelCalibrationSnapshot
|
||||
artifacts: CompleteGateArtifacts
|
||||
|
||||
@model_validator(mode="after")
|
||||
def baseline_and_candidate_are_distinct(self) -> "ModelChangeGateRequest":
|
||||
if self.baseline.snapshot_id == self.candidate.snapshot_id:
|
||||
raise ValueError("baseline and candidate snapshots must be distinct")
|
||||
if self.baseline_snapshot_record_id == self.candidate_snapshot_record_id:
|
||||
raise ValueError("baseline and candidate record UUIDs must be distinct")
|
||||
return self
|
||||
|
||||
|
||||
class ModelChangeGateResponse(BaseModel):
|
||||
submission_id: UUID
|
||||
gate_id: UUID
|
||||
gate_decision: Literal["promote", "rollback", "quarantine"]
|
||||
state: Literal["pending_human_approval"]
|
||||
human_approval_required: Literal[True]
|
||||
promotion_executed: Literal[False]
|
||||
idempotent_replay: bool
|
||||
|
||||
|
||||
class ReleaseGateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", protected_namespaces=())
|
||||
|
||||
submission_id: UUID
|
||||
gate_id: UUID
|
||||
data_classification: SyntheticDataClassification
|
||||
manifest: AgenticReleaseManifest
|
||||
artifacts: CompleteGateArtifacts
|
||||
|
||||
|
||||
class ReleaseGateResponse(BaseModel):
|
||||
submission_id: UUID
|
||||
gate_id: UUID
|
||||
qualified: bool
|
||||
state: Literal["pending_human_approval"]
|
||||
human_approval_required: Literal[True]
|
||||
promotion_executed: Literal[False]
|
||||
idempotent_replay: bool
|
||||
|
||||
|
||||
class IncidentDagRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
submission_id: UUID
|
||||
incident_record_id: UUID
|
||||
data_classification: SyntheticDataClassification
|
||||
incident: OperationalIncident
|
||||
|
||||
|
||||
class IncidentDagResponse(BaseModel):
|
||||
submission_id: UUID
|
||||
incident_record_id: UUID
|
||||
node_count: Literal[4]
|
||||
idempotent_replay: bool
|
||||
pii_included: Literal[False] = False
|
||||
|
||||
|
||||
class HumanApprovalRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
submission_id: UUID
|
||||
approval_event_id: UUID
|
||||
effect_record_id: UUID
|
||||
target_kind: Literal["content_qualification", "model_change_gate", "release_gate"]
|
||||
target_id: UUID
|
||||
decision: Literal[
|
||||
"approve_content",
|
||||
"approve_promotion",
|
||||
"authorize_rollback",
|
||||
"reject",
|
||||
"keep_quarantine",
|
||||
]
|
||||
reason_code: str = Field(min_length=1, max_length=180)
|
||||
evidence_refs: list[str] = Field(min_length=1, max_length=100)
|
||||
|
||||
@field_validator("evidence_refs")
|
||||
@classmethod
|
||||
def evidence_refs_are_unique(cls, value: list[str]) -> list[str]:
|
||||
if len(value) != len(set(value)):
|
||||
raise ValueError("approval evidence refs must be unique")
|
||||
return value
|
||||
|
||||
|
||||
class HumanApprovalResponse(BaseModel):
|
||||
submission_id: UUID
|
||||
approval_event_id: UUID
|
||||
target_kind: str
|
||||
target_id: UUID
|
||||
decision: str
|
||||
effect_record_id: UUID
|
||||
idempotent_replay: bool
|
||||
|
||||
|
||||
class MonitorEventRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
submission_id: UUID
|
||||
lifecycle_event_id: UUID
|
||||
data_classification: SyntheticDataClassification
|
||||
target_kind: Literal["model_change_gate", "release_gate"]
|
||||
target_id: UUID
|
||||
event_status: Literal[
|
||||
"healthy", "drift_detected", "rollback_recommended", "rollback_verified"
|
||||
]
|
||||
evidence_refs: list[str] = Field(min_length=1, max_length=100)
|
||||
|
||||
|
||||
class MonitorEventResponse(BaseModel):
|
||||
submission_id: UUID
|
||||
lifecycle_event_id: UUID
|
||||
event_status: str
|
||||
idempotent_replay: bool
|
||||
|
||||
|
||||
class CatalogGroundedClaim(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
claim: str = Field(min_length=1, max_length=600)
|
||||
source_ref: str = Field(min_length=1, max_length=180)
|
||||
|
||||
|
||||
class CatalogVisiblePayload(BaseModel):
|
||||
"""Explicit allowlist for content that may cross the approved catalog boundary."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
title: str = Field(min_length=1, max_length=180)
|
||||
synthetic_profile: str = Field(min_length=1, max_length=1200)
|
||||
scenario: str = Field(min_length=1, max_length=6000)
|
||||
rupture_or_challenge: str = Field(min_length=1, max_length=2400)
|
||||
learner_task: str = Field(min_length=1, max_length=2000)
|
||||
success_criteria: list[str] = Field(min_length=1, max_length=10)
|
||||
source_refs: list[str] = Field(min_length=1, max_length=100)
|
||||
grounded_claims: list[CatalogGroundedClaim] = Field(min_length=1, max_length=20)
|
||||
|
||||
|
||||
class ContentQualificationView(BaseModel):
|
||||
qualification_id: UUID
|
||||
pipeline_id: UUID
|
||||
catalog_entry_id: str
|
||||
payload_sha256: str
|
||||
content_kind: Literal["case", "rupture", "practice", "benchmark"]
|
||||
difficulty_level: int = Field(ge=1, le=5)
|
||||
synthetic_identity_id: str
|
||||
source_count: int = Field(ge=1)
|
||||
red_team_review_count: int = Field(ge=2)
|
||||
benchmark_variant_count: int = Field(ge=3)
|
||||
benchmark_pass_rate: float = Field(ge=0.85, le=1.0)
|
||||
source_provenance_uris: list[str] = Field(min_length=1)
|
||||
draft_payload: CatalogVisiblePayload | None = None
|
||||
gate_state: Literal["pending_human_approval"]
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class ModelChangeGateView(BaseModel):
|
||||
gate_id: UUID
|
||||
gate_decision: Literal["promote", "rollback", "quarantine"]
|
||||
reasons: list[str]
|
||||
state: Literal["pending_human_approval"]
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class ReleaseGateView(BaseModel):
|
||||
gate_id: UUID
|
||||
release_id: str
|
||||
qualified: bool
|
||||
state: Literal["pending_human_approval"]
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class GateArtifactView(BaseModel):
|
||||
artifact_record_id: UUID
|
||||
owner_kind: Literal["model_change_gate", "release_gate"]
|
||||
owner_id: UUID
|
||||
artifact_kind: Literal["baseline", "threshold", "provenance", "rollback"]
|
||||
artifact_id: str
|
||||
content_sha256: str
|
||||
provenance_uri: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class HumanApprovalView(BaseModel):
|
||||
approval_event_id: UUID
|
||||
target_kind: Literal["content_qualification", "model_change_gate", "release_gate"]
|
||||
target_id: UUID
|
||||
decision: Literal[
|
||||
"approve_content",
|
||||
"approve_promotion",
|
||||
"authorize_rollback",
|
||||
"reject",
|
||||
"keep_quarantine",
|
||||
]
|
||||
reason_code: str
|
||||
evidence_refs: list[str]
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class CatalogEntryView(BaseModel):
|
||||
catalog_record_id: UUID
|
||||
qualification_id: UUID
|
||||
catalog_entry_id: str
|
||||
status: Literal["approved"]
|
||||
clinical_claim_allowed: Literal[False]
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class ApprovedCatalogConsumerEntry(BaseModel):
|
||||
catalog_record_id: UUID
|
||||
qualification_id: UUID
|
||||
catalog_entry_id: str
|
||||
payload_sha256: str
|
||||
content_kind: Literal["case", "rupture", "practice", "benchmark"]
|
||||
difficulty_level: int = Field(ge=1, le=5)
|
||||
synthetic_identity_id: str
|
||||
source_provenance_uris: list[str] = Field(min_length=1)
|
||||
payload: CatalogVisiblePayload
|
||||
status: Literal["approved"]
|
||||
clinical_claim_allowed: Literal[False]
|
||||
approved_at: datetime
|
||||
|
||||
|
||||
class ApprovedCatalogConsumerResponse(BaseModel):
|
||||
entries: list[ApprovedCatalogConsumerEntry]
|
||||
data_classification: SyntheticDataClassification
|
||||
human_approval_required: Literal[True] = True
|
||||
raw_transcript_included: Literal[False] = False
|
||||
pii_included: Literal[False] = False
|
||||
clinical_claim_allowed: Literal[False] = False
|
||||
|
||||
|
||||
class LifecycleEventView(BaseModel):
|
||||
lifecycle_event_id: UUID
|
||||
target_kind: Literal["model_change_gate", "release_gate"]
|
||||
target_id: UUID
|
||||
event_type: Literal["promotion", "rollback", "monitor"]
|
||||
event_status: Literal[
|
||||
"approved",
|
||||
"requested",
|
||||
"executed",
|
||||
"failed",
|
||||
"healthy",
|
||||
"drift_detected",
|
||||
"rollback_recommended",
|
||||
"rollback_verified",
|
||||
]
|
||||
approval_event_id: UUID | None = None
|
||||
artifact_record_id: UUID | None = None
|
||||
evidence_refs: list[str]
|
||||
executor_receipt_id: str | None = None
|
||||
executor_evidence_refs: list[str] | None = None
|
||||
created_at: datetime
|
||||
|
||||
@model_validator(mode="after")
|
||||
def enforce_rollback_receipt_boundary(self) -> "LifecycleEventView":
|
||||
if self.event_type != "rollback":
|
||||
if self.executor_receipt_id is not None or self.executor_evidence_refs:
|
||||
raise ValueError("non-rollback lifecycle event cannot carry a receipt")
|
||||
return self
|
||||
if self.approval_event_id is None or self.artifact_record_id is None:
|
||||
raise ValueError("rollback requires approval and pinned artifact")
|
||||
if self.event_status == "executed":
|
||||
if not (self.executor_receipt_id or "").strip():
|
||||
raise ValueError("executed rollback requires executor receipt id")
|
||||
if not self.executor_evidence_refs:
|
||||
raise ValueError("executed rollback requires executor evidence")
|
||||
if not set(self.executor_evidence_refs).issubset(self.evidence_refs):
|
||||
raise ValueError("executor evidence must be included in lifecycle evidence")
|
||||
elif self.executor_receipt_id is not None or self.executor_evidence_refs:
|
||||
raise ValueError("non-executed rollback cannot carry executor receipt evidence")
|
||||
return self
|
||||
|
||||
|
||||
class OperationalIncidentView(BaseModel):
|
||||
incident_record_id: UUID
|
||||
incident_id: str
|
||||
error_fingerprint: str
|
||||
affected_contract: str
|
||||
evidence_refs: list[str]
|
||||
pii_included: Literal[False]
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class RegressionDagNodeView(BaseModel):
|
||||
node_record_id: UUID
|
||||
incident_record_id: UUID
|
||||
node_id: str
|
||||
node_type: Literal["reproduction_test", "implementation", "e2e", "runtime_proof"]
|
||||
depends_on_record_ids: list[UUID]
|
||||
evidence_ref: str | None = None
|
||||
node_status: Literal["pending", "passed", "failed"]
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class ContinuousImprovementViewResponse(BaseModel):
|
||||
model_config = ConfigDict(protected_namespaces=())
|
||||
|
||||
content_qualifications: list[ContentQualificationView]
|
||||
model_change_gates: list[ModelChangeGateView]
|
||||
release_gates: list[ReleaseGateView]
|
||||
gate_artifacts: list[GateArtifactView]
|
||||
approvals: list[HumanApprovalView]
|
||||
catalog_entries: list[CatalogEntryView]
|
||||
lifecycle_events: list[LifecycleEventView]
|
||||
incidents: list[OperationalIncidentView]
|
||||
regression_dag_nodes: list[RegressionDagNodeView]
|
||||
data_classification: SyntheticDataClassification
|
||||
silent_auto_promotion_allowed: Literal[False]
|
||||
raw_transcript_included: Literal[False]
|
||||
pii_included: Literal[False]
|
||||
clinical_claim_allowed: Literal[False]
|
||||
|
||||
|
||||
def _artifact_dict(value: GateArtifact) -> dict[str, object]:
|
||||
return value.model_dump(mode="python")
|
||||
|
||||
|
||||
def _raise_store_error(exc: Exception) -> None:
|
||||
if isinstance(exc, continuous_improvement_store.ContinuousImprovementConflictError):
|
||||
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
||||
if isinstance(exc, continuous_improvement_store.ContinuousImprovementNotFoundError):
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/internal/continuous-improvement/agentic-content-pipelines",
|
||||
response_model=AgenticContentPipelineResponse,
|
||||
status_code=201,
|
||||
)
|
||||
async def create_agentic_content_pipeline(
|
||||
request: AgenticContentPipelineRequest, conn: ResearchDB
|
||||
) -> AgenticContentPipelineResponse:
|
||||
"""Run real model-owned generation/review/judging before pending approval."""
|
||||
|
||||
try:
|
||||
result = await continuous_improvement_agentic.run_agentic_content_pipeline(
|
||||
conn=conn,
|
||||
engine=engine_client,
|
||||
submission_id=request.submission_id,
|
||||
pipeline_id=request.pipeline_id,
|
||||
benchmark_record_id=request.benchmark_record_id,
|
||||
qualification_id=request.qualification_id,
|
||||
source_packs=request.source_packs,
|
||||
content_kind=request.content_kind,
|
||||
difficulty_level=request.difficulty_level,
|
||||
variant_count=request.variant_count,
|
||||
prompt_version=request.prompt_version,
|
||||
trigger_kind="source_pack",
|
||||
)
|
||||
except continuous_improvement_store.ContinuousImprovementError as exc:
|
||||
_raise_store_error(exc)
|
||||
except continuous_improvement_agentic.AgenticPipelineRejectedError as exc:
|
||||
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||
except continuous_improvement_agentic.AgenticPipelineExecutionError as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
return AgenticContentPipelineResponse.model_validate(result.model_dump(mode="json"))
|
||||
|
||||
|
||||
@router.post(
|
||||
"/internal/continuous-improvement/incidents/{incident_record_id}/adversarial-content-pipelines",
|
||||
response_model=AgenticContentPipelineResponse,
|
||||
status_code=201,
|
||||
)
|
||||
async def create_incident_adversarial_content_pipeline(
|
||||
incident_record_id: UUID,
|
||||
request: IncidentAdversarialPipelineRequest,
|
||||
conn: ResearchDB,
|
||||
) -> AgenticContentPipelineResponse:
|
||||
"""Turn a persisted metadata-only operational failure into a gated benchmark."""
|
||||
|
||||
try:
|
||||
incident = await continuous_improvement_store.read_operational_incident(
|
||||
conn, incident_record_id=incident_record_id
|
||||
)
|
||||
source_pack = (
|
||||
continuous_improvement_agentic.source_pack_from_operational_incident(
|
||||
incident
|
||||
)
|
||||
)
|
||||
result = await continuous_improvement_agentic.run_agentic_content_pipeline(
|
||||
conn=conn,
|
||||
engine=engine_client,
|
||||
submission_id=request.submission_id,
|
||||
pipeline_id=request.pipeline_id,
|
||||
benchmark_record_id=request.benchmark_record_id,
|
||||
qualification_id=request.qualification_id,
|
||||
source_packs=[source_pack],
|
||||
content_kind="benchmark",
|
||||
difficulty_level=request.difficulty_level,
|
||||
variant_count=request.variant_count,
|
||||
prompt_version=request.prompt_version,
|
||||
trigger_kind="operational_incident",
|
||||
)
|
||||
except continuous_improvement_store.ContinuousImprovementError as exc:
|
||||
_raise_store_error(exc)
|
||||
except continuous_improvement_agentic.AgenticPipelineRejectedError as exc:
|
||||
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||
except continuous_improvement_agentic.AgenticPipelineExecutionError as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
return AgenticContentPipelineResponse.model_validate(result.model_dump(mode="json"))
|
||||
|
||||
|
||||
@router.post(
|
||||
"/internal/continuous-improvement/content-pipelines",
|
||||
response_model=ContentPipelineResponse,
|
||||
status_code=201,
|
||||
)
|
||||
async def create_content_pipeline(
|
||||
request: ContentPipelineRequest, conn: ResearchDB
|
||||
) -> ContentPipelineResponse:
|
||||
try:
|
||||
result = await continuous_improvement_store.submit_content_pipeline(
|
||||
conn,
|
||||
submission_id=request.submission_id,
|
||||
pipeline_id=request.pipeline_id,
|
||||
benchmark_record_id=request.benchmark_record_id,
|
||||
qualification_id=request.qualification_id,
|
||||
draft=request.draft,
|
||||
sources=request.sources,
|
||||
reviews=request.reviews,
|
||||
benchmark=request.benchmark,
|
||||
)
|
||||
except (ValueError, continuous_improvement_store.ContinuousImprovementError) as exc:
|
||||
_raise_store_error(exc)
|
||||
return ContentPipelineResponse.model_validate(result)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/internal/continuous-improvement/model-change-gates",
|
||||
response_model=ModelChangeGateResponse,
|
||||
status_code=201,
|
||||
)
|
||||
async def create_model_change_gate(
|
||||
request: ModelChangeGateRequest, conn: ResearchDB
|
||||
) -> ModelChangeGateResponse:
|
||||
try:
|
||||
result = await continuous_improvement_store.submit_model_change_gate(
|
||||
conn,
|
||||
submission_id=request.submission_id,
|
||||
gate_id=request.gate_id,
|
||||
baseline_snapshot_record_id=request.baseline_snapshot_record_id,
|
||||
candidate_snapshot_record_id=request.candidate_snapshot_record_id,
|
||||
baseline=request.baseline,
|
||||
candidate=request.candidate,
|
||||
baseline_artifact=_artifact_dict(request.artifacts.baseline),
|
||||
threshold_artifact=_artifact_dict(request.artifacts.threshold),
|
||||
provenance_artifacts=[
|
||||
_artifact_dict(item) for item in request.artifacts.provenance
|
||||
],
|
||||
rollback_artifact=_artifact_dict(request.artifacts.rollback),
|
||||
)
|
||||
except continuous_improvement_store.ContinuousImprovementError as exc:
|
||||
_raise_store_error(exc)
|
||||
return ModelChangeGateResponse.model_validate(result)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/internal/continuous-improvement/release-gates",
|
||||
response_model=ReleaseGateResponse,
|
||||
status_code=201,
|
||||
)
|
||||
async def create_release_gate(
|
||||
request: ReleaseGateRequest, conn: ResearchDB
|
||||
) -> ReleaseGateResponse:
|
||||
try:
|
||||
result = await continuous_improvement_store.submit_release_gate(
|
||||
conn,
|
||||
submission_id=request.submission_id,
|
||||
gate_id=request.gate_id,
|
||||
manifest=request.manifest,
|
||||
baseline_artifact=_artifact_dict(request.artifacts.baseline),
|
||||
threshold_artifact=_artifact_dict(request.artifacts.threshold),
|
||||
provenance_artifacts=[
|
||||
_artifact_dict(item) for item in request.artifacts.provenance
|
||||
],
|
||||
rollback_artifact=_artifact_dict(request.artifacts.rollback),
|
||||
)
|
||||
except continuous_improvement_store.ContinuousImprovementError as exc:
|
||||
_raise_store_error(exc)
|
||||
return ReleaseGateResponse.model_validate(result)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/internal/continuous-improvement/incidents",
|
||||
response_model=IncidentDagResponse,
|
||||
status_code=201,
|
||||
)
|
||||
async def create_incident_dag(
|
||||
request: IncidentDagRequest, conn: ResearchDB
|
||||
) -> IncidentDagResponse:
|
||||
try:
|
||||
result = await continuous_improvement_store.submit_incident_dag(
|
||||
conn,
|
||||
submission_id=request.submission_id,
|
||||
incident_record_id=request.incident_record_id,
|
||||
incident=request.incident,
|
||||
)
|
||||
except continuous_improvement_store.ContinuousImprovementError as exc:
|
||||
_raise_store_error(exc)
|
||||
return IncidentDagResponse.model_validate(result)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/continuous-improvement/approvals",
|
||||
response_model=HumanApprovalResponse,
|
||||
status_code=201,
|
||||
)
|
||||
async def create_human_approval(
|
||||
request: HumanApprovalRequest,
|
||||
principal: AdminPrincipal,
|
||||
conn: AdminDB,
|
||||
settings: Annotated[Settings, Depends(get_settings)],
|
||||
) -> HumanApprovalResponse:
|
||||
try:
|
||||
rollback_executor = (
|
||||
continuous_improvement_agentic.build_configured_rollback_executor(settings)
|
||||
if request.decision == "authorize_rollback"
|
||||
else None
|
||||
)
|
||||
result = await continuous_improvement_store.append_human_approval(
|
||||
conn,
|
||||
submission_id=request.submission_id,
|
||||
approval_event_id=request.approval_event_id,
|
||||
effect_record_id=request.effect_record_id,
|
||||
target_kind=request.target_kind,
|
||||
target_id=request.target_id,
|
||||
decision=request.decision,
|
||||
actor_uid=UUID(principal.user_id),
|
||||
reason_code=request.reason_code,
|
||||
evidence_refs=request.evidence_refs,
|
||||
rollback_executor=rollback_executor,
|
||||
)
|
||||
except continuous_improvement_store.ContinuousImprovementError as exc:
|
||||
_raise_store_error(exc)
|
||||
return HumanApprovalResponse.model_validate(result)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/internal/continuous-improvement/monitor-events",
|
||||
response_model=MonitorEventResponse,
|
||||
status_code=201,
|
||||
)
|
||||
async def create_monitor_event(
|
||||
request: MonitorEventRequest, conn: ResearchDB
|
||||
) -> MonitorEventResponse:
|
||||
try:
|
||||
result = await continuous_improvement_store.append_monitor_event(
|
||||
conn,
|
||||
submission_id=request.submission_id,
|
||||
lifecycle_event_id=request.lifecycle_event_id,
|
||||
target_kind=request.target_kind,
|
||||
target_id=request.target_id,
|
||||
event_status=request.event_status,
|
||||
evidence_refs=request.evidence_refs,
|
||||
)
|
||||
except continuous_improvement_store.ContinuousImprovementError as exc:
|
||||
_raise_store_error(exc)
|
||||
return MonitorEventResponse.model_validate(result)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/internal/continuous-improvement",
|
||||
response_model=ContinuousImprovementViewResponse,
|
||||
)
|
||||
async def read_internal_continuous_improvement(
|
||||
conn: ResearchDB,
|
||||
) -> ContinuousImprovementViewResponse:
|
||||
result = await continuous_improvement_store.read_continuous_improvement_view(conn)
|
||||
return ContinuousImprovementViewResponse.model_validate(result)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/continuous-improvement",
|
||||
response_model=ContinuousImprovementViewResponse,
|
||||
)
|
||||
async def read_admin_continuous_improvement(
|
||||
_principal: AdminPrincipal,
|
||||
conn: AdminDB,
|
||||
) -> ContinuousImprovementViewResponse:
|
||||
result = await continuous_improvement_store.read_continuous_improvement_view(conn)
|
||||
return ContinuousImprovementViewResponse.model_validate(result)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/continuous-improvement/catalog",
|
||||
response_model=ApprovedCatalogConsumerResponse,
|
||||
)
|
||||
async def read_admin_approved_catalog(
|
||||
_principal: AdminPrincipal,
|
||||
conn: AdminDB,
|
||||
) -> ApprovedCatalogConsumerResponse:
|
||||
entries = await continuous_improvement_store.read_approved_catalog_entries(conn)
|
||||
return ApprovedCatalogConsumerResponse(
|
||||
entries=[ApprovedCatalogConsumerEntry.model_validate(item) for item in entries],
|
||||
data_classification="synthetic_replay_red_team_coverage_drift",
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"INTERNAL_TOKEN_HEADER",
|
||||
"continuous_improvement_internal_db",
|
||||
"continuous_improvement_admin_db",
|
||||
"router",
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue