Migrate NPC normalization to shared reconciliation

This commit is contained in:
2026-08-03 23:56:48 +00:00
parent c3513da880
commit c51934d5c6
21 changed files with 204 additions and 767 deletions

View File

@@ -17,12 +17,14 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/entityreconcile"
)
const (
Key = "dnd/npcs"
PromptID = "dnd.npcs.normalize"
normalizationPolicy = "dnd.npcs.normalize.v3"
semanticContextPolicy = "dnd.npcs.semantic_context.v1"
semanticContextPolicy = "dnd.entity_reconcile.context.v1"
NormalizationPolicy = normalizationPolicy
ReasonCodeNPCFieldsNormalized = "npc_fields_normalized"
@@ -57,7 +59,7 @@ func New(llmClient contracts.StructuredLLMClient, _ Options) (*Normalizer, error
if err != nil {
return nil, normalizerErrorf("load prompt metadata: %w", err)
}
responseSchema, err := loadResponseSchema()
responseSchema, err := entityreconcile.LoadResponseSchema()
if err != nil {
return nil, normalizerErrorf("load response schema: %w", err)
}
@@ -73,12 +75,12 @@ func (n *Normalizer) ManifestMetadata() map[string]any {
}
return map[string]any{
"prompt_id": PromptID,
"prompt_version": SchemaVersion,
"prompt_version": entityreconcile.SchemaVersion,
"prompt_sha256": n.promptSHA,
"response_schema_key": string(ResponseSchemaKey),
"response_schema_id": ResponseSchemaID,
"response_schema_name": ResponseSchemaName,
"response_schema_version": SchemaVersion,
"response_schema_key": string(entityreconcile.ResponseSchemaKey),
"response_schema_id": entityreconcile.ResponseSchemaID,
"response_schema_name": entityreconcile.ResponseSchemaName,
"response_schema_version": entityreconcile.SchemaVersion,
"response_schema_sha256": n.responseSchemaSHA,
"identity_policy": identity.Policy,
"normalization_policy": normalizationPolicy,
@@ -117,7 +119,7 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
order := shared.NewSourceRefOrder(req.Source)
records, warnings := preprocessRecords(req.MergeOutput.Value, order)
deterministic := recordList(records)
materials, ready, err := buildDefaultNormalizeContextMaterials(req.Source, recordValues(records))
materials, ready, err := entityreconcile.BuildContext(req.Source, reconciliationCandidates(records), semanticContextRadius)
if err != nil {
return contracts.TypedNormalizeResult[dnd.NPCList]{}, normalizerErrorf("build semantic context: %w", err)
}
@@ -125,9 +127,9 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
return contracts.TypedNormalizeResult[dnd.NPCList]{Value: deterministic, Warnings: limitWarnings(warnings)}, nil
}
var response normalizeProposalResponse
var response entityreconcile.ProposalResponse
if _, err := n.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{
StageName: Key, PromptID: PromptID, PromptVersion: SchemaVersion,
StageName: Key, PromptID: PromptID, PromptVersion: entityreconcile.SchemaVersion,
ProfileID: req.LLMProfile, SessionID: req.SessionID,
Inputs: contracts.LLMInputSet{"candidates": materials.Candidates, "transcript": materials.Transcript},
}, &response); err != nil {
@@ -137,10 +139,10 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
return contracts.TypedNormalizeResult[dnd.NPCList]{}, normalizerErrorf("complete structured output: %w", err)
}
assessment := assessProposal(response, records, materials.candidatePositions)
applied, semanticWarnings := applySafeGroups(records, assessment.safeGroups, order)
assessment := materials.Assess(response)
applied, semanticWarnings := applySafeGroups(records, reconciliationGroups(assessment, materials.CandidateKeys()), order)
warnings = append(warnings, semanticWarnings...)
if assessment.discardedGroups == 0 {
if assessment.DiscardedGroups() == 0 {
return contracts.TypedNormalizeResult[dnd.NPCList]{Value: recordList(applied), Warnings: limitWarnings(warnings)}, nil
}
return retryResult(recordList(applied), warnings, assessment), nil
@@ -158,14 +160,14 @@ func (n *Normalizer) invalidStructuredResult(value dnd.NPCList, warnings []contr
}
}
func retryResult(value dnd.NPCList, warnings []contracts.Warning, assessment proposalAssessment) contracts.TypedNormalizeResult[dnd.NPCList] {
func retryResult(value dnd.NPCList, warnings []contracts.Warning, assessment entityreconcile.Assessment) contracts.TypedNormalizeResult[dnd.NPCList] {
return contracts.TypedNormalizeResult[dnd.NPCList]{
Value: value,
Warnings: limitWarningsForRetry(warnings),
Retry: &contracts.NormalizeRetry{
ReasonCode: ReasonCodeNPCSemanticProposalInvalid,
Message: diagnostics.Aggregate("semantic proposal requires retry", assessment.issues),
FallbackWarnings: []contracts.Warning{semanticFallbackWarning(assessment.discardedGroups)},
Message: diagnostics.Aggregate("semantic proposal requires retry", reconciliationIssues(assessment)),
FallbackWarnings: []contracts.Warning{semanticFallbackWarning(assessment.DiscardedGroups())},
},
}
}