Class: Ask::Agent::MemoryExtractor

Inherits:
Object
  • Object
show all
Defined in:
lib/ask/agent/memory_extractor.rb

Overview

Extracts durable facts from a finished session's transcript and writes them to the session's Memory — the "learning" half of durable memory.

One LLM call with a structured-output prompt: the model reads the memory-relevant messages and returns a JSON list of durable facts. Candidates are deduped against the store (exact + near-duplicate via search), written with provenance (extracted: true, source session id), and capped at max_candidates.

Extraction never raises: a failed call or unparseable response yields an empty result hash, never a broken session.

Session wiring: Session.new(memory: memory, memory_learning: true).

Constant Summary collapse

DEFAULT_SYSTEM_PROMPT =
<<~PROMPT.strip
  You are a memory curator. Read the conversation transcript and extract
  durable facts worth remembering across sessions: user preferences,
  decisions, conventions, resolved problems, and standing instructions.
  Skip ephemeral details, session-specific chatter, secrets, and facts
  already obvious from the conversation itself.
  Respond with JSON only: {"facts": ["fact one", "fact two", ...]}.
  Return an empty list if nothing is worth remembering.
PROMPT

Instance Method Summary collapse

Constructor Details

#initialize(model:, memory:, chat: nil, system_prompt: DEFAULT_SYSTEM_PROMPT, max_candidates: 10, max_transcript_messages: 60) ⇒ MemoryExtractor

Returns a new instance of MemoryExtractor.

Parameters:

  • model (String)

    model to extract with

  • memory (Ask::Agent::Memory)

    store to write into

  • chat (Ask::Agent::Chat, nil) (defaults to: nil)

    chat to use (built from model when nil; inject a stub in tests)

  • system_prompt (String) (defaults to: DEFAULT_SYSTEM_PROMPT)

    domain-specific extraction instructions

  • max_candidates (Integer) (defaults to: 10)

    cap on facts written per extraction

  • max_transcript_messages (Integer) (defaults to: 60)

    cap on transcript messages sent to the model (oldest dropped first)



39
40
41
42
43
44
45
46
47
# File 'lib/ask/agent/memory_extractor.rb', line 39

def initialize(model:, memory:, chat: nil, system_prompt: DEFAULT_SYSTEM_PROMPT,
               max_candidates: 10, max_transcript_messages: 60)
  @model = model
  @memory = memory
  @chat = chat
  @system_prompt = system_prompt
  @max_candidates = max_candidates
  @max_transcript_messages = max_transcript_messages
end

Instance Method Details

#extract(transcript:, session_id: nil) ⇒ Hash

Returns [String], skipped: [String], error: [String, nil].

Parameters:

  • transcript (Array<Ask::Message>)

    the finished session's messages

  • session_id (String, nil) (defaults to: nil)

    stamped into extracted entries as provenance

Returns:

  • (Hash)

    [String], skipped: [String], error: [String, nil]



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/ask/agent/memory_extractor.rb', line 53

def extract(transcript:, session_id: nil)
  relevant = memory_relevant_messages(transcript)
  return { extracted: [], skipped: [], error: nil } if relevant.empty?

  facts = request_facts(relevant)
  return { extracted: [], skipped: [], error: "no facts returned" } if facts.empty?

  extracted = []
  skipped = []
  facts.first(@max_candidates).each do |fact|
    if duplicate?(fact)
      skipped << fact
    else
      @memory.write(
        fact,
        metadata: { extracted: true, session_id: session_id, extracted_at: Time.now.iso8601 }
      )
      extracted << fact
    end
  end
  { extracted: extracted, skipped: skipped, error: nil }
rescue StandardError => e
  { extracted: [], skipped: [], error: e.message }
end