Update D&D schemas to require integer unit_id values
This commit is contained in:
@@ -22,7 +22,8 @@ dnd/scenes boundary policy:
|
||||
- return sequential scenes with no gaps;
|
||||
- do not overlap scenes;
|
||||
- preserve source-unit order;
|
||||
- use exact source-unit IDs from the transcript;
|
||||
- use 1-based integer source-unit numbers from the transcript, where 1 is the
|
||||
first provided source unit;
|
||||
- each scene must have start_unit_id and end_unit_id;
|
||||
- do not include final chunk IDs or chunk indexes.
|
||||
|
||||
|
||||
@@ -26,12 +26,12 @@
|
||||
],
|
||||
"properties": {
|
||||
"start_unit_id": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"end_unit_id": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"short_title": {
|
||||
"type": "string",
|
||||
|
||||
@@ -150,7 +150,7 @@ func chunksFromResponse(doc *source.SourceDocument, response chunkResponse) ([]c
|
||||
chunks := make([]contracts.SourceChunk, 0, len(response.Scenes))
|
||||
previousEnd := -1
|
||||
for i, scene := range response.Scenes {
|
||||
normalized, err := normalizeScene(i, scene)
|
||||
normalized, err := normalizeScene(doc, i, scene)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -206,10 +206,19 @@ func chunksFromResponse(doc *source.SourceDocument, response chunkResponse) ([]c
|
||||
return chunks, nil
|
||||
}
|
||||
|
||||
func normalizeScene(index int, scene sceneResponse) (sceneResponse, error) {
|
||||
out := sceneResponse{
|
||||
StartUnitID: strings.TrimSpace(scene.StartUnitID),
|
||||
EndUnitID: strings.TrimSpace(scene.EndUnitID),
|
||||
func normalizeScene(doc *source.SourceDocument, index int, scene sceneResponse) (normalizedScene, error) {
|
||||
startUnitID, err := dnd.ResolveUnitID(doc, "start_unit_id", scene.StartUnitID)
|
||||
if err != nil {
|
||||
return normalizedScene{}, fmt.Errorf("scene[%d] %w", index, err)
|
||||
}
|
||||
endUnitID, err := dnd.ResolveUnitID(doc, "end_unit_id", scene.EndUnitID)
|
||||
if err != nil {
|
||||
return normalizedScene{}, fmt.Errorf("scene[%d] %w", index, err)
|
||||
}
|
||||
|
||||
out := normalizedScene{
|
||||
StartUnitID: startUnitID,
|
||||
EndUnitID: endUnitID,
|
||||
ShortTitle: strings.TrimSpace(scene.ShortTitle),
|
||||
PrimaryMode: strings.TrimSpace(scene.PrimaryMode),
|
||||
Summary: strings.TrimSpace(scene.Summary),
|
||||
@@ -228,23 +237,23 @@ func normalizeScene(index int, scene sceneResponse) (sceneResponse, error) {
|
||||
}
|
||||
for field, value := range required {
|
||||
if value == "" {
|
||||
return sceneResponse{}, fmt.Errorf("scene[%d] %s must not be empty", index, field)
|
||||
return normalizedScene{}, fmt.Errorf("scene[%d] %s must not be empty", index, field)
|
||||
}
|
||||
}
|
||||
if !validPrimaryMode(out.PrimaryMode) {
|
||||
return sceneResponse{}, fmt.Errorf("scene[%d] primary_mode %q is not supported", index, out.PrimaryMode)
|
||||
return normalizedScene{}, fmt.Errorf("scene[%d] primary_mode %q is not supported", index, out.PrimaryMode)
|
||||
}
|
||||
if !validBoundaryConfidence(out.BoundaryConfidence) {
|
||||
return sceneResponse{}, fmt.Errorf("scene[%d] boundary_confidence %q is not supported", index, out.BoundaryConfidence)
|
||||
return normalizedScene{}, fmt.Errorf("scene[%d] boundary_confidence %q is not supported", index, out.BoundaryConfidence)
|
||||
}
|
||||
if len(scene.MainParticipants) == 0 {
|
||||
return sceneResponse{}, fmt.Errorf("scene[%d] main_participants must not be empty", index)
|
||||
return normalizedScene{}, fmt.Errorf("scene[%d] main_participants must not be empty", index)
|
||||
}
|
||||
out.MainParticipants = make([]string, 0, len(scene.MainParticipants))
|
||||
for participantIndex, participant := range scene.MainParticipants {
|
||||
trimmed := strings.TrimSpace(participant)
|
||||
if trimmed == "" {
|
||||
return sceneResponse{}, fmt.Errorf("scene[%d] main_participants[%d] must not be empty", index, participantIndex)
|
||||
return normalizedScene{}, fmt.Errorf("scene[%d] main_participants[%d] must not be empty", index, participantIndex)
|
||||
}
|
||||
out.MainParticipants = append(out.MainParticipants, trimmed)
|
||||
}
|
||||
|
||||
@@ -105,8 +105,8 @@ func TestChunkReturnsSceneChunksFromStructuredOutput(t *testing.T) {
|
||||
response: chunkResponse{
|
||||
Scenes: []sceneResponse{
|
||||
{
|
||||
StartUnitID: "seg-001",
|
||||
EndUnitID: "seg-002",
|
||||
StartUnitID: dnd.UnitRefFromInt(1),
|
||||
EndUnitID: dnd.UnitRefFromInt(2),
|
||||
ShortTitle: " Goblin parley ",
|
||||
PrimaryMode: "Discussion",
|
||||
MainParticipants: []string{" Aria ", "Goblin scout"},
|
||||
@@ -115,8 +115,8 @@ func TestChunkReturnsSceneChunksFromStructuredOutput(t *testing.T) {
|
||||
BoundaryConfidence: "High",
|
||||
},
|
||||
{
|
||||
StartUnitID: "seg-003",
|
||||
EndUnitID: "seg-004",
|
||||
StartUnitID: dnd.UnitRefFromInt(3),
|
||||
EndUnitID: dnd.UnitRefFromInt(4),
|
||||
ShortTitle: "Ambush at the gate",
|
||||
PrimaryMode: "Combat",
|
||||
MainParticipants: []string{"Aria", "Goblin ambushers"},
|
||||
@@ -204,8 +204,8 @@ func TestChunkPassesReferencesAsPromptInputs(t *testing.T) {
|
||||
client := &fakeScenesLLMClient{response: chunkResponse{
|
||||
Scenes: []sceneResponse{
|
||||
{
|
||||
StartUnitID: "seg-001",
|
||||
EndUnitID: "seg-004",
|
||||
StartUnitID: dnd.UnitRefFromInt(1),
|
||||
EndUnitID: dnd.UnitRefFromInt(4),
|
||||
ShortTitle: "Ambush",
|
||||
PrimaryMode: "Combat",
|
||||
MainParticipants: []string{"Aria"},
|
||||
@@ -445,8 +445,8 @@ func TestChunkRejectsMalformedStructuredOutput(t *testing.T) {
|
||||
name: "empty metadata field",
|
||||
response: replaceScenes(validSceneResponse(), []sceneResponse{
|
||||
{
|
||||
StartUnitID: "seg-001",
|
||||
EndUnitID: "seg-004",
|
||||
StartUnitID: dnd.UnitRefFromString("seg-001"),
|
||||
EndUnitID: dnd.UnitRefFromString("seg-004"),
|
||||
ShortTitle: " ",
|
||||
PrimaryMode: "Narrative",
|
||||
MainParticipants: []string{"Aria"},
|
||||
@@ -461,8 +461,8 @@ func TestChunkRejectsMalformedStructuredOutput(t *testing.T) {
|
||||
name: "empty participant",
|
||||
response: replaceScenes(validSceneResponse(), []sceneResponse{
|
||||
{
|
||||
StartUnitID: "seg-001",
|
||||
EndUnitID: "seg-004",
|
||||
StartUnitID: dnd.UnitRefFromString("seg-001"),
|
||||
EndUnitID: dnd.UnitRefFromString("seg-004"),
|
||||
ShortTitle: "Title",
|
||||
PrimaryMode: "Narrative",
|
||||
MainParticipants: []string{"Aria", " "},
|
||||
@@ -553,8 +553,8 @@ func replaceScenes(response chunkResponse, scenes []sceneResponse) chunkResponse
|
||||
|
||||
func scene(startUnitID string, endUnitID string) sceneResponse {
|
||||
return sceneResponse{
|
||||
StartUnitID: startUnitID,
|
||||
EndUnitID: endUnitID,
|
||||
StartUnitID: dnd.UnitRefFromString(startUnitID),
|
||||
EndUnitID: dnd.UnitRefFromString(endUnitID),
|
||||
ShortTitle: "Scene title",
|
||||
PrimaryMode: "Narrative",
|
||||
MainParticipants: []string{"Aria"},
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
package scenes
|
||||
|
||||
import "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets/dnd"
|
||||
|
||||
type chunkResponse struct {
|
||||
Scenes []sceneResponse `json:"scenes"`
|
||||
BoundaryCaveats []string `json:"boundary_caveats"`
|
||||
}
|
||||
|
||||
type sceneResponse struct {
|
||||
StartUnitID string `json:"start_unit_id"`
|
||||
EndUnitID string `json:"end_unit_id"`
|
||||
ShortTitle string `json:"short_title"`
|
||||
PrimaryMode string `json:"primary_mode"`
|
||||
MainParticipants []string `json:"main_participants"`
|
||||
Summary string `json:"summary"`
|
||||
BoundaryNote string `json:"boundary_note"`
|
||||
BoundaryConfidence string `json:"boundary_confidence"`
|
||||
StartUnitID dnd.UnitRef `json:"start_unit_id"`
|
||||
EndUnitID dnd.UnitRef `json:"end_unit_id"`
|
||||
ShortTitle string `json:"short_title"`
|
||||
PrimaryMode string `json:"primary_mode"`
|
||||
MainParticipants []string `json:"main_participants"`
|
||||
Summary string `json:"summary"`
|
||||
BoundaryNote string `json:"boundary_note"`
|
||||
BoundaryConfidence string `json:"boundary_confidence"`
|
||||
}
|
||||
|
||||
type normalizedScene struct {
|
||||
StartUnitID string
|
||||
EndUnitID string
|
||||
ShortTitle string
|
||||
PrimaryMode string
|
||||
MainParticipants []string
|
||||
Summary string
|
||||
BoundaryNote string
|
||||
BoundaryConfidence string
|
||||
}
|
||||
|
||||
@@ -64,8 +64,11 @@ func TestResponseSchemaShapeUsesSourceUnitBoundaries(t *testing.T) {
|
||||
}
|
||||
for _, field := range []string{"start_unit_id", "end_unit_id"} {
|
||||
property := sceneProperties[field].(map[string]any)
|
||||
if property["type"] != "string" {
|
||||
t.Fatalf("%s type = %#v, want string", field, property["type"])
|
||||
if property["type"] != "integer" {
|
||||
t.Fatalf("%s type = %#v, want integer", field, property["type"])
|
||||
}
|
||||
if property["minimum"] != float64(1) {
|
||||
t.Fatalf("%s minimum = %#v, want 1", field, property["minimum"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +90,7 @@ func TestResponseSchemaShapeUsesSourceUnitBoundaries(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseStructRejectsIntegerBoundaries(t *testing.T) {
|
||||
func TestResponseStructAcceptsIntegerBoundaries(t *testing.T) {
|
||||
raw := []byte(`{
|
||||
"scenes": [
|
||||
{
|
||||
@@ -105,12 +108,14 @@ func TestResponseStructRejectsIntegerBoundaries(t *testing.T) {
|
||||
}`)
|
||||
|
||||
var response chunkResponse
|
||||
err := json.Unmarshal(raw, &response)
|
||||
if err == nil {
|
||||
t.Fatalf("Unmarshal() error = nil, want integer boundary type error: %#v", response)
|
||||
if err := json.Unmarshal(raw, &response); err != nil {
|
||||
t.Fatalf("Unmarshal() error = %v, want nil", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "string") {
|
||||
t.Fatalf("Unmarshal() error = %v, want string type error", err)
|
||||
if got := response.Scenes[0].StartUnitID.String(); got != "1" {
|
||||
t.Fatalf("StartUnitID = %q, want 1", got)
|
||||
}
|
||||
if got := response.Scenes[0].EndUnitID.String(); got != "3" {
|
||||
t.Fatalf("EndUnitID = %q, want 3", got)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user