PMFlow/backend/alembic/versions/20260516_agent_runtime_sema...

176 lines
5.7 KiB
Python

"""refresh agent runtime semantic claim contracts
Revision ID: 20260516_agent_runtime_semantic_claims
Revises: 20260516_refresh_agent_goal_frame_prompt
Create Date: 2026-05-16
"""
from __future__ import annotations
import json
import os
from typing import Any
from alembic import op
import sqlalchemy as sa
revision = "20260516_agent_runtime_semantic_claims"
down_revision = "20260516_refresh_agent_goal_frame_prompt"
branch_labels = None
depends_on = None
PROMPT_KEYS = (
"PROMPT_PMFLOW_AI_AGENT_GOAL_FRAME",
"PROMPT_PMFLOW_AI_AGENT_EVIDENCE_NORMALIZER",
"PROMPT_PMFLOW_AI_AGENT_ANSWER_COMPOSER",
"PROMPT_PMFLOW_AI_AGENT_ANSWER_REVISER",
)
REFRESH_NOTES = "Refreshed by 20260516_agent_runtime_semantic_claims"
def _table_exists(bind: Any, table_name: str) -> bool:
return sa.inspect(bind).has_table(table_name)
def _prompt_body(prompt_key: str) -> str:
from app.config import ai_prompts
return str(getattr(ai_prompts, prompt_key, "") or "").strip()
def _refresh_active_prompt(bind: Any, *, tenant_id: int, prompt_key: str, body_snapshot: str) -> None:
active = bind.execute(
sa.text(
"""
SELECT id, body_snapshot FROM prompt_releases
WHERE tenant_id = :tenant_id
AND prompt_key = :prompt_key
AND status = 'active'
LIMIT 1
"""
),
{"tenant_id": tenant_id, "prompt_key": prompt_key},
).mappings().first()
if active is not None and str(active.get("body_snapshot") or "").strip() == body_snapshot:
return
bind.execute(
sa.text(
"""
UPDATE prompt_releases
SET status = 'archived'
WHERE tenant_id = :tenant_id
AND prompt_key = :prompt_key
AND status = 'active'
"""
),
{"tenant_id": tenant_id, "prompt_key": prompt_key},
)
max_version = bind.execute(
sa.text(
"""
SELECT MAX(version) FROM prompt_releases
WHERE tenant_id = :tenant_id
AND prompt_key = :prompt_key
"""
),
{"tenant_id": tenant_id, "prompt_key": prompt_key},
).scalar()
bind.execute(
sa.text(
"""
INSERT INTO prompt_releases
(tenant_id, prompt_key, version, body_snapshot, status, notes, created_by, activated_by, activated_at)
VALUES
(:tenant_id, :prompt_key, :version, :body_snapshot, 'active', :notes, NULL, NULL, :activated_at)
"""
),
{
"tenant_id": tenant_id,
"prompt_key": prompt_key,
"version": int(max_version or 0) + 1,
"body_snapshot": body_snapshot,
"notes": REFRESH_NOTES,
"activated_at": "2026-05-16T00:00:00",
},
)
def _refresh_runtime_profiles(bind: Any, *, tenant_id: int) -> None:
if not _table_exists(bind, "integrations"):
return
row = bind.execute(
sa.text(
"""
SELECT id, config FROM integrations
WHERE tenant_id = :tenant_id
AND type = 'ai_function_runtime_profiles'
AND is_active = true
ORDER BY id DESC
LIMIT 1
"""
),
{"tenant_id": tenant_id},
).mappings().first()
if row is None:
return
config = row.get("config") if isinstance(row.get("config"), dict) else {}
changed = False
for stage in ("active", "draft"):
profiles = ((config.get(stage) or {}).get("profiles") or {}) if isinstance(config.get(stage), dict) else {}
normalizer = profiles.get("pmflow_ai_agent_evidence_normalizer")
if isinstance(normalizer, dict):
if normalizer.get("runtime_mode") != "active":
normalizer["runtime_mode"] = "active"
changed = True
if normalizer.get("quality_gate_mode") != "strict":
normalizer["quality_gate_mode"] = "strict"
changed = True
for key in ("pmflow_ai_agent_answer_composer", "pmflow_ai_agent_answer_reviser"):
profile = profiles.get(key)
if not isinstance(profile, dict):
continue
if profile.get("runtime_mode") != "active":
profile["runtime_mode"] = "active"
changed = True
if profile.get("quality_gate_mode") != "off":
profile["quality_gate_mode"] = "off"
changed = True
if not changed:
return
config["active_version"] = "profile-v20260516-semantic-claims"
config["version"] = config["active_version"]
bind.execute(
sa.text("UPDATE integrations SET config = CAST(:config AS jsonb) WHERE id = :id"),
{"id": int(row["id"]), "config": json.dumps(config, ensure_ascii=False)},
)
def upgrade() -> None:
bind = op.get_bind()
tenant_id = int(os.getenv("PMFLOW_PLATFORM_RUNTIME_TENANT_ID", "1") or "1")
if _table_exists(bind, "prompt_releases"):
for prompt_key in PROMPT_KEYS:
body_snapshot = _prompt_body(prompt_key)
if body_snapshot:
_refresh_active_prompt(bind, tenant_id=tenant_id, prompt_key=prompt_key, body_snapshot=body_snapshot)
_refresh_runtime_profiles(bind, tenant_id=tenant_id)
def downgrade() -> None:
bind = op.get_bind()
if not _table_exists(bind, "prompt_releases"):
return
tenant_id = int(os.getenv("PMFLOW_PLATFORM_RUNTIME_TENANT_ID", "1") or "1")
bind.execute(
sa.text(
"""
DELETE FROM prompt_releases
WHERE tenant_id = :tenant_id
AND notes = :notes
"""
),
{"tenant_id": tenant_id, "notes": REFRESH_NOTES},
)