Kansho/backend/engine.py
2026-08-25 13:57:23 +02:00

108 lines
4.0 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, last_trace, public_trace
class EngineError(Exception):
def __init__(self, code: str, message: str, status_code: int = 400):
super().__init__(message)
self.code = code
self.message = message
self.status_code = status_code
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():
if 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) -> 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 = user_source_text(ctx.get("dialogue_context") or "")
extras = []
if purpose == "journal_generate":
extras = [part for part in (ctx.get("existing_text"), ctx.get("writing_profile")) if part]
elif purpose == "profile_review":
source_text = ctx.get("source_text") or ""
extras = [part for part in (ctx.get("review_package"),) if part]
if extras:
source_text = "\n".join(part for part in (source_text, *extras) if part)
elif not source_text:
source_text = "\n".join(
part for part in (ctx.get("existing_text"), ctx.get("writing_profile")) if part
)
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"),
},
)
)
except PrivacyGatewayError as exc:
raise EngineError(exc.code, exc.message, exc.status_code) from exc
increment_feature_usage(profile_id, feature_id)
return {
**preview,
"llm": True,
"content": result.content,
"provider": result.provider,
"trace": public_trace(last_trace()),
}