Source code for app.schema.generate

"""
Schemas for descriptive generation and structured run logging.

These models cover the character-description page's core generation request,
generation response, and append-only JSONL logging format.  They depend on
the shared axis payload primitives but otherwise contain no business logic.
"""

from __future__ import annotations

from typing import Any

from pydantic import BaseModel, Field

from app.schema.axis import AxisPayload


[docs] class GenerateRequest(BaseModel): """ Full request body for POST /api/generate. The frontend serialises its current in-memory state into this object and sends it to the backend, which forwards the payload to Ollama. Optional fields allow the frontend to override per-request settings (model, temperature, token budget, custom system prompt) without restarting the server. """ payload: AxisPayload = Field( ..., description="The axis payload that will be serialised to JSON and sent as the LLM user turn.", ) model: str = Field( ..., description="Ollama model name to use (e.g. 'gemma2:2b').", examples=["gemma2:2b", "llama3.2:1b"], ) temperature: float = Field( default=0.2, ge=0.0, le=2.0, description=( "Sampling temperature. Low values (0.1–0.3) reduce variance " "and are preferred for drift comparison experiments." ), ) max_tokens: int = Field( default=120, ge=10, le=2048, description="Maximum number of tokens the model may produce.", ) system_prompt: str | None = Field( default=None, description=( "Optional override for the system prompt. When None the server " "loads the default prompt from app/prompts/character_description/." ), ) ollama_host: str | None = Field( default=None, description=( "Optional Ollama server URL override. When None the server uses " "the OLLAMA_HOST environment variable (default: http://localhost:11434)." ), examples=["http://localhost:11434", "http://192.168.1.50:11434"], )
[docs] class GenerateResponse(BaseModel): """ Response body for POST /api/generate. Carries the raw LLM output plus enough metadata to reconstruct the exact call for logging, diffing, and repeatability analysis. The four hash fields (``input_hash``, ``system_prompt_hash``, ``output_hash``, ``ipc_id``) form the **Interpretive Provenance Chain (IPC)** — a complete fingerprint of every variable that influenced the generation. Together they enable drift detection and reproducibility audits. """ text: str = Field( ..., description="The raw descriptive paragraph produced by the LLM.", ) model: str = Field( ..., description="Ollama model name that was used.", ) temperature: float = Field( ..., description="Sampling temperature that was used.", ) usage: dict[str, Any] | None = Field( default=None, description=( "Token-usage information returned by Ollama, if available. " "Keys vary by model (e.g. 'prompt_eval_count', 'eval_count')." ), ) input_hash: str | None = Field( default=None, description="SHA-256 hex digest of the canonical serialised AxisPayload.", ) system_prompt_hash: str | None = Field( default=None, description=( "SHA-256 hex digest of the normalised system prompt used. " "Normalisation strips per-line whitespace and edge blank lines." ), ) output_hash: str | None = Field( default=None, description=( "SHA-256 hex digest of the normalised output text. " "Normalisation collapses extra spaces and strips edges." ), ) ipc_id: str | None = Field( default=None, description=( "Interpretive Provenance Chain identifier — a SHA-256 digest of " "input_hash:system_prompt_hash:model:temperature:max_tokens:seed. " "Two generations with the same IPC ID used identical inputs." ), )
[docs] class LogEntry(BaseModel): """ Schema for a single structured log record written by POST /api/log. Captures everything needed to reproduce a run and detect drift across repeated calls with the same seed / policy hash. `input_hash` groups runs that should be identical, `payload` stores the full deterministic input, and the remaining fields capture the generated output plus the runtime parameters and timestamp needed for later audit or drift analysis. """ input_hash: str = Field( ..., description="SHA-256 hex digest of the canonical serialised AxisPayload.", ) payload: AxisPayload = Field( ..., description="Full axis payload for this run.", ) output: str = Field( ..., description="LLM-generated descriptive text.", ) model: str = Field(..., description="Ollama model used.") temperature: float = Field(..., description="Sampling temperature used.") max_tokens: int = Field(..., description="Token budget used.") timestamp: str = Field( ..., description="ISO-8601 UTC timestamp of when the log entry was created.", ) system_prompt_hash: str | None = Field( default=None, description=( "SHA-256 hex digest of the normalised system prompt. " "None when the log call did not include the prompt text." ), ) output_hash: str | None = Field( default=None, description="SHA-256 hex digest of the normalised output text.", ) ipc_id: str | None = Field( default=None, description=( "Interpretive Provenance Chain identifier. None when " "system_prompt was not provided to the log endpoint." ), )