Add example profiles, schemas, and an end-to-end local fixture

This commit is contained in:
2026-05-04 21:23:48 -05:00
parent eac6b69217
commit 328703df36
8 changed files with 319 additions and 12 deletions

101
README.md
View File

@@ -1,25 +1,102 @@
# scriptorium
Scriptorium is a prompt-profile execution engine written in Go.
Scriptorium (Analyzer) is a generic prompt-profile execution engine written in Go.
Current implementation scope:
- domain model and core interfaces
- YAML-backed prompt profile loading
- inline and file artifact reading
- provider-neutral prompt rendering
It loads a prompt profile, resolves named input artifacts, renders a prompt, calls an OpenAI-compatible LLM endpoint, validates output, and returns an artifact plus metadata.
## Local CLI usage
## Relationship to Narratio
Run a profile locally against an OpenAI-compatible endpoint:
In the broader workflow, Narratio handles pipeline orchestration (transcription, cleanup, storage, notifications). Scriptorium handles only prompt-profile execution for a single run.
## Repository Example Assets
- Profiles: `profiles/`
- Schemas: `schemas/`
- Tiny fixtures: `examples/fixtures/`
Included profiles:
- `generic.markdown_summary`
- `dnd.session_recap` (example content only; no D&D-specific Go logic)
- `generic.structured_events` (JSON + JSON Schema validation)
## Run a Local Profile (CLI)
```bash
go run ./cmd/scriptorium run \
--profile-dir ./profiles \
--profile-id recap \
--input transcript=./testdata/transcript.md \
--input glossary=./testdata/glossary.yml \
--var session_date=2026-05-04 \
--profile-id generic.markdown_summary \
--input transcript=./examples/fixtures/transcript.md \
--input glossary=./examples/fixtures/glossary.yml \
--llm-base-url http://localhost:8000/v1 \
--model gpt-4o-mini \
--out ./out.md
```
For schema-validated JSON output:
```bash
go run ./cmd/scriptorium run \
--profile-dir ./profiles \
--profile-id generic.structured_events \
--input transcript=./examples/fixtures/transcript.md \
--input glossary=./examples/fixtures/glossary.yml \
--llm-base-url http://localhost:8000/v1 \
--model gpt-4o-mini \
--schema-dir ./schemas \
--out ./events.json
```
## Start Local HTTP API
```bash
go run ./cmd/scriptorium serve \
--addr :8080 \
--profile-dir ./profiles \
--schema-dir ./schemas \
--llm-base-url http://localhost:8000/v1 \
--model gpt-4o-mini
```
## Call `POST /v1/runs`
```bash
curl -sS http://localhost:8080/v1/runs \
-H 'Content-Type: application/json' \
-d '{
"profile_id": "generic.structured_events",
"inputs": {
"transcript": {"type": "file", "uri": "./examples/fixtures/transcript.md"},
"glossary": {"type": "file", "uri": "./examples/fixtures/glossary.yml"}
},
"model": {"model": "gpt-4o-mini"}
}'
```
Response shape:
- `artifact`
- `validation`
- `metadata`
- `raw_model_output`
## Add a New Prompt Profile
1. Add a YAML file under `profiles/` with:
- `id`, `version`, `expected_inputs`, `templates`, `model_defaults`, `output_format`, `validation`
2. Use template helpers such as `{{input "transcript"}}`.
3. For structured JSON output, set:
- `output_format: json`
- `validation.validation_mode: json_schema`
- `validation.schema_path: <schema file>`
4. Place schema files in `schemas/` and pass `--schema-dir ./schemas` for CLI/serve.
## Validation Behavior
Validation modes currently implemented:
- `none`
- `basic` (non-empty output)
- `json` (must parse as JSON)
- `json_schema` (must parse JSON and satisfy schema)
Important behavior:
- Validation content failures are returned as structured run results (`validation.status = failed`) and preserve `raw_model_output`.
- Validation runtime/configuration failures are treated as run errors.

View File

@@ -0,0 +1,7 @@
party:
- Rin (ranger)
- Kara (cleric)
locations:
- Ruined Watchtower
factions:
- Goblin Raiders

View File

@@ -0,0 +1,7 @@
# Session Transcript (Excerpt)
DM: The party arrives at the ruined watchtower at dusk.
Rin: I search the courtyard for tracks.
DM: You find fresh boot prints leading below the tower.
Kara: We light a lantern and descend carefully.
DM: In the cellar, a frightened scout begs for help escaping goblin raiders.

View File

