175 lines
5.2 KiB
Go
175 lines
5.2 KiB
Go
package spells
|
|
|
|
import (
|
|
"bytes"
|
|
"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")
|
|
}
|
|
sourceInput, err := chunkSourceInput(req)
|
|
if err != nil {
|
|
return contracts.ExtractionResult{}, err
|
|
}
|
|
|
|
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(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 chunkSourceInput(req contracts.ExtractionRequest) (contracts.LLMInputMaterial, error) {
|
|
material := req.SourceInput.Clone()
|
|
if len(material.Content) == 0 {
|
|
material = contracts.NewLLMInputMaterial("source", req.Chunk.MediaType, req.Chunk.Content, "", "")
|
|
}
|
|
if !bytes.Equal(material.Content, req.Chunk.Content) {
|
|
return contracts.LLMInputMaterial{}, extractorErrorf("source input must match chunk %q content", req.Chunk.ID)
|
|
}
|
|
if material.Name == "" {
|
|
material.Name = "source"
|
|
}
|
|
if material.MediaType == "" {
|
|
material.MediaType = req.Chunk.MediaType
|
|
}
|
|
if material.SizeBytes == 0 {
|
|
material.SizeBytes = int64(len(material.Content))
|
|
}
|
|
return material, 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...)
|
|
}
|