package memory import ( "context" "strings" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" ) // Extractor derives candidate memory items from a conversation (design §1). // // The production path is an LLM-driven asynchronous extractor that denoises and // deduplicates each turn; that is deferred to milestone M4, and its integration // point is this interface. The M2 implementation is the rule-based // ManualExtractor, which only records what the user explicitly asks to remember. 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 }