@@ -0,0 +1,99 @@
package usecase
import (
"context"
"path/filepath"
"testing"
"gitea.maximumdirect.net/eric/scriptorium/internal/artifact"
"gitea.maximumdirect.net/eric/scriptorium/internal/domain"
"gitea.maximumdirect.net/eric/scriptorium/internal/profile"
"gitea.maximumdirect.net/eric/scriptorium/internal/prompt"
"gitea.maximumdirect.net/eric/scriptorium/internal/validate"
)
type integrationLLM struct{}
func (f *integrationLLM) Generate(ctx context.Context, req domain.GenerateRequest) (*domain.GenerateResponse, error) {
return &domain.GenerateResponse{
Content: `{"summary":"Party discovered a captive scout beneath the tower.","events":[{"title":"Scout found in cellar","type":"discovery","notes":"Scout requested rescue from goblin raiders."}]}`,
Usage: domain.TokenUsage{
PromptTokens: 42,
CompletionTokens: 36,
TotalTokens: 78,
},
}, nil
}
func TestRunnerIntegrationWithProfilesFixturesAndValidation(t *testing.T) {
root, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("failed to resolve repo root: %v", err)
}
profilesDir := filepath.Join(root, "profiles")
schemasDir := filepath.Join(root, "schemas")
fixturesDir := filepath.Join(root, "examples", "fixtures")
runner := NewRunner(
profile.NewFilesystemRepository(profilesDir),
artifact.NewCompositeReader(),
prompt.NewGoRenderer(),
&integrationLLM{},
validate.NewStandardValidator(schemasDir),
)
res, err := runner.Run(context.Background(), domain.RunRequest{
ProfileID: "generic.structured_events",
Inputs: map[string]domain.ArtifactRef{
"transcript": {
Type: domain.ArtifactRefFile,
URI: filepath.Join(fixturesDir, "transcript.md"),
},
"glossary": {
Type: domain.ArtifactRefFile,
URI: filepath.Join(fixturesDir, "glossary.yml"),
},
},
})
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if res.ProfileID != "generic.structured_events" {
t.Fatalf("unexpected profile id: %q", res.ProfileID)
}
if res.ProfileVersion != "1.0.0" {
t.Fatalf("unexpected profile version: %q", res.ProfileVersion)
}
if res.Validation.Status != domain.ValidationPassed {
t.Fatalf("expected passed validation, got %q", res.Validation.Status)
}
if res.Validation.Mode != domain.ValidationJSONSchema {
t.Fatalf("expected json_schema mode, got %q", res.Validation.Mode)
}
if res.Artifact.ContentType != "application/json" {
t.Fatalf("expected application/json output, got %q", res.Artifact.ContentType)
}
if len(res.RawOutput) == 0 {
t.Fatal("expected raw output to be preserved")
}
if res.PromptHash == "" {
t.Fatal("expected non-empty prompt hash")
}
if len(res.InputHashes) != 2 {
t.Fatalf("expected two input hashes, got %d", len(res.InputHashes))
}
if res.InputHashes["transcript"] == "" || res.InputHashes["glossary"] == "" {
t.Fatalf("expected both input hashes to be set, got %#v", res.InputHashes)
}
if res.Usage.TotalTokens != 78 {
t.Fatalf("expected usage from fake llm, got %+v", res.Usage)
}
if res.StartTime.IsZero() || res.EndTime.IsZero() {
t.Fatal("expected start/end timestamps")
}
if res.EndTime.Before(res.StartTime) {
t.Fatalf("expected end >= start, got start=%v end=%v", res.StartTime, res.EndTime)
}
}

View File

@@ -0,0 +1,31 @@
id: dnd.session_recap
version: "1.0.0"
description: Example D&D session recap profile for demonstration only.
expected_inputs:
- transcript
- glossary
templates:
- role: system
content: |
You create concise tabletop RPG session recaps.
Stay factual and avoid inventing events.
- role: user
content: |
Produce a recap with these sections:
- Session Highlights
- Open Threads
- NPC Mentions
Transcript:
{{input "transcript"}}
Glossary:
{{input "glossary"}}
model_defaults:
endpoint: http://localhost:8000/v1
model: gpt-4o-mini
temperature: 0.3
max_tokens: 900
output_format: markdown
validation:
validation_mode: basic

View File

@@ -0,0 +1,28 @@
id: generic.markdown_summary
version: "1.0.0"
description: Generic markdown summary from transcript and glossary.
expected_inputs:
- transcript
- glossary
templates:
- role: system
content: |
You are a concise analysis assistant.
Write clear Markdown output.
- role: user
content: |
Summarize the following source transcript:
{{input "transcript"}}
Reference glossary:
{{input "glossary"}}
model_defaults:
endpoint: http://localhost:8000/v1
model: gpt-4o-mini
temperature: 0.2
max_tokens: 700
output_format: markdown
validation:
validation_mode: basic

View File

@@ -0,0 +1,31 @@
id: generic.structured_events
version: "1.0.0"
description: Produce structured event JSON from a transcript.
expected_inputs:
- transcript
- glossary
templates:
- role: system
content: |
Return only JSON following the requested schema.
Do not include commentary.
- role: user
content: |
Extract important events from the transcript and emit JSON.
Transcript:
{{input "transcript"}}
Glossary:
{{input "glossary"}}
model_defaults:
endpoint: http://localhost:8000/v1
model: gpt-4o-mini
temperature: 0.0
max_tokens: 500
output_format: json
validation:
format: json
validation_mode: json_schema
schema_path: structured_events.schema.json
repair_attempts: 0

View File

@@ -0,0 +1,27 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.local/structured_events.schema.json",
"type": "object",
"required": ["summary", "events"],
"properties": {
"summary": {
"type": "string",
"minLength": 1
},
"events": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["title", "type"],
"properties": {
"title": {"type": "string", "minLength": 1},
"type": {"type": "string", "enum": ["combat", "social", "travel", "discovery", "other"]},
"notes": {"type": "string"}
},
"additionalProperties": false
}
}
},
"additionalProperties": false
}