Ctrl K
ComunidadGuías…

internal/memory/extraction.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package memory

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"strings"
	"time"

	"github.com/yeomyeonggeori/blueclaw/internal/policy"
	"github.com/yeomyeonggeori/bluememo"
	"github.com/yeomyeonggeori/blueprotocol/agentcontract"
	"github.com/yeomyeonggeori/blueprotocol/taskstate"
)

const (
	ExtractionContextEventName = "memory.extraction_context"
	extractionTimeout          = 2 * time.Minute
)

type ExtractionContext struct {
	RequesterName string `json:"requesterName,omitempty"`
	Platform      string `json:"platform,omitempty"`
}

type TaskRunReader interface {
	FindTaskRun(taskRunID string) (agentcontract.TaskRun, bool)
	ListTaskEvent(taskRunID string) []agentcontract.TaskEvent
	AppendTaskEvent(taskRunID string, name string, body string)
}

type TaskStepReader interface {
	ListTaskStep(taskRunID string) []taskstate.TaskStep
}

type PersonAccessResolver interface {
	ResolvePersonAccess(personID string) policy.PersonAccess
	ContainedCircles() map[string][]string
}

// Transcript is what a finished task run reads like. The memory store takes a
// note of what happened, so the shape of a task run is the host's to render.
type Transcript struct {
	Prompt        string
	Result        string
	Outcome       string
	FailureReason string
	Steps         []TranscriptStep
}

type TranscriptStep struct {
	Instruction string
	Status      string
	Output      string
}

func TaskTranscript(taskRun agentcontract.TaskRun, steps []taskstate.TaskStep) Transcript {
	transcript := Transcript{
		Prompt:        taskRun.Prompt,
		Result:        taskRun.Result,
		Outcome:       string(taskRun.Status),
		FailureReason: taskRun.FailureReason,
	}
	for _, step := range steps {
		transcript.Steps = append(transcript.Steps, TranscriptStep{Instruction: step.Instruction, Status: string(step.Status), Output: step.Output})
	}
	return transcript
}

func RenderTranscript(transcript Transcript) string {
	lines := []string{}
	appendLine := func(label string, value string) {
		if strings.TrimSpace(value) != "" {
			lines = append(lines, label+": "+strings.TrimSpace(value))
		}
	}
	appendLine("Asked", transcript.Prompt)
	for _, step := range transcript.Steps {
		appendLine("Did", step.Instruction)
		appendLine("Which gave", step.Output)
	}
	appendLine("Answered", transcript.Result)
	appendLine("Ended", transcript.Outcome)
	appendLine("Because", transcript.FailureReason)
	return strings.Join(lines, "\n")
}

// TaskRunTransitionObserver takes a note of a task run that has just ended.
// Settling it asks a model, so the note is taken off the transition rather
// than on it.
type TaskRunTransitionObserver struct {
	Stores   *Stores
	TaskRuns TaskRunReader
	Steps    TaskStepReader
	Access   PersonAccessResolver
	Logger   *slog.Logger
}

func (observer TaskRunTransitionObserver) Observe(taskRun agentcontract.TaskRun) {
	switch taskRun.Status {
	case agentcontract.TaskStatusCompleted, agentcontract.TaskStatusFailed, agentcontract.TaskStatusCancelled:
	default:
		return
	}
	if observer.Stores == nil || observer.TaskRuns == nil {
		return
	}
	if strings.TrimSpace(taskRun.RequesterPersonID) == "" || strings.TrimSpace(taskRun.Prompt) == "" {
		return
	}
	go observer.remember(taskRun)
}

func (observer TaskRunTransitionObserver) remember(taskRun agentcontract.TaskRun) {
	ctx, cancel := context.WithTimeout(context.Background(), extractionTimeout)
	defer cancel()
	events := observer.TaskRuns.ListTaskEvent(taskRun.TaskRunID)
	extractionContext, _ := findExtractionContext(events)
	stack := observer.stackFor(taskRun.RequesterPersonID)
	scope := stack[0]
	note := bluememo.Note{
		GroupID:     taskRun.TaskRunID,
		Body:        RenderTranscript(TaskTranscript(taskRun, observer.taskSteps(taskRun.TaskRunID))),
		SpeakerName: extractionContext.RequesterName,
	}
	report, errorValue := observer.Stores.Remember(ctx, stack, note, answersOf(events))
	if errorValue != nil {
		observer.logger().Warn("memory.extraction.failed", "taskRunID", taskRun.TaskRunID, "scope", scope.Kind, "error", errorValue.Error())
		observer.TaskRuns.AppendTaskEvent(taskRun.TaskRunID, "memory.extraction_failed", marshalEventBody(map[string]any{
			"scope": scope.Kind,
			"error": errorValue.Error(),
		}))
		return
	}
	observer.TaskRuns.AppendTaskEvent(taskRun.TaskRunID, "memory.extraction_completed", marshalEventBody(map[string]any{
		"scope":      scope.Kind,
		"scopeID":    scope.ID,
		"inserted":   report.Inserted,
		"extended":   report.Extended,
		"superseded": report.Superseded,
		"reinforced": report.Reinforced,
	}))
}

func (observer TaskRunTransitionObserver) stackFor(personID string) []Scope {
	target := PersonScope(personID)
	if observer.Access == nil {
		return []Scope{target}
	}
	return StackToRemember(target, ScopesToSearch(observer.Access.ResolvePersonAccess(personID), observer.Access.ContainedCircles()))
}

func (observer TaskRunTransitionObserver) taskSteps(taskRunID string) []taskstate.TaskStep {
	if observer.Steps == nil {
		return nil
	}
	return observer.Steps.ListTaskStep(taskRunID)
}

func (observer TaskRunTransitionObserver) logger() *slog.Logger {
	if observer.Logger != nil {
		return observer.Logger
	}
	return slog.Default()
}

func findExtractionContext(events []agentcontract.TaskEvent) (ExtractionContext, bool) {
	for index := len(events) - 1; index >= 0; index-- {
		if events[index].Name != ExtractionContextEventName {
			continue
		}
		var extractionContext ExtractionContext
		if errorValue := json.Unmarshal([]byte(events[index].Body), &extractionContext); errorValue != nil {
			return ExtractionContext{}, false
		}
		return extractionContext, true
	}
	return ExtractionContext{}, false
}

func marshalEventBody(value any) string {
	document, errorValue := json.Marshal(value)
	if errorValue != nil {
		return fmt.Sprintf("%v", value)
	}
	return string(document)
}