Class: Llmemory::Extractors::FactExtractor

Inherits:
Object
  • Object
show all
Defined in:
lib/llmemory/extractors/fact_extractor.rb

Constant Summary collapse

CLASSIFY_JSON_SCHEMA =
{
  name: "fact_classification",
  schema: {
    type: "object",
    properties: {
      items: {
        type: "array",
        items: {
          type: "object",
          properties: {
            content: { type: "string" },
            category: { type: "string" }
          },
          required: ["content", "category"],
          additionalProperties: false
        }
      }
    },
    required: ["items"],
    additionalProperties: false
  }
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(llm: nil) ⇒ FactExtractor

Returns a new instance of FactExtractor.



8
9
10
# File 'lib/llmemory/extractors/fact_extractor.rb', line 8

def initialize(llm: nil)
  @llm = llm || Llmemory::LLM.client
end

Instance Method Details

#classify_item(content) ⇒ Object



45
46
47
# File 'lib/llmemory/extractors/fact_extractor.rb', line 45

def classify_item(content)
  classify_items([content])[content.to_s] || "general"
end

#classify_items(contents) ⇒ Object



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/llmemory/extractors/fact_extractor.rb', line 72

def classify_items(contents)
  list = Array(contents).map(&:to_s).reject(&:empty?)
  return {} if list.empty?

  if @llm.respond_to?(:invoke_with_json_schema)
    begin
      numbered = list.map.with_index { |c, i| "#{i + 1}. #{c}" }.join("\n")
      prompt = <<~PROMPT
        Classify each fact into ONE category. Use lowercase with underscores.
        Examples: work_life, personal_life, preferences, general.

        Facts:
        #{numbered}

        Return one category per fact in the same order.
      PROMPT
      parsed = @llm.invoke_with_json_schema(prompt.strip, CLASSIFY_JSON_SCHEMA)
      mapped = map_classifications(parsed, list)
      return mapped if mapped.size == list.size
    rescue Llmemory::LLMError
      # fall back to per-item classification
    end
  end

  list.each_with_object({}) { |content, acc| acc[content] = classify_item_fallback(content) }
end

#evolve_summary(existing:, new_memories:) ⇒ Object



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/llmemory/extractors/fact_extractor.rb', line 25

def evolve_summary(existing:, new_memories:)
  memory_list_text = Array(new_memories).map { |m| "- #{m}" }.join("\n")
  prompt = <<~PROMPT
    You are a Memory Synchronization Specialist.
    Topic Scope: User Profile

    ## Original Profile
    #{existing.to_s.empty? ? "No existing profile." : existing}

    ## New Memory Items to Integrate
    #{memory_list_text}

    # Task
    1. Update: If new items conflict with the Original Profile, overwrite the old facts.
    2. Add: If items are new, append them logically.
    3. Output: Return ONLY the updated markdown profile.
  PROMPT
  @llm.invoke(prompt.strip).to_s
end

#extract_items(conversation_text) ⇒ Object



12
13
14
15
16
17
18
19
20
21
22
23
# File 'lib/llmemory/extractors/fact_extractor.rb', line 12

def extract_items(conversation_text)
  prompt = <<~PROMPT
    Extract discrete facts from this conversation.
    Focus on preferences, behaviors, and important details.
    Conversation: #{conversation_text}
    Return as JSON array of objects with "content" and "importance" (0-1) keys.
    Importance: 0.8-0.95 for preferences/corrections/decisions, 0.5-0.8 for factual context, 0.3-0.5 for ephemeral.
    Example: [{"content": "User prefers Ruby", "importance": 0.9}, {"content": "User mentioned the weather", "importance": 0.4}]
  PROMPT
  response = @llm.invoke(prompt.strip)
  parse_items_response(response)
end