Files
Cyrene_v2/internal/runtime/memory/extract.go
T

92 lines
3.0 KiB
Go

package memory
import (
"context"
"errors"
"strings"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
)
var (
// ErrNilAdapter is returned when an LLMExtractor is built or used without a
// model.Adapter to drive extraction.
ErrNilAdapter = errors.New("memory: nil model adapter")
)
// Extractor derives candidate memory items from a conversation (design §1).
//
// Two implementations exist:
//
// - LLMExtractor: asks a model.Adapter to denoise and deduplicate each turn,
// running synchronously (Extract) or on a background goroutine
// (ExtractAsync).
// - ManualExtractor: deterministic, rule-based — it only records what the user
// explicitly asks to remember ("记住 X").
type Extractor interface {
// Extract returns memory items worth persisting from conversation. ctx
// controls cancellation; a canceled context returns an error.
Extract(ctx context.Context, conversation []model.Message) ([]MemoryItem, error)
}
// ManualExtractor implements the manual "记住 X" extraction path without an LLM.
// It scans user turns for a leading remember directive and produces a memory
// item for each, falling back to no item for ordinary dialogue. It is the
// deterministic counterpart to the M4 LLM extractor.
type ManualExtractor struct {
personaID string
roleID string
}
// NewManualExtractor builds a ManualExtractor whose extracted items are scoped
// to personaID and roleID, isolated from other personas (design §4).
func NewManualExtractor(personaID, roleID string) *ManualExtractor {
return &ManualExtractor{personaID: personaID, roleID: roleID}
}
// Extract returns one memory item (kind relationship, persona scope) per user
// turn that asks the agent to remember something. A leading "记住"/"请记住"
// directive is stripped from the stored content; turns without one are skipped.
func (e *ManualExtractor) Extract(ctx context.Context, conversation []model.Message) ([]MemoryItem, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
items := make([]MemoryItem, 0)
for _, msg := range conversation {
if msg.Role != model.RoleUser {
continue
}
text := strings.TrimSpace(messageText(msg))
content, ok := rememberDirective(text)
if !ok {
continue
}
items = append(items, MemoryItem{
PersonaID: e.personaID,
RoleID: e.roleID,
Scope: ScopePersona,
Kind: KindRelationship,
Content: content,
Confidence: 1.0,
DecayWeight: 1.0,
})
}
return items, nil
}
// rememberDirective strips the "记住" / "请记住" prefix when text is a remember
// instruction. It returns the remembered content and true, or ok=false when the
// text is not an explicit remember request.
func rememberDirective(text string) (string, bool) {
for _, prefix := range []string{"请记住", "记住"} {
if strings.HasPrefix(text, prefix) {
content := strings.TrimSpace(strings.TrimPrefix(text, prefix))
if content == "" {
return "", false
}
return content, true
}
}
return "", false
}