Class: RubyLLM::ResearchJob

Inherits:
Object
  • Object
show all
Includes:
Support::Inspectable
Defined in:
lib/ruby_llm/research_job.rb

Overview

A hosted, single-turn research task. RubyLLM.research_later returns a job immediately; #wait polls it and #message returns its report. The provider's agent identity is separate from an inference model.

job = RubyLLM.research_later(question, provider: provider, agent: agent_id)
job.wait
puts job.message.content

Defined Under Namespace

Classes: Error, InterruptedError, TimeoutError

Constant Summary

Constants included from Support::Inspectable

Support::Inspectable::TRUNCATE_AT

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Support::Inspectable

#full_inspect, #inspect, #pretty_print

Constructor Details

#initialize(id:, provider:, agent:, protocol:, **state) ⇒ ResearchJob

:nodoc:



121
122
123
124
125
126
127
# File 'lib/ruby_llm/research_job.rb', line 121

def initialize(id:, provider:, agent:, protocol:, **state) # :nodoc:
  @id = id
  @provider = provider.to_sym
  @agent = agent
  @protocol = protocol
  apply_state(state)
end

Instance Attribute Details

#agentObject (readonly)

The hosted agent's identity, separate from a model ID.



53
54
55
# File 'lib/ruby_llm/research_job.rb', line 53

def agent
  @agent
end

#cancellation_errorObject (readonly)

The error from an unsuccessful automatic cancellation attempt, if any.



66
67
68
# File 'lib/ruby_llm/research_job.rb', line 66

def cancellation_error
  @cancellation_error
end

#errorObject (readonly)

The provider's failure explanation, if any.



60
61
62
# File 'lib/ruby_llm/research_job.rb', line 60

def error
  @error
end

#idObject (readonly)

The provider-assigned job ID.



47
48
49
# File 'lib/ruby_llm/research_job.rb', line 47

def id
  @id
end

#providerObject (readonly)

The provider slug.



50
51
52
# File 'lib/ruby_llm/research_job.rb', line 50

def provider
  @provider
end

#rawObject (readonly)

The original provider response from submission or the latest poll.



63
64
65
# File 'lib/ruby_llm/research_job.rb', line 63

def raw
  @raw
end

#statusObject (readonly)

The normalized state: :pending, :completed, :incomplete, :failed, or :cancelled.



57
58
59
# File 'lib/ruby_llm/research_job.rb', line 57

def status
  @status
end

#tokensObject (readonly)

Returns the provider-reported task usage. Unreported fields are nil.



197
198
199
# File 'lib/ruby_llm/research_job.rb', line 197

def tokens
  @tokens
end

Class Method Details

.find(id, provider:, context: nil) ⇒ Object

Retrieves an existing task by ID without submitting another one. Use the same provider configuration that created the task.



110
111
112
113
# File 'lib/ruby_llm/research_job.rb', line 110

def self.find(id, provider:, context: nil)
  config = context&.config || RubyLLM.config
  Provider.resolve!(provider).new(config).find_research_job(id)
end

.research(prompt, timeout: 600, interval: 5, **options) ⇒ Object

Runs a research task and returns its Message. On a timeout or interrupt, attempts to cancel the remote task before raising an error retaining the job. timeout: and interval: are seconds.



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/ruby_llm/research_job.rb', line 88

def self.research(prompt, timeout: 600, interval: 5, **options)
  validate_polling_options(timeout, interval)
  job = research_later(prompt, **options)
  job.wait(timeout:, interval:).message
rescue Interrupt => e
  raise unless job

  job.send(:attempt_cancellation)
  raise InterruptedError.new("Research interrupted (job #{job.id})", job:), cause: e
rescue StandardError => e
  job ||= e.job if e.is_a?(Error)
  raise unless job

  job.send(:attempt_cancellation)
  raise if e.is_a?(Error)

  response = e.response if e.respond_to?(:response)
  raise Error.new("Research failed: #{e.message} (job #{job.id})", job:, response:), cause: e
end

.research_later(prompt, provider:, agent:, with: nil, provider_tools: nil, context: nil, provider_options: {}, metadata: nil) ⇒ Object

Submits one research task without waiting. provider: and agent: are required; with: attaches documents or images where supported. provider_tools: accepts an array of aliases or a Hash of aliases and their options, as on Chat#with_provider_tools.



72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/ruby_llm/research_job.rb', line 72

