package ice import ( "context" "fmt" "strings" "ai-agent/internal/llm" "ai-agent/internal/memory" ) var autoMemorySystemPrompt = "Extract any important facts, user preferences, decisions, or action items from this exchange.\n" + "Output one item per line in the format: TYPE: content\n" + "Where TYPE is one of: FACT, DECISION, PREFERENCE, TODO\n" + "If there is nothing worth remembering, output exactly: NONE" var autoMemoryUserTemplate = "User: %s\nAssistant: %s" type AutoMemory struct { client llm.Client memStore *memory.Store } func (am *AutoMemory) Detect(ctx context.Context, userMsg, assistantMsg string) error { if am.memStore == nil { return nil } if len(userMsg) < 20 && len(assistantMsg) < 50 { return nil } prompt := fmt.Sprintf(autoMemoryUserTemplate, userMsg, assistantMsg) var response strings.Builder err := am.client.ChatStream(ctx, llm.ChatOptions{ System: autoMemorySystemPrompt, Messages: []llm.Message{ {Role: "user", Content: prompt}, }, }, func(chunk llm.StreamChunk) error { response.WriteString(chunk.Text) return nil }) if err != nil { return fmt.Errorf("auto-memory LLM call: %w", err) } return am.parseAndSave(response.String()) } func (am *AutoMemory) parseAndSave(response string) error { lines := strings.Split(strings.TrimSpace(response), "\n") for _, line := range lines { line = strings.TrimSpace(line) if line == "" || strings.EqualFold(line, "NONE") { continue } parts := strings.SplitN(line, ": ", 2) if len(parts) != 2 { continue } typeName := strings.TrimSpace(parts[0]) content := strings.TrimSpace(parts[1]) if content == "" { continue } tag := strings.ToLower(typeName) switch tag { case "fact", "decision", "preference", "todo": // Valid type. default: continue } if _, err := am.memStore.Save(content, []string{tag, "auto"}); err != nil { return fmt.Errorf("save auto-memory: %w", err) } } return nil }