82 lines
2.7 KiB
Go
82 lines
2.7 KiB
Go
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
|
|
}
|