Class: Riffer::Agent

Inherits:
Object
  • Object
show all
Extended by:
Helpers::ClassNameConverter, Helpers::Validations
Includes:
Messages::Converter
Defined in:
lib/riffer/agent.rb

Overview

Riffer::Agent is the base class for all agents in the Riffer framework.

Provides orchestration for LLM calls, tool use, and message management. Subclass this to create your own agents.

See Riffer::Messages and Riffer::Providers.

class MyAgent < Riffer::Agent
  model 'openai/gpt-4o'
  instructions 'You are a helpful assistant.'
end

agent = MyAgent.new
agent.generate('Hello!')

Defined Under Namespace

Classes: Response

Constant Summary

Constants included from Helpers::ClassNameConverter

Helpers::ClassNameConverter::DEFAULT_SEPARATOR

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Helpers::ClassNameConverter

class_name_to_path

Methods included from Helpers::Validations

validate_is_string!

Methods included from Messages::Converter

#convert_to_message_object

Constructor Details

#initializeAgent

Initializes a new agent.

Raises Riffer::ArgumentError if the configured model string is invalid (must be “provider/model” format).

: () -> void



157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/riffer/agent.rb', line 157

def initialize
  @messages = []
  @message_callbacks = []
  @token_usage = nil
  @model_string = self.class.model
  @instructions_text = self.class.instructions

  provider_name, model_name = @model_string.split("/", 2)

  raise Riffer::ArgumentError, "Invalid model string: #{@model_string}" unless [provider_name, model_name].all? { |part| part.is_a?(String) && !part.strip.empty? }

  @provider_name = provider_name
  @model_name = model_name
end

Instance Attribute Details

#messagesObject (readonly)

The message history for the agent.



146
147
148
# File 'lib/riffer/agent.rb', line 146

def messages
  @messages
end

#token_usageObject (readonly)

Cumulative token usage across all LLM calls.



149
150
151
# File 'lib/riffer/agent.rb', line 149

def token_usage
  @token_usage
end

Class Method Details

.allObject

Returns all agent subclasses.

: () -> Array



86
87
88
# File 'lib/riffer/agent.rb', line 86

def self.all
  subclasses
end

.find(identifier) ⇒ Object

Finds an agent class by identifier.

: (String) -> singleton(Riffer::Agent)?



79
80
81
# File 'lib/riffer/agent.rb', line 79

def self.find(identifier)
  subclasses.find { |agent_class| agent_class.identifier == identifier.to_s }
end

.generateObject

Generates a response using a new agent instance.

See #generate for parameters and return value.

: (*untyped, **untyped) -> Riffer::Agent::Response



95
96
97
# File 'lib/riffer/agent.rb', line 95

def self.generate(...)
  new.generate(...)
end

.guardrail(phase, with:, **options) ⇒ Object

Registers a guardrail for input, output, or both phases.

phase - :before, :after, or :around. with - the guardrail class (must be subclass of Riffer::Guardrail). options - additional options passed to the guardrail.

Raises Riffer::ArgumentError if phase is invalid or guardrail is not a Guardrail class. : (Symbol, with: singleton(Riffer::Guardrail), **untyped) -> void



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/riffer/agent.rb', line 116

def self.guardrail(phase, with:, **options)
  valid_phases = [*Riffer::Guardrails::PHASES, :around]
  raise Riffer::ArgumentError, "Invalid guardrail phase: #{phase}" unless valid_phases.include?(phase)
  raise Riffer::ArgumentError, "Guardrail must be a Riffer::Guardrail subclass" unless with.is_a?(Class) && with <= Riffer::Guardrail

  @guardrails ||= {before: [], after: []}
  config = {class: with, options: options}

  case phase
  when :before
    @guardrails[:before] << config
  when :after
    @guardrails[:after] << config
  when :around
    @guardrails[:before] << config
    @guardrails[:after] << config
  end
end

.guardrails_for(phase) ⇒ Object

Returns the registered guardrail configs for a given phase.

phase - :before or :after.

: (Symbol) -> Array[Hash[Symbol, untyped]]



140
141
142
143
# File 'lib/riffer/agent.rb', line 140

def self.guardrails_for(phase)
  @guardrails ||= {before: [], after: []}
  @guardrails[phase] || []
end

.identifier(value = nil) ⇒ Object

Gets or sets the agent identifier.

: (?String?) -> String



29
30
31
32
# File 'lib/riffer/agent.rb', line 29

def self.identifier(value = nil)
  return @identifier || class_name_to_path(name) if value.nil?
  @identifier = value.to_s
end

.instructions(instructions_text = nil) ⇒ Object

Gets or sets the agent instructions.

: (?String?) -> String?



46
47
48
49
50
# File 'lib/riffer/agent.rb', line 46

def self.instructions(instructions_text = nil)
  return @instructions if instructions_text.nil?
  validate_is_string!(instructions_text, "instructions")
  @instructions = instructions_text
end

.model(model_string = nil) ⇒ Object

Gets or sets the model string (e.g., “openai/gpt-4o”).

: (?String?) -> String?



37
38
39
40
41
# File 'lib/riffer/agent.rb', line 37

def self.model(model_string = nil)
  return @model if model_string.nil?
  validate_is_string!(model_string, "model")
  @model = model_string
end

.model_options(options = nil) ⇒ Object

Gets or sets model options passed to generate_text/stream_text.

