Kansho/backend/engine.py

124 lines
4.5 KiB
Python

"""Single prompt execution entry. Templates live in the DB; LLM egress only via Privacy Gateway."""
from __future__ import annotations
from typing import Any
import placeholder_mvp # noqa: F401
import placeholder_system # noqa: F401 registers system keys
import privacy_placeholders
from db import get_db, row_to_dict
from entitlements import EntitlementError, check_feature_access, increment_feature_usage
from placeholders import PlaceholderError, resolve_template
from privacy_gateway import GatewayRequest, PrivacyGatewayError, complete
from journal_reconstruct import parse_dialogue_line
class EngineError(Exception):
def __init__(self, code: str, message: str, status_code: int = 400, diagnostics: dict | None = None):
super().__init__(message)
self.code = code
self.message = message
self.status_code = status_code
self.diagnostics = diagnostics or {}
def load_active_prompt(slug: str) -> dict:
with get_db() as conn:
row = row_to_dict(
conn.execute(
"SELECT * FROM ai_prompts WHERE slug = ? AND active = 1",
(slug,),
).fetchone()
)
if not row:
raise EngineError("prompt_missing", f"Prompt fehlt oder ist inaktiv: {slug}", 404)
return row
def preview_prompt(prompt: dict, context: dict[str, Any] | None = None) -> dict:
template = prompt.get("template") or ""
if prompt.get("prompt_type") != "base":
raise EngineError("prompt_type_not_ready", "Pipeline- und Workflow-Typen sind vorbereitet, aber noch ohne Executor-Stufen.")
try:
rendered = resolve_template(template, context or {})
privacy_tokens = privacy_placeholders.validate_tokens(template)
except PlaceholderError as exc:
raise EngineError(exc.code, exc.message) from exc
return {
"prompt_id": prompt["id"],
"slug": prompt["slug"],
"rendered": rendered,
"privacy_tokens": privacy_tokens,
"llm": False,
}
def user_source_text(dialogue_context: str) -> str:
parts = []
for line in (dialogue_context or "").splitlines():
parsed = parse_dialogue_line(line)
if parsed and parsed["role"] == "user":
parts.append(parsed["body"])
elif line.startswith("user:"):
parts.append(line[5:].lstrip())
return "\n".join(parts)
def execute_prompt(
prompt: dict,
profile_id: str,
purpose: str,
data_class: str,
context: dict[str, Any] | None = None,
*,
max_tokens: int | None = None,
disable_context_compression: bool = False,
budget=None,
diagnostics: dict[str, Any] | None = None,
) -> dict:
preview = preview_prompt(prompt, context)
feature_id = prompt.get("required_feature") or "ai_calls"
try:
check_feature_access(profile_id, feature_id)
except EntitlementError as exc:
raise EngineError(exc.code, exc.message, exc.status_code) from exc
try:
ctx = context or {}
source_text = preview["rendered"]
if purpose == "profile_review" and ctx.get("source_text"):
source_text = "\n".join(
part for part in (ctx.get("source_text"), ctx.get("review_package"), source_text) if part
)
elif not (source_text or "").strip():
source_text = user_source_text(ctx.get("dialogue_context") or "")
result = complete(
GatewayRequest(
prompt_id=prompt["id"],
purpose=purpose,
data_class=data_class,
profile_id=profile_id,
payload={
"rendered": preview["rendered"],
"source_text": source_text,
"privacy_tokens": preview["privacy_tokens"],
"prompt_slug": prompt.get("slug"),
"max_tokens": max_tokens,
"disable_context_compression": disable_context_compression,
"budget": budget,
"diagnostics": diagnostics or {},
},
)
)
except PrivacyGatewayError as exc:
raise EngineError(exc.code, exc.message, exc.status_code, getattr(exc, "diagnostics", None)) from exc
increment_feature_usage(profile_id, feature_id)
return {
**preview,
"llm": True,
"content": result.content,
"provider": result.provider,
"trace": result.trace,
"diagnostics": result.diagnostics,
"local_identities": result.local_identities,
}