Files
notarius/internal/modules/extract/dnd/spells/extractor.go

150 lines
4.5 KiB
Go

package spells
import (
"context"
"encoding/json"
"fmt"
"strings"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets/dnd"
)
const Key = "dnd/spells"
const ArtifactType = "dnd.spell_cast"
const SchemaVersion = "v1"
var requiredCapabilities = []string{
"chunks",
"source.transcript",
}
var providedCapabilities = []string{
"dnd.spell_casts",
}
var referenceSlotDescriptions = dnd.ReferenceSlotDescriptions{
Glossary: "Optional campaign glossary reference material used only for disambiguation.",
Party: "Optional party roster reference material used only for disambiguation.",
Players: "Optional player list reference material used only for disambiguation.",
Roster: "Deprecated alias for party roster reference material used only for disambiguation.",
}
var _ contracts.Extractor = (*Extractor)(nil)
type Extractor struct{}
func New() *Extractor {
return &Extractor{}
}
func (e *Extractor) Key() string {
return Key
}
func (e *Extractor) ReferenceSlots() []contracts.ReferenceSlot {
return dnd.ReferenceSlots(referenceSlotDescriptions)
}
func (e *Extractor) ManifestMetadata() map[string]any {
promptSHA, err := scriptoriumPromptMetadata()
if err != nil {
promptSHA = ""
}
metadata := map[string]any{
"prompt_id": PromptID,
"prompt_version": SchemaVersion,
"prompt_sha256": promptSHA,
"response_schema_key": string(ResponseSchemaKey),
"response_schema_id": ResponseSchemaID,
"response_schema_name": ResponseSchemaName,
}
if schema, err := loadResponseSchema(); err == nil {
metadata["response_schema_version"] = schema.Version
metadata["response_schema_sha256"] = schema.SHA256
}
return metadata
}
func (e *Extractor) Extract(ctx context.Context, req contracts.ExtractionRequest) (contracts.ExtractionResult, error) {
if e == nil {
return contracts.ExtractionResult{}, extractorErrorf("extractor must not be nil")
}
if ctx == nil {
return contracts.ExtractionResult{}, extractorErrorf("context must not be nil")
}
if err := ctx.Err(); err != nil {
return contracts.ExtractionResult{}, extractorErrorf("context error before extraction: %w", err)
}
if req.Source == nil {
return contracts.ExtractionResult{}, extractorErrorf("source must not be nil")
}
if req.Chunk == nil {
return contracts.ExtractionResult{}, extractorErrorf("chunk must not be nil")
}
if len(req.Chunk.Units) == 0 {
return contracts.ExtractionResult{}, extractorErrorf("chunk %q units must not be empty", req.Chunk.ID)
}
if req.LLMClient == nil {
return contracts.ExtractionResult{}, extractorErrorf("LLM client must not be nil")
}
var response extractionResponse
completion, err := req.LLMClient.CompleteStructured(ctx, contracts.StructuredCompletionRequest{
StageName: Key,
PromptID: PromptID,
PromptVersion: SchemaVersion,
ProfileID: req.LLMProfile,
SessionID: req.SessionID,
Inputs: dnd.PromptInputs(req.SourceInput, req.References),
}, &response)
if err != nil {
return contracts.ExtractionResult{}, extractorErrorf("complete structured output: %w", err)
}
content := append([]byte(nil), completion.Content...)
if len(strings.TrimSpace(string(content))) == 0 {
var err error
content, err = json.Marshal(response)
if err != nil {
return contracts.ExtractionResult{}, extractorErrorf("marshal raw output: %w", err)
}
}
return contracts.ExtractionResult{
Output: contracts.ExtractOutput{
Schema: contracts.ResponseSchema{
ID: ResponseSchemaID,
Name: ResponseSchemaName,
Version: SchemaVersion,
},
Payload: contracts.RawPayload{
Content: content,
MediaType: "application/json",
Metadata: map[string]any{
"spell_cast_count": len(response.SpellCasts),
},
},
},
}, nil
}
func ModuleSpec() pipeline.ModuleSpec {
return pipeline.ModuleSpec{
Key: Key,
Stage: pipeline.StageExtract,
Requires: append([]string(nil), requiredCapabilities...),
Provides: append([]string(nil), providedCapabilities...),
ReferenceSlots: dnd.ReferenceSlots(referenceSlotDescriptions),
}
}
func Register(registry *pipeline.ExtractorRegistry) error {
return registry.RegisterWithSpec(ModuleSpec(), func() (contracts.Extractor, error) {
return New(), nil
})
}
func extractorErrorf(format string, args ...any) error {
return fmt.Errorf("dnd spells extractor: "+format, args...)
}