feat(memory): 长记忆 store(SQLite+persona 隔离)/工作记忆滑动窗口/检索注入/手工提取钩子
This commit is contained in:
1 parent
285e1e9911
commit
aa697c385f
10 files changed
+1705
No files matched your search
@@ -0,0 +1,81 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user