Class: Zephira::History

Inherits:
Object
  • Object
show all
Defined in:
lib/zephira/history.rb

Constant Summary collapse

STORAGE_DIR =
".zephira"
STORAGE_FILE =
"history.jsonl"
COMPACTION_CHUNK_SIZE =
10

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(messages = []) ⇒ History

Returns a new instance of History.



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/zephira/history.rb', line 15

def initialize(messages = [])
  @storage_dir = File.join(Dir.pwd, STORAGE_DIR)
  @storage_file = File.join(@storage_dir, STORAGE_FILE)

  FileUtils.mkdir_p(@storage_dir)

  if messages.empty? && File.file?(@storage_file) && File.size(@storage_file) > 0
    @messages = load_from_disk
  else
    @messages = messages.dup
    write_all_to_disk
  end

  @session_start = @messages.size
end

Instance Attribute Details

#messagesObject (readonly)

Returns the value of attribute messages.



13
14
15
# File 'lib/zephira/history.rb', line 13

def messages
  @messages
end

#session_startObject (readonly)

Returns the value of attribute session_start.



13
14
15
# File 'lib/zephira/history.rb', line 13

def session_start
  @session_start
end

Instance Method Details

#append(role:, content:, tool_calls: nil, tool_call_id: nil) ⇒ Object



31
32
33
34
35
36
37
38
39
40
41
# File 'lib/zephira/history.rb', line 31

def append(role:, content:, tool_calls: nil, tool_call_id: nil)
  entry = {
    role: role,
    content: content,
    tool_calls: tool_calls,
    tool_call_id: tool_call_id,
    timestamp: Time.now.iso8601
  }.compact
  @messages << entry
  persist_entry(entry)
end

#clearObject



73
74
75
76
# File 'lib/zephira/history.rb', line 73

def clear
  @messages.clear
  write_all_to_disk
end

#clear_sessionObject



78
79
80
81
82
# File 'lib/zephira/history.rb', line 78

def clear_session
  return unless @session_start
  @messages = @messages[0...@session_start]
  write_all_to_disk
end

#compact(response_model:, api_key:, agent:, token_limit: Float::INFINITY) ⇒ Object



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/zephira/history.rb', line 47

def compact(response_model:, api_key:, agent:, token_limit: Float::INFINITY)
  return unless size > token_limit

  chunks = []
  while size > token_limit && !@messages.empty?
    chunks << @messages.shift(COMPACTION_CHUNK_SIZE)
  end

  chunks.each do |chunk|
    conversation = chunk.map { |message| "#{message[:role]}: #{message[:content]}" }.join("\n")
    summary = response_model.simple_inference(
      api_key: api_key,
      agent: agent,
      messages: [{role: "user", content: "Summarize the following conversation:\n#{conversation}"}]
    )

    @messages.unshift(
      role: "system",
      content: "[Summary of #{chunk.size} messages]\n#{summary}",
      timestamp: Time.now.iso8601
    )
  end

  write_all_to_disk
end

#compact_tool_messages!Object



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/zephira/history.rb', line 84

def compact_tool_messages!
  @messages = @messages
    .reject { |message| message[:role] == "tool" }
    .map do |message|
      next message unless message[:tool_calls]&.any?

      summary_lines = message[:tool_calls].map do |tool_call|
        name = tool_call.dig(:function, :name) || tool_call.dig("function", "name")
        arguments = tool_call.dig(:function, :arguments) || tool_call.dig("function", "arguments")
        arguments = JSON.parse(arguments, symbolize_names: true)
        "- `#{name}` with intent `#{arguments[:intent]}`"
      end
      summary_lines.unshift("Agent used tool(s):\n")
      {role: "assistant", content: summary_lines.join("\n"), timestamp: message[:timestamp]}
    end

  write_all_to_disk
end

#sizeObject



43
44
45
# File 'lib/zephira/history.rb', line 43

def size
  @messages.sum { |message| Tokens.estimate(message[:content]) }
end