Centralize shared D&D LLM assets

This commit is contained in:
2026-08-05 00:37:19 +00:00
parent a57f83e30d
commit 08954f17e2
15 changed files with 69 additions and 34 deletions

View File

@@ -1,5 +0,0 @@
id: dnd-extraction
backend: openrouter
model: openai/gpt-5.6-luna
timeout_seconds: 240
service_tier: flex

View File

@@ -1,14 +1,17 @@
package register
import (
"embed"
"fmt"
"io/fs"
rootassets "gitea.maximumdirect.net/eric/notarius/assets"
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
)
//go:embed assets/profiles/*.yaml
var embeddedProfileAssets embed.FS
func registerFallbackProfiles(assets *llm.AssetRegistry) error {
return assets.RegisterFallbackProfileFS(embeddedProfileAssets, "assets/profiles")
profiles, err := fs.Sub(rootassets.FS(), "dnd/profiles")
if err != nil {
return fmt.Errorf("scope D&D fallback profiles: %w", err)
}
return assets.RegisterFallbackProfileFS(profiles, ".")
}

View File

@@ -1,18 +1,15 @@
package shared
import (
"embed"
"fmt"
"io/fs"
"strings"
rootassets "gitea.maximumdirect.net/eric/notarius/assets"
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
"gitea.maximumdirect.net/eric/notarius/internal/framework/promptfs"
)
//go:embed assets/prompts/*.md
var embeddedAssets embed.FS
// PromptAssetManifest is the ordered set of assets that make up one prompt.
// Module files are addressed in the owning module filesystem; shared files use
// the names in sharedPromptPaths and are mounted beneath sharedassets.
@@ -23,13 +20,21 @@ type PromptAssetManifest struct {
}
var sharedPromptPaths = map[string]string{
"common-dnd-system.md": "assets/prompts/common-dnd-system.md",
"common-dnd-extraction-evidence.md": "assets/prompts/common-dnd-extraction-evidence.md",
"common-dnd-identity.md": "assets/prompts/common-dnd-identity.md",
"common-dnd-transcript.md": "assets/prompts/common-dnd-transcript.md",
"common-dnd-references.md": "assets/prompts/common-dnd-references.md",
"common-dnd-npcs.md": "assets/prompts/common-dnd-npcs.md",
"common-dnd-entity-reconciliation.md": "assets/prompts/common-dnd-entity-reconciliation.md",
"common-dnd-system.md": "prompts/common-dnd-system.md",
"common-dnd-extraction-evidence.md": "prompts/common-dnd-extraction-evidence.md",
"common-dnd-identity.md": "prompts/common-dnd-identity.md",
"common-dnd-transcript.md": "prompts/common-dnd-transcript.md",
"common-dnd-references.md": "prompts/common-dnd-references.md",
"common-dnd-npcs.md": "prompts/common-dnd-npcs.md",
"common-dnd-entity-reconciliation.md": "prompts/common-dnd-entity-reconciliation.md",
}
func sharedAssetFS() (fs.FS, error) {
assets, err := fs.Sub(rootassets.FS(), "dnd/shared")
if err != nil {
return nil, fmt.Errorf("scope shared D&D prompt assets: %w", err)
}
return assets, nil
}
func (manifest PromptAssetManifest) PromptFS(moduleFS fs.FS) (fs.FS, error) {
@@ -57,6 +62,11 @@ func (manifest PromptAssetManifest) Hash(moduleFS fs.FS) (string, error) {
}
func resolveSharedPromptFiles(names []string) ([]promptfs.SharedPromptFile, error) {
assets, err := sharedAssetFS()
if err != nil {
return nil, err
}
files := make([]promptfs.SharedPromptFile, 0, len(names))
seen := make(map[string]struct{}, len(names))
for _, name := range names {
@@ -76,7 +86,7 @@ func resolveSharedPromptFiles(names []string) ([]promptfs.SharedPromptFile, erro
seen[name] = struct{}{}
files = append(files, promptfs.SharedPromptFile{
Name: name,
FS: embeddedAssets,
FS: assets,
Path: path,
})
}

View File

@@ -1,6 +0,0 @@
Identify only well-supported duplicate groups among the supplied candidates.
Candidate keys are opaque identifiers. Copy each selected key exactly. A group
must contain at least two supplied keys, and its `canonical` key must be one of
its members. Do not create keys, records, names, source references, evidence,
or replacement values. Omit any uncertain or unsafe group.

View File

@@ -1,14 +0,0 @@
Transcript units are the only evidence for extracted events and factual claims.
Campaign and registry references may disambiguate names, aliases, speakers, or
other identities, but they do not establish events, participation, effects, or
source evidence.
Every reported factual claim must be supported by cited transcript units. Use
integer `start_unit_id` and `end_unit_id` values from the transcript. Omit
`source_id`; Notarius assigns the current source identity.
When supporting evidence is non-contiguous, use multiple narrow ranges rather
than a broad range that bridges unrelated conversation.
Return exactly one JSON object and no explanatory text. Output only the
configured JSON object and fields defined by its response schema.

View File

@@ -1,7 +0,0 @@
Use the most specific supported in-world character or creature identity for
each actor or participant. Do not identify a human player, transcript speaker,
or the GM as an out-of-world person when an in-world identity is supported.
Player, party, glossary, campaign, and NPC registry references may disambiguate
an identity, but a reference alone cannot establish that the identity
participated in the transcript.

View File

@@ -1,10 +0,0 @@
A normalized Dungeons & Dragons NPC registry is provided below as grounding
material. It may be empty. Use it only to prefer exact canonical participant
names when the transcript identifies a participant.
Registry content is context, not event evidence. Do not extract events,
participants, effects, or source references from the registry. Registry source
references describe registry provenance and may belong to another session; they
are never evidence for the current transcript.
{{ input "npcs" }}

View File

@@ -1,12 +0,0 @@
Optional reference material for this Dungeons & Dragons campaign is provided
below. Use it only to disambiguate names, aliases, speakers, campaign terms, or
spell names already present in the transcript.
Player list reference:
{{ input "players" }}
Party roster reference:
{{ input "party" }}
Glossary reference:
{{ input "glossary" }}

View File

@@ -1,6 +0,0 @@
You work with Dungeons & Dragons gameplay transcripts.
Use only the provided transcript and reference material. Source text may contain
transcription errors, repeated lines, incomplete sentences, and misheard proper
nouns. Reference material, when present, is supporting context only and must not
be treated as a source of extracted events by itself.

View File

@@ -1,3 +0,0 @@
A transcript of a Dungeons & Dragons gameplay session is provided below.
{{ input "transcript" }}

View File

@@ -111,7 +111,7 @@ func TestPromptAssetManifestRejectsMissingModuleFile(t *testing.T) {
func TestPromptAssetManifestRejectsMissingSharedFile(t *testing.T) {
const name = "missing-for-test.md"
const path = "assets/prompts/missing-for-test.md"
const path = "prompts/missing-for-test.md"
previous, existed := sharedPromptPaths[name]
sharedPromptPaths[name] = path
t.Cleanup(func() {
@@ -157,11 +157,15 @@ func TestPromptAssetManifestHashMatchesManifestParts(t *testing.T) {
if err != nil {
t.Fatalf("Hash() error = %v, want nil", err)
}
sharedFS, err := sharedAssetFS()
if err != nil {
t.Fatalf("sharedAssetFS() error = %v, want nil", err)
}
want, err := llm.HashAssets([]llm.AssetHashPart{
{FS: moduleFS, Path: "assets/prompts/dnd.test.yaml"},
{FS: moduleFS, Path: "assets/prompts/task.md"},
{FS: embeddedAssets, Path: "assets/prompts/common-dnd-transcript.md"},
{FS: embeddedAssets, Path: "assets/prompts/common-dnd-system.md"},
{FS: sharedFS, Path: "prompts/common-dnd-transcript.md"},
{FS: sharedFS, Path: "prompts/common-dnd-system.md"},
})
if err != nil {
t.Fatalf("HashAssets() error = %v, want nil", err)
@@ -172,9 +176,9 @@ func TestPromptAssetManifestHashMatchesManifestParts(t *testing.T) {
withUnused, err := llm.HashAssets([]llm.AssetHashPart{
{FS: moduleFS, Path: "assets/prompts/dnd.test.yaml"},
{FS: moduleFS, Path: "assets/prompts/task.md"},
{FS: embeddedAssets, Path: "assets/prompts/common-dnd-transcript.md"},
{FS: embeddedAssets, Path: "assets/prompts/common-dnd-system.md"},
{FS: embeddedAssets, Path: "assets/prompts/common-dnd-npcs.md"},
{FS: sharedFS, Path: "prompts/common-dnd-transcript.md"},
{FS: sharedFS, Path: "prompts/common-dnd-system.md"},
{FS: sharedFS, Path: "prompts/common-dnd-npcs.md"},
})
if err != nil {
t.Fatalf("HashAssets() with unused asset error = %v, want nil", err)
@@ -195,7 +199,7 @@ func TestSharedPromptDescriptorsReturnFreshCopies(t *testing.T) {
}
first[0].Name = "changed.md"
first[0].Path = "changed.md"
if reflect.DeepEqual(first, second) || second[0].Name != "common-dnd-system.md" || second[0].Path != "assets/prompts/common-dnd-system.md" {
if reflect.DeepEqual(first, second) || second[0].Name != "common-dnd-system.md" || second[0].Path != "prompts/common-dnd-system.md" {
t.Fatalf("resolveSharedPromptFiles() reused descriptor state: first=%#v second=%#v", first, second)
}
}

View File

@@ -1,6 +0,0 @@
package entityreconcile
import "embed"
//go:embed assets/schemas/dnd_entity_reconcile_llm.v1.json
var embeddedAssets embed.FS

View File

@@ -1,24 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "notarius.dnd.entity_reconcile.llm",
"type": "object",
"additionalProperties": false,
"required": ["duplicate_groups"],
"properties": {
"duplicate_groups": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["members", "canonical"],
"properties": {
"members": {
"type": "array",
"items": {"type": "string"}
},
"canonical": {"type": "string"}
}
}
}
}
}

View File

@@ -1,6 +1,10 @@
package entityreconcile
import (
"fmt"
"io/fs"
rootassets "gitea.maximumdirect.net/eric/notarius/assets"
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
)
@@ -9,13 +13,25 @@ const (
ResponseSchemaID = "notarius.dnd.entity_reconcile.llm"
ResponseSchemaName = "notarius_dnd_entity_reconcile_llm_v1"
SchemaVersion = "v1"
SchemaAssetPath = "assets/schemas/dnd_entity_reconcile_llm.v1.json"
SchemaAssetPath = "schemas/dnd_entity_reconcile_llm.v1.json"
)
func schemaAssetFS() (fs.FS, error) {
assets, err := fs.Sub(rootassets.FS(), "dnd/entity-reconciliation")
if err != nil {
return nil, fmt.Errorf("scope entity reconciliation assets: %w", err)
}
return assets, nil
}
// LoadResponseSchema returns the shared private duplicate-group response
// contract. It is intentionally separate from durable artifact schemas.
func LoadResponseSchema() (llm.ResponseSchema, error) {
return llm.LoadResponseSchema(embeddedAssets, llm.ResponseSchemaDefinition{
assets, err := schemaAssetFS()
if err != nil {
return llm.ResponseSchema{}, err
}
return llm.LoadResponseSchema(assets, llm.ResponseSchemaDefinition{
Key: ResponseSchemaKey,
ID: ResponseSchemaID,
Version: SchemaVersion,
@@ -27,5 +43,13 @@ func LoadResponseSchema() (llm.ResponseSchema, error) {
// RegisterSchemaAssets makes the shared private response schema available to
// prompt preparation. A family registrar can register it once for all consumers.
func RegisterSchemaAssets(registry *llm.AssetRegistry) error {
return registry.RegisterSchemaFS(embeddedAssets, "assets/schemas")
assets, err := schemaAssetFS()
if err != nil {
return err
}
schemas, err := fs.Sub(assets, "schemas")
if err != nil {
return fmt.Errorf("scope entity reconciliation schemas: %w", err)
}
return registry.RegisterSchemaFS(schemas, ".")
}