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 산출물은 커밋에서 제외했다.
809 lines
28 KiB
Python
809 lines
28 KiB
Python
"""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",
|
|
]
|