: (?Hash[Symbol, untyped]?) -> Hash[Symbol, untyped]



63
64
65
66
# File 'lib/riffer/agent.rb', line 63

def self.model_options(options = nil)
  return @model_options || {} if options.nil?
  @model_options = options
end

.provider_options(options = nil) ⇒ Object

Gets or sets provider options passed to the provider client.

: (?Hash[Symbol, untyped]?) -> Hash[Symbol, untyped]



55
56
57
58
# File 'lib/riffer/agent.rb', line 55

def self.provider_options(options = nil)
  return @provider_options || {} if options.nil?
  @provider_options = options
end

.streamObject

Streams a response using a new agent instance.

See #stream for parameters and return value.

: (*untyped, **untyped) -> Enumerator[Riffer::StreamEvents::Base, void]



104
105
106
# File 'lib/riffer/agent.rb', line 104

def self.stream(...)
  new.stream(...)
end

.uses_tools(tools_or_lambda = nil) ⇒ Object

Gets or sets the tools used by this agent.

: (?(Array | Proc)?) -> (Array | Proc)?



71
72
73
74
# File 'lib/riffer/agent.rb', line 71

def self.uses_tools(tools_or_lambda = nil)
  return @tools_config if tools_or_lambda.nil?
  @tools_config = tools_or_lambda
end

Instance Method Details

#generate(prompt_or_messages, tool_context: nil) ⇒ Object

Generates a response from the agent.

: ((String | Array[Hash[Symbol, untyped] | Riffer::Messages::Base]), ?tool_context: Hash[Symbol, untyped]?) -> Riffer::Agent::Response



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/riffer/agent.rb', line 175

def generate(prompt_or_messages, tool_context: nil)
  @tool_context = tool_context
  @resolved_tools = nil
  initialize_messages(prompt_or_messages)

  all_modifications = [] #: Array[Riffer::Guardrails::Modification]

  tripwire, modifications = run_before_guardrails
  all_modifications.concat(modifications)
  return build_response("", tripwire: tripwire, modifications: all_modifications) if tripwire

  loop do
    response = call_llm

    track_token_usage(response.token_usage)

    processed_response, tripwire, modifications = run_after_guardrails(response)
    all_modifications.concat(modifications)

    return build_response("", tripwire: tripwire, modifications: all_modifications) if tripwire

    add_message(processed_response)

    break unless has_tool_calls?(processed_response)

    execute_tool_calls(processed_response)
  end

  build_response(extract_final_response, modifications: all_modifications)
end

#on_message(&block) ⇒ Object

Registers a callback to be invoked when messages are added during generation.

Raises Riffer::ArgumentError if no block is given.

: () { (Riffer::Messages::Base) -> void } -> self



284
285
286
287
288
# File 'lib/riffer/agent.rb', line 284

def on_message(&block)
  raise Riffer::ArgumentError, "on_message requires a block" unless block_given?
  @message_callbacks << block
  self
end

#stream(prompt_or_messages, tool_context: nil) ⇒ Object

Streams a response from the agent.

: ((String | Array[Hash[Symbol, untyped] | Riffer::Messages::Base]), ?tool_context: Hash[Symbol, untyped]?) -> Enumerator[Riffer::StreamEvents::Base, void]



209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/riffer/agent.rb', line 209

def stream(prompt_or_messages, tool_context: nil)
  @tool_context = tool_context
  @resolved_tools = nil
  initialize_messages(prompt_or_messages)

  Enumerator.new do |yielder|
    tripwire, modifications = run_before_guardrails
    modifications.each { |m| yielder << Riffer::StreamEvents::GuardrailModification.new(m) }

    if tripwire
      yielder << Riffer::StreamEvents::GuardrailTripwire.new(tripwire)
      next
    end

    loop do
      accumulated_content = ""
      accumulated_tool_calls = []
      accumulated_token_usage = nil
      current_tool_call = nil

      call_llm_stream.each do |event|
        yielder << event

        case event
        when Riffer::StreamEvents::TextDelta
          accumulated_content += event.content
        when Riffer::StreamEvents::TextDone
          accumulated_content = event.content
        when Riffer::StreamEvents::ToolCallDelta
          current_tool_call ||= {item_id: event.item_id, name: event.name, arguments: ""}
          current_tool_call[:arguments] += event.arguments_delta
          current_tool_call[:name] ||= event.name
        when Riffer::StreamEvents::ToolCallDone
          accumulated_tool_calls << Riffer::Messages::Assistant::ToolCall.new(
            id: event.item_id,
            call_id: event.call_id,
            name: event.name,
            arguments: event.arguments
          )
          current_tool_call = nil
        when Riffer::StreamEvents::TokenUsageDone
          accumulated_token_usage = event.token_usage
        end
      end

      response = Riffer::Messages::Assistant.new(
        accumulated_content,
        tool_calls: accumulated_tool_calls,
        token_usage: accumulated_token_usage
      )

      track_token_usage(accumulated_token_usage)

      processed_response, tripwire, modifications = run_after_guardrails(response)
      modifications.each { |m| yielder << Riffer::StreamEvents::GuardrailModification.new(m) }

      if tripwire
        yielder << Riffer::StreamEvents::GuardrailTripwire.new(tripwire)
        break
      end

      add_message(processed_response)

      break unless has_tool_calls?(processed_response)

      execute_tool_calls(processed_response)
    end
  end
end