def self.research_later(prompt, provider:, agent:, with: nil, provider_tools: nil,
                        context: nil, provider_options: {}, metadata: nil)
  config = context&.config || RubyLLM.config
  instance = Provider.resolve!(provider).new(config)
  payload = { provider: instance.slug, agent:, prompt:, metadata: }
  RubyLLM.instrument('research_job.ruby_llm', payload, config:) do |event|
    job = instance.research_later(prompt, agent:, with:, provider_tools:, provider_options:)
    event[:job_id] = job.id
    event[:status] = job.status
    job
  end
end

.validate_polling_options(timeout, interval) ⇒ Object

:nodoc:

Raises:

  • (ArgumentError)


115
116
117
118
119
# File 'lib/ruby_llm/research_job.rb', line 115

def self.validate_polling_options(timeout, interval) # :nodoc:
  return if [timeout, interval].all? { |value| value.is_a?(Numeric) && value.positive? && value.finite? }

  raise ArgumentError, 'Research timeout and interval must be positive finite numbers'
end

Instance Method Details

#cancel(timeout: 5) ⇒ Object

Requests cancellation and returns self. Only the provider's response can confirm cancellation; this does not delete stored task data.



178
179
180
181
182
183
184
185
# File 'lib/ruby_llm/research_job.rb', line 178

def cancel(timeout: 5)
  self.class.validate_polling_options(timeout, 1)
  unless done?
    state = request_with_timeout(timeout) { @protocol.cancel_research_job(self, timeout:) }
    apply_state(state)
  end
  self
end

#cancelled?Boolean

Returns whether the provider confirmed cancellation.

Returns:

  • (Boolean)


145
# File 'lib/ruby_llm/research_job.rb', line 145

def cancelled? = status == :cancelled

#completed?Boolean

Returns whether the task finished with a complete report.

Returns:

  • (Boolean)


136
# File 'lib/ruby_llm/research_job.rb', line 136

def completed? = status == :completed

#costObject

Returns the reported cost, or unknown cost when the provider supplies no price. Agent IDs are never used to look up model token prices.



201
202
203
# File 'lib/ruby_llm/research_job.rb', line 201

def cost
  Cost.from_h({ total: tokens.reported_cost }.compact, tokens:)
end

#done?Boolean

Returns whether the task reached any terminal state.

Returns:

  • (Boolean)


133
# File 'lib/ruby_llm/research_job.rb', line 133

def done? = !pending?

#failed?Boolean

Returns whether the provider reported failure.

Returns:

  • (Boolean)


142
# File 'lib/ruby_llm/research_job.rb', line 142

def failed? = status == :failed

#incomplete?Boolean

Returns whether the provider stopped before completing the report.

Returns:

  • (Boolean)


139
# File 'lib/ruby_llm/research_job.rb', line 139

def incomplete? = status == :incomplete

#messageObject

Returns the report, or nil while pending. An incomplete report has Message#finish_reason :max_tokens. Raises Error for failed or cancelled tasks.

Raises:



190
191
192
193
194
# File 'lib/ruby_llm/research_job.rb', line 190

def message
  raise Error.new("Research #{status}: #{error} (job #{id})", job: self) if failed? || cancelled?

  @message
end

#pending?Boolean

Returns whether the task is waiting or running.

Returns:

  • (Boolean)


130
# File 'lib/ruby_llm/research_job.rb', line 130

def pending? = status == :pending

#refresh(timeout: nil) ⇒ Object

Fetches the latest state and returns self. Does nothing after the task finishes. timeout: limits this request in seconds.



149
150
151
152
153
154
155
156
# File 'lib/ruby_llm/research_job.rb', line 149

def refresh(timeout: nil)
  self.class.validate_polling_options(timeout, 1) unless timeout.nil?
  unless done?
    state = request_with_timeout(timeout) { @protocol.refresh_research_job(self, timeout:) }
    apply_state(state)
  end
  self
end

#wait(timeout: 600, interval: 5) ⇒ Object

Polls until a terminal state and returns self. A timeout leaves the independent task running and raises TimeoutError with this job. Incomplete reports remain available through #message.

Raises:



161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/ruby_llm/research_job.rb', line 161

def wait(timeout: 600, interval: 5)
  self.class.validate_polling_options(timeout, interval)
  deadline = monotonic_time + timeout
  until done?
    remaining = deadline - monotonic_time
    raise TimeoutError.new("Research timed out (job #{id})", job: self) unless remaining.positive?

    refresh(timeout: remaining)
    sleep [interval, deadline - monotonic_time].min if pending? && monotonic_time < deadline
  end
  raise Error.new("Research #{status}: #{error} (job #{id})", job: self) if failed? || cancelled?

  self
end