Kairo-Jinkendo/backend/prompt_registry.py
Lars 53047b6c16
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 18s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 18s
Test Suite / playwright-smoke (push) Successful in 24s
AP0.4: Feature-, Prompt- und Config-Registry mit Single-Render und Entitlements-Features.
Migration 005, Registry-Sync, Placeholder-Validation, capability-geschuetzte API, Tests und Abschlussbericht v0.1.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-04 23:29:02 +02:00

311 lines
11 KiB
Python

"""Prompt definition registry with versioning and optional steps."""
from __future__ import annotations
import json
from dataclasses import dataclass
from db import get_connection
from feature_registry import VALID_CONTEXT_KINDS, VALID_EXECUTION_MODES
VALID_STEP_TYPES = frozenset(
{"template", "resolver", "llm_call", "postprocess", "validation"}
)
@dataclass(frozen=True)
class PromptStepRegistration:
step_key: str
step_order: int
step_type: str
template_body: str
input_mapping: dict | None = None
output_mapping: dict | None = None
enabled: bool = True
@dataclass(frozen=True)
class PromptVersionRegistration:
version: str
body: str
input_schema: dict | None = None
output_schema: dict | None = None
model_policy_key: str | None = None
changelog: str = ""
steps: tuple[PromptStepRegistration, ...] = ()
@dataclass(frozen=True)
class PromptRegistration:
prompt_key: str
name: str
purpose: str
context_kind: str
execution_mode: str
active_version: str
versions: tuple[PromptVersionRegistration, ...]
_REGISTRY: dict[str, PromptRegistration] = {}
def register_prompt(registration: PromptRegistration) -> None:
if not registration.prompt_key or not registration.prompt_key.strip():
raise ValueError("Prompt key is required")
if registration.context_kind not in VALID_CONTEXT_KINDS:
raise ValueError(f"Unknown context_kind: {registration.context_kind}")
if registration.execution_mode not in VALID_EXECUTION_MODES:
raise ValueError(f"Unknown execution_mode: {registration.execution_mode}")
if not registration.versions:
raise ValueError("At least one prompt version is required")
version_ids = {v.version for v in registration.versions}
if registration.active_version not in version_ids:
raise ValueError(f"active_version not found in versions: {registration.active_version}")
for step in _iter_steps(registration):
if step.step_type not in VALID_STEP_TYPES:
raise ValueError(f"Unknown step_type: {step.step_type}")
if registration.prompt_key in _REGISTRY:
raise ValueError(f"Prompt already registered: {registration.prompt_key}")
_REGISTRY[registration.prompt_key] = registration
def _iter_steps(registration: PromptRegistration):
for version in registration.versions:
yield from version.steps
def get_registered_prompts() -> tuple[PromptRegistration, ...]:
return tuple(_REGISTRY.values())
def clear_prompt_registry_for_tests() -> None:
_REGISTRY.clear()
def sync_prompts_to_db() -> int:
if not _REGISTRY:
print("[prompt_registry] Keine Prompts registriert — Sync übersprungen")
return 0
conn = get_connection()
try:
with conn.cursor() as cur:
for reg in _REGISTRY.values():
cur.execute(
"""
INSERT INTO prompt_definitions (
prompt_key, name, purpose, context_kind, execution_mode, status
)
VALUES (%s, %s, %s, %s, %s, 'active')
ON CONFLICT (prompt_key) DO UPDATE SET
name = EXCLUDED.name,
purpose = EXCLUDED.purpose,
context_kind = EXCLUDED.context_kind,
execution_mode = EXCLUDED.execution_mode,
status = 'active',
updated_at = NOW()
RETURNING id
""",
(
reg.prompt_key,
reg.name,
reg.purpose,
reg.context_kind,
reg.execution_mode,
),
)
definition_id = cur.fetchone()[0]
active_version_id = None
for version in reg.versions:
cur.execute(
"""
INSERT INTO prompt_versions (
prompt_definition_id, version, body,
input_schema, output_schema, model_policy_key, changelog
)
VALUES (%s, %s, %s, %s::jsonb, %s::jsonb, %s, %s)
ON CONFLICT (prompt_definition_id, version) DO UPDATE SET
body = EXCLUDED.body,
input_schema = EXCLUDED.input_schema,
output_schema = EXCLUDED.output_schema,
model_policy_key = EXCLUDED.model_policy_key,
changelog = EXCLUDED.changelog
RETURNING id
""",
(
definition_id,
version.version,
version.body,
json.dumps(version.input_schema) if version.input_schema else None,
json.dumps(version.output_schema) if version.output_schema else None,
version.model_policy_key,
version.changelog,
),
)
version_id = cur.fetchone()[0]
if version.version == reg.active_version:
active_version_id = version_id
for step in version.steps:
cur.execute(
"""
INSERT INTO prompt_steps (
prompt_version_id, step_key, step_order, step_type,
template_body, input_mapping, output_mapping, enabled
)
VALUES (%s, %s, %s, %s, %s, %s::jsonb, %s::jsonb, %s)
ON CONFLICT (prompt_version_id, step_key) DO UPDATE SET
step_order = EXCLUDED.step_order,
step_type = EXCLUDED.step_type,
template_body = EXCLUDED.template_body,
input_mapping = EXCLUDED.input_mapping,
output_mapping = EXCLUDED.output_mapping,
enabled = EXCLUDED.enabled,
updated_at = NOW()
""",
(
version_id,
step.step_key,
step.step_order,
step.step_type,
step.template_body,
json.dumps(step.input_mapping) if step.input_mapping else None,
json.dumps(step.output_mapping) if step.output_mapping else None,
step.enabled,
),
)
if active_version_id:
cur.execute(
"""
UPDATE prompt_definitions
SET active_version_id = %s, updated_at = NOW()
WHERE id = %s
""",
(active_version_id, definition_id),
)
conn.commit()
print(f"[prompt_registry] Sync OK — {len(_REGISTRY)} Prompt(s)")
return 0
except Exception as exc:
conn.rollback()
print(f"[prompt_registry] Sync FAIL: {exc}")
return 1
finally:
conn.close()
def load_prompt_definition(prompt_key: str) -> dict | None:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT id, prompt_key, name, purpose, context_kind, execution_mode,
status, active_version_id
FROM prompt_definitions
WHERE prompt_key = %s
""",
(prompt_key,),
)
row = cur.fetchone()
if not row:
return None
return {
"id": str(row[0]),
"prompt_key": row[1],
"name": row[2],
"purpose": row[3],
"context_kind": row[4],
"execution_mode": row[5],
"status": row[6],
"active_version_id": str(row[7]) if row[7] else None,
}
finally:
conn.close()
def load_active_prompt_version(definition: dict) -> dict | None:
version_id = definition.get("active_version_id")
if not version_id:
return None
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT id, version, body, input_schema, output_schema, model_policy_key
FROM prompt_versions
WHERE id = %s
""",
(version_id,),
)
row = cur.fetchone()
if not row:
return None
return {
"id": str(row[0]),
"version": row[1],
"body": row[2],
"input_schema": row[3],
"output_schema": row[4],
"model_policy_key": row[5],
}
finally:
conn.close()
def load_template_steps(version_id: str) -> list[dict]:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT step_key, step_order, step_type, template_body, enabled
FROM prompt_steps
WHERE prompt_version_id = %s AND enabled = TRUE
ORDER BY step_order, step_key
""",
(version_id,),
)
return [
{
"step_key": row[0],
"step_order": row[1],
"step_type": row[2],
"template_body": row[3],
"enabled": row[4],
}
for row in cur.fetchall()
]
finally:
conn.close()
def list_prompt_definitions() -> list[dict]:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT prompt_key, name, purpose, context_kind, execution_mode, status
FROM prompt_definitions
ORDER BY prompt_key
"""
)
return [
{
"prompt_key": row[0],
"name": row[1],
"purpose": row[2],
"context_kind": row[3],
"execution_mode": row[4],
"status": row[5],
}
for row in cur.fetchall()
]
finally:
conn.close()