Class: Pikuri::Memory::Recall

Inherits:
Tool
  • Object
show all
Defined in:
lib/pikuri/memory/recall.rb

Overview

The recall Tool: explicit, topic-driven deepening beyond the automatic per-turn prefetch (Extension#on_user_message). The “mined, deep” retrieval mode — the model calls it when the prefetch slice hints there is more to find on a topic. Same two-step agentic-RAG shape as pikuri-vectordb‘s vectordb_search (cheap auto slice) + vectordb_read (pull more) — here, prefetch is the slice and recall is the dig.

Single-param surface

recall(topic:) — no top_k, no user_id. Retrieval depth is host policy baked into the Extension; the namespace is fixed at construction. The model shouldn’t tune retrieval mid-conversation; a minimal surface is the deliberate choice (same rationale as vectordb_search).

Recall does not resolve contradictions

mem0 returns the relevant memories ranked by similarity, not by recency, and keeps a stale fact alongside its correction. So the observation carries each memory’s created_at timestamp and the tool description tells the model to treat newer-about-the-same- thing as current — resolution lives in the model’s reasoning, not in the store (DESIGN.md §“Supersede recall: resolution is the consumer’s job”).

Constant Summary collapse

LOGGER =
Pikuri.logger_for('Memory::Recall')
TOP_K =

Returns memories returned per recall. A handful —enough to surface a topic’s facts plus any correction, few enough to stay cheap in the turn.

Returns:

  • (Integer)

    memories returned per recall. A handful —enough to surface a topic’s facts plus any correction, few enough to stay cheap in the turn.

7
DESCRIPTION =

Returns static description shown to the LLM, opencode-shape (summary + Usage: bullets).

Returns:

  • (String)

    static description shown to the LLM, opencode-shape (summary + Usage: bullets).

<<~DESC
  Search your durable memory of the user for facts relevant to a topic.

  Usage:
  - Use to recall what you know about the user or their work beyond what was automatically surfaced this turn — preferences, ongoing projects, people, decisions.
  - Phrase `topic` as a natural-language statement or question, e.g. "the user's current main project" or "how the user likes test output".
  - Each result carries a timestamp. Memory is append-only: if two results conflict, the more recent one is current truth — the older one is kept as history, not a contradiction to flag.
  - Returns up to #{TOP_K} memories. If nothing relevant comes back, say you don't have a memory of it rather than guessing.
DESC

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(client:, user_id:) ⇒ Recall

Parameters:

  • client (Mem0Client)

    the mem0 client recall queries go to.

  • user_id (String)

    the fixed mem0 namespace to search.



52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/pikuri/memory/recall.rb', line 52

def initialize(client:, user_id:)
  super(
    name: 'recall',
    description: DESCRIPTION,
    parameters: Pikuri::Tool::Parameters.build { |p|
      p.required_string :topic,
                        'Natural-language topic or question to recall about the user, e.g. ' \
                        '"the user\'s dietary preferences" or "what project are we working on?".'
    },
    execute: lambda { |topic:|
      Recall.execute(client: client, user_id: user_id, topic: topic)
    }
  )
end

Class Method Details

.execute(client:, user_id:, topic:) ⇒ String

Public so specs can exercise recall without constructing a Tool wrapper. Catches Mem0Client failures and renders them as an “Error: …” observation the LLM can react to (a transient mem0 blip shouldn’t crash the loop) — bugs in pikuri’s own code still raise.

Parameters:

  • client (Mem0Client)
  • user_id (String)
  • topic (String)

Returns:

  • (String)

    formatted observation.



77
78
79
80
81
82
83
84
85
86
87
# File 'lib/pikuri/memory/recall.rb', line 77

def self.execute(client:, user_id:, topic:)
  return 'Error: topic is empty' if topic.nil? || topic.strip.empty?

  records = client.search(query: topic, user_id: user_id, top_k: TOP_K)
  return 'No relevant memories found.' if records.empty?

  format_observation(records)
rescue RuntimeError => e
  LOGGER.warn("recall failed: #{e.message}")
  "Error: memory recall failed: #{e.message}"
end