Class: Phronomy::LlmContextWindow::Assembler

Inherits:
Object
  • Object
show all
Defined in:
lib/phronomy/llm_context_window/assembler.rb

Overview

Assembler collects all four context regions and produces the final messages:, tool_classes: hash consumed by Agent::Base.

Regions:

  1. Instruction — system prompt text set via #add_instruction
  2. Capability — tool classes registered via #add_capability
  3. Knowledge — external facts injected via #add_knowledge (generates XML tags)
  4. Conversation — historical messages added via #add_messages

Token budgeting: When a budget is given, conversation messages are trimmed from oldest to newest until they fit. Capability token cost is estimated and deducted from the budget before conversation trimming so the reserve is accurate. Knowledge chunks are always included in full (they are assumed to be pre-screened by the caller). When no budget is given all messages are passed through unchanged.

Examples:

assembler = Phronomy::LlmContextWindow::Assembler.new(budget: budget)
assembler.add_instruction("You are a helpful assistant.")
assembler.add_knowledge("The user lives in Tokyo.", type: :entity, trusted: false)
assembler.add_messages(manager.load(thread_id: "t1", query: user_input))
context = assembler.build
# => { system: "You are ...\n<context ...>...</context>", messages: [...] }

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(budget: nil) ⇒ Assembler

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

mutant:disable - @instruction = nil deletion is a genuine equivalent (uninitialized Ruby instance variables return nil)

Parameters:



50
51
52
53
54
55
56
# File 'lib/phronomy/llm_context_window/assembler.rb', line 50

def initialize(budget: nil)
  @budget = budget
  @instruction = nil
  @tool_classes = []
  @knowledge_chunks = []
  @messages = []
end

Class Method Details

.xml_tag(text, type:, trusted: false) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Builds a single XML context tag string. Exposed as a class method so callers (e.g. Agent::Base) can build static knowledge XML tags independently of an Assembler instance.

mutant:disable - text.to_str and plain text (no to_s) are genuine equivalents when text is a String; type.to_str is genuine equivalent when type is a String

Parameters:

  • text (String)
  • type (Symbol, String)
  • trusted (Boolean) (defaults to: false)

Returns:

  • (String)


42
43
44
# File 'lib/phronomy/llm_context_window/assembler.rb', line 42

def self.xml_tag(text, type:, trusted: false)
  "<context type=\"#{CGI.escapeHTML(type.to_s)}\" trusted=\"#{trusted}\">\n#{CGI.escapeHTML(text.to_s)}\n</context>"
end

Instance Method Details

#add_capability(tool_classes) ⇒ self

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Register tool classes (Region 2). Estimates their token cost and deducts it from the budget so that conversation trimming accounts for tool definition overhead.

Parameters:

  • tool_classes (Array<Class, Object>)

    tool classes or instances

Returns:

  • (self)


65
66
67
68
# File 'lib/phronomy/llm_context_window/assembler.rb', line 65

def add_capability(tool_classes)
  @tool_classes = Array(tool_classes)
  self
end

#add_instruction(text) ⇒ self

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Set the system instruction text (Region 1). Calling this multiple times replaces the previous value.

mutant:disable - text.to_str and plain text (no .to_s) are genuine equivalents when callers always pass a String

Parameters:

  • text (String)

Returns:

  • (self)


77
78
79
80
# File 'lib/phronomy/llm_context_window/assembler.rb', line 77

def add_instruction(text)
  @instruction = text.to_s
  self
end

#add_knowledge(text, type:, trusted: false, source: nil) ⇒ self

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Append a knowledge chunk (Region 3). The chunk is wrapped in an XML context tag automatically.

mutant:disable - text: (shorthand, no .to_s) and text.to_str are genuine equivalents when text is a String; type: shorthand is genuine equivalent because xml_context_tag always calls .to_s on chunk[:type]

Parameters:

  • text (String)
  • type (Symbol, String)

    semantic label for the context tag (e.g. :entity, :rag, :static)

  • trusted (Boolean) (defaults to: false)

    false (default) indicates externally sourced data

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

    optional source label (e.g. filename); included in the XML tag so the LLM can produce grounded citations. Omitted when nil.

Returns:

  • (self)


93
94
95
96
# File 'lib/phronomy/llm_context_window/assembler.rb', line 93

def add_knowledge(text, type:, trusted: false, source: nil)
  @knowledge_chunks << {text: text.to_s, type: type.to_s, trusted: trusted, source: source}
  self
end

#add_messages(messages) ⇒ self

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Set conversation messages (Region 4). Replaces any previously set messages.

mutant:disable - @messages = messages (no Array()) is a genuine equivalent when callers always pass an Array

Parameters:

  • messages (Array)

    message-like objects with #role and #content

Returns:

  • (self)


104
105
106
107
# File 'lib/phronomy/llm_context_window/assembler.rb', line 104

def add_messages(messages)
  @messages = Array(messages)
  self
end

#available_for_messagesInteger?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the number of tokens available for conversation messages after accounting for instruction, knowledge, and capability overhead. Returns +nil+ when no budget is configured.

Returns:

  • (Integer, nil)


115
116
117
118
119
120
121
122
# File 'lib/phronomy/llm_context_window/assembler.rb', line 115

def available_for_messages
  return nil unless @budget
  knowledge_text = @knowledge_chunks.map { |c| xml_context_tag(c) }.join("\n\n")
  system_parts = [@instruction, knowledge_text.empty? ? nil : knowledge_text].compact
  system_text = system_parts.join("\n\n")
  used = TokenEstimator.estimate(system_text) + estimate_capability_tokens
  @budget.available(used: used)
end

#buildHash{Symbol => Object}

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Assemble the context.

Raises ContextLengthError when a budget is set and the conversation messages do not fit within the remaining token allowance. No automatic trimming is performed — callers must pre-process messages (e.g. via Agent::Base#trim_messages or #compact_messages) before passing them to the Assembler.

mutant:disable - multiple genuine equivalent mutations: map{}.join("\n\n") → map{} is genuine; unless knowledge_text.empty? vs ternary is genuine; { system: unless system_text.empty? } vs ternary is genuine; messages: shorthand vs messages: messages is genuine

Returns:

  • (Hash{Symbol => Object})

    :system [String, nil] combined system prompt (instruction + knowledge XML tags) :messages [Array] conversation messages, trimmed to budget if set :tool_classes [Array] tool classes/instances to register with the chat



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/phronomy/llm_context_window/assembler.rb', line 138

def build
  knowledge_text = @knowledge_chunks.map { |c| xml_context_tag(c) }.join("\n\n")
  system_parts = [@instruction, knowledge_text.empty? ? nil : knowledge_text].compact
  system_text = system_parts.join("\n\n")

  if @budget && @messages.any?
    capability_tokens = estimate_capability_tokens
    used = TokenEstimator.estimate(system_text) + capability_tokens
    remaining = @budget.available(used: used)
    msg_tokens = @messages.sum { |m| TokenEstimator.estimate(m.content.to_s) }
    if msg_tokens > remaining
      raise Phronomy::ContextLengthError,
        "Context exceeds token budget: messages require #{msg_tokens} tokens but " \
        "only #{remaining} available (context_window=#{@budget.context_window}, " \
        "used_by_system=#{used}). Override build_context to trim or compact messages."
    end
  end

  {
    system: system_text.empty? ? nil : system_text,
    messages: @messages,
    tool_classes: @tool_classes
  }
end