llm_chain 0.5.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../interfaces/agent'
4
+ require_relative '../client_registry'
5
+
6
+ module LLMChain
7
+ module Agents
8
+ # Agent that decomposes a complex user request into a sequence of atomic steps.
9
+ #
10
+ # The agent uses the same model system as other agents in the framework.
11
+ # It creates a client through the ClientRegistry based on the provided model.
12
+ #
13
+ # @example Basic usage
14
+ # planner = LLMChain::Agents::PlannerAgent.new(model: "qwen3:1.7b")
15
+ # steps = planner.plan("Find the president of the US and the capital of France")
16
+ # # => ["Find the president of the US", "Find the capital of France"]
17
+ class PlannerAgent < LLMChain::Interfaces::Agent
18
+ attr_reader :model, :client
19
+
20
+ # Initialize the planner agent with a model identifier.
21
+ # @param model [String] LLM model identifier
22
+ # @param client_options [Hash] additional client options
23
+ def initialize(model:, **client_options)
24
+ @model = model
25
+ @client = LLMChain::ClientRegistry.client_for(@model, **client_options)
26
+ end
27
+
28
+ # Decompose a complex user task into a sequence of atomic steps.
29
+ # @param task [String] The complex user request to decompose.
30
+ # @return [Array<String>] The list of atomic steps (one per string).
31
+ # @example
32
+ # planner = PlannerAgent.new(client: my_llm_client)
33
+ # steps = planner.plan("Find the president of the US and the capital of France")
34
+ # # => ["Find the president of the US", "Find the capital of France"]
35
+ def plan(task)
36
+ prompt = <<~PROMPT
37
+ Decompose the following user request into a minimal sequence of atomic steps.
38
+ Return only the steps, one per line, no explanations, *no numbering*.
39
+
40
+ User request:
41
+ #{task}
42
+
43
+ Steps:
44
+ PROMPT
45
+
46
+ response = @client.chat(prompt)
47
+ response.lines.map(&:strip).reject(&:empty?)
48
+ end
49
+
50
+ # Check if this agent can handle the given task
51
+ # @param task [String] The user request
52
+ # @return [Boolean] Whether this agent can handle the task
53
+ def can_handle?(task)
54
+ !task.nil? && !task.strip.empty?
55
+ end
56
+
57
+ # Execute a task using the agent's capabilities
58
+ # @param task [String] the task to accomplish
59
+ # @param stream [Boolean] whether to stream reasoning steps
60
+ # @yield [Hash] reasoning step information (when streaming)
61
+ # @return [Hash] execution result with reasoning trace
62
+ def run(task, stream: false, &block)
63
+ steps = plan(task)
64
+
65
+ result = {
66
+ task: task,
67
+ steps: steps,
68
+ result: steps.join("\n\n"),
69
+ reasoning_trace: [
70
+ {
71
+ step: 1,
72
+ action: "plan",
73
+ action_input: task,
74
+ observation: "Decomposed into #{steps.length} steps: #{steps.join(', ')}"
75
+ }
76
+ ]
77
+ }
78
+
79
+ yield(result) if block_given? && stream
80
+ result
81
+ end
82
+
83
+ # Get the model identifier used by this agent
84
+ # @return [String] model name
85
+ def model
86
+ @model
87
+ end
88
+
89
+ # Get the tool manager available to this agent
90
+ # @return [LLMChain::Interfaces::ToolManager] tool manager
91
+ def tools
92
+ nil # Planner doesn't use tools directly
93
+ end
94
+
95
+ # Get the memory system used by this agent
96
+ # @return [LLMChain::Interfaces::Memory] memory backend
97
+ def memory
98
+ nil # Planner doesn't use memory
99
+ end
100
+
101
+ # Get description of agent capabilities
102
+ # @return [String] Agent description
103
+ def description
104
+ "Planner agent that decomposes complex tasks into atomic steps."
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,307 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../interfaces/agent'
4
+ require_relative '../client_registry'
5
+
6
+ module LLMChain
7
+ module Agents
8
+ # ReAct Agent implements the Reasoning + Acting paradigm
9
+ #
10
+ # The agent follows this cycle:
11
+ # 1. **Reasoning**: Analyze the task and plan steps
12
+ # 2. **Acting**: Execute tools based on the plan
13
+ # 3. **Observing**: Evaluate results and adjust plan
14
+ # 4. **Repeating**: Continue until task completion
15
+ #
16
+ # @example Basic usage
17
+ # agent = ReActAgent.new(
18
+ # model: "qwen3:1.7b",
19
+ # tools: ToolManagerFactory.create_default_toolset,
20
+ # max_iterations: 5
21
+ # )
22
+ #
23
+ # result = agent.run("Find the weather in Moscow and calculate the average temperature for the week")
24
+ class ReActAgent < LLMChain::Interfaces::Agent
25
+
26
+ attr_reader :model, :tools, :memory, :max_iterations, :client
27
+
28
+ # Initialize ReAct agent
29
+ # @param model [String] LLM model identifier
30
+ # @param tools [LLMChain::Interfaces::ToolManager] tool manager
31
+ # @param memory [LLMChain::Interfaces::Memory] memory backend
32
+ # @param max_iterations [Integer] maximum reasoning iterations
33
+ # @param client_options [Hash] additional client options
34
+ def initialize(model:, tools:, memory: nil, max_iterations: 3, **client_options)
35
+ @model = model
36
+ @tools = tools
37
+ @memory = memory || LLMChain::Memory::Array.new
38
+ @max_iterations = max_iterations
39
+ @client = LLMChain::ClientRegistry.client_for(@model, **client_options)
40
+ end
41
+
42
+ # Execute a task using ReAct methodology
43
+ # @param task [String] the task to accomplish
44
+ # @param stream [Boolean] whether to stream reasoning steps
45
+ # @yield [Hash] reasoning step information
46
+ # @return [Hash] final result with reasoning trace
47
+ def run(task, stream: false, &block)
48
+ # Special case: if asking about available tools, return immediately
49
+ if task.downcase.include?("tools") && (task.downcase.include?("available") || task.downcase.include?("which"))
50
+ return {
51
+ task: task,
52
+ final_answer: "Available tools: #{@tools.list_tools.map(&:name).join(', ')}",
53
+ reasoning_trace: [],
54
+ iterations: 0,
55
+ success: true
56
+ }
57
+ end
58
+
59
+ reasoning_trace = []
60
+ current_state = { task: task, observations: [] }
61
+ failed_actions = {} # Track failed actions to avoid repetition
62
+
63
+ @max_iterations.times do |iteration|
64
+ # Step 1: Reasoning - Analyze current state and plan next action
65
+ reasoning_step = reason(current_state, reasoning_trace, failed_actions)
66
+ reasoning_trace << reasoning_step
67
+
68
+ yield reasoning_step if block_given? && stream
69
+
70
+ # Step 2: Acting - Execute planned action
71
+ action_result = act(reasoning_step)
72
+ reasoning_trace.last[:action_result] = action_result
73
+
74
+ # Step 3: Observing - Update state with results
75
+ current_state[:observations] << action_result
76
+
77
+ # Track failed actions
78
+ if !action_result[:success] || action_result[:formatted].include?("error")
79
+ action_key = "#{reasoning_step[:action]}:#{reasoning_step[:action_input]}"
80
+ failed_actions[action_key] = (failed_actions[action_key] || 0) + 1
81
+ end
82
+
83
+ # Step 4: Check if task is complete
84
+ if reasoning_step[:thought].include?("FINAL ANSWER") ||
85
+ reasoning_step[:thought].include?("Task completed") ||
86
+ (action_result[:success] && !action_result[:formatted].include?("error") &&
87
+ action_result[:formatted].length > 100 && !action_result[:formatted].include?("timezone"))
88
+ break
89
+ end
90
+
91
+ # Stop if too many failures
92
+ if failed_actions.values.any? { |count| count >= 3 }
93
+ break
94
+ end
95
+ end
96
+
97
+ # Extract final answer from reasoning trace
98
+ final_answer = extract_final_answer(reasoning_trace)
99
+
100
+ {
101
+ task: task,
102
+ final_answer: final_answer,
103
+ reasoning_trace: reasoning_trace,
104
+ iterations: reasoning_trace.length,
105
+ success: !final_answer.nil? && !final_answer.empty? && !final_answer.include?("error")
106
+ }
107
+ end
108
+
109
+ # Check if this agent can handle the given task
110
+ # @param task [String] task description
111
+ # @return [Boolean] whether this agent can handle the task
112
+ def can_handle?(task)
113
+ # ReAct agent can handle complex multi-step tasks
114
+ task.include?("analyze") ||
115
+ task.include?("find") && task.include?("and") ||
116
+ task.include?("calculate") && task.include?("and") ||
117
+ task.length > 50 # Long tasks likely need reasoning
118
+ end
119
+
120
+ # Get description of agent capabilities
121
+ # @return [String] agent description
122
+ def description
123
+ "ReAct agent with reasoning and acting capabilities for complex multi-step tasks"
124
+ end
125
+
126
+ private
127
+
128
+ # Generate reasoning step based on current state
129
+ # @param state [Hash] current state including task and observations
130
+ # @param trace [Array] previous reasoning steps
131
+ # @param failed_actions [Hash] tracking of failed actions
132
+ # @return [Hash] reasoning step with thought and action
133
+ def reason(state, trace, failed_actions)
134
+ prompt = build_reasoning_prompt(state, trace, failed_actions)
135
+ response = @client.chat(prompt)
136
+ parsed = parse_reasoning_response(response)
137
+
138
+ {
139
+ iteration: trace.length + 1,
140
+ thought: parsed[:thought],
141
+ action: parsed[:action],
142
+ action_input: parsed[:action_input],
143
+ timestamp: Time.now
144
+ }
145
+ end
146
+
147
+ # Execute the planned action using available tools
148
+ # @param reasoning_step [Hash] the reasoning step with action details
149
+ # @return [Hash] action execution result
150
+ def act(reasoning_step)
151
+ action = reasoning_step[:action]
152
+ action_input = reasoning_step[:action_input]
153
+
154
+ return { success: false, error: "No action specified" } unless action
155
+
156
+ tool = @tools.get_tool(action)
157
+ return { success: false, error: "Tool '#{action}' not found" } unless tool
158
+
159
+ begin
160
+ result = tool.call(action_input)
161
+ {
162
+ success: true,
163
+ tool: action,
164
+ input: action_input,
165
+ result: result,
166
+ formatted: tool.format_result(result)
167
+ }
168
+ rescue => e
169
+ {
170
+ success: false,
171
+ tool: action,
172
+ input: action_input,
173
+ error: e.message
174
+ }
175
+ end
176
+ end
177
+
178
+ # Build prompt for reasoning step
179
+ # @param state [Hash] current state
180
+ # @param trace [Array] reasoning trace
181
+ # @param failed_actions [Hash] tracking of failed actions
182
+ # @return [String] formatted prompt
183
+ def build_reasoning_prompt(state, trace, failed_actions)
184
+ tools_description = @tools.tools_description
185
+ observations = state[:observations].map { |obs| obs[:formatted] }.join("\n")
186
+
187
+ failed_actions_info = if failed_actions.any?
188
+ "Failed actions (avoid repeating):\n" +
189
+ failed_actions.map { |action, count| " #{action} (failed #{count} times)" }.join("\n")
190
+ else
191
+ "No failed actions yet"
192
+ end
193
+
194
+ <<~PROMPT
195
+ You are a ReAct agent. Your task is: #{state[:task]}
196
+
197
+ Available tools:
198
+ #{tools_description}
199
+
200
+ Previous observations:
201
+ #{observations.empty? ? "None" : observations}
202
+
203
+ #{failed_actions_info}
204
+
205
+ Instructions:
206
+ - For calculations: Use calculator tool, then provide FINAL ANSWER
207
+ - For web searches: Use web_search tool, maximize requests as much as possible to get the most relevant information
208
+ - For dates and time: Use date_time tool with empty input or timezone name (e.g., "Moscow", "New York")
209
+ - For multiple timezones: Use date_time tool multiple times, once for each timezone
210
+ - For current information searches: ALWAYS first use date_time tool to get current date, then use web_search with current year
211
+ - For questions about current subjects, or recent events: First get current date with date_time, then search
212
+ - If you need to pass a date to web_search, use date_time tool to get the date in the correct format
213
+ - For code: Use code_interpreter tool
214
+ - For multi-step tasks (with "and"): Complete ALL steps before FINAL ANSWER
215
+ - After getting a good result, provide FINAL ANSWER
216
+ - Don't repeat failed actions
217
+ - Summarize the result in a few words
218
+
219
+ IMPORTANT: When searching for current information (recent events),
220
+ you MUST first use date_time tool to get the current year, then include that year in your web_search query.
221
+
222
+ Format:
223
+ Thought: [brief reasoning]
224
+ Action: [tool_name]
225
+ Action Input: [input]
226
+
227
+ Or when done:
228
+ Thought: [reasoning]
229
+ FINAL ANSWER: [answer]
230
+ PROMPT
231
+ end
232
+
233
+ # Format reasoning trace for prompt
234
+ # @param trace [Array] reasoning trace
235
+ # @return [String] formatted trace
236
+ def format_reasoning_trace(trace)
237
+ return "None" if trace.empty?
238
+
239
+ trace.map do |step|
240
+ "Step #{step[:iteration]}: #{step[:thought]}"
241
+ end.join("\n")
242
+ end
243
+
244
+ # Parse LLM response to extract reasoning components
245
+ # @param response [String] LLM response
246
+ # @return [Hash] parsed thought, action, and action_input
247
+ def parse_reasoning_response(response)
248
+ # Try to extract structured response
249
+ if response.include?("Thought:") && response.include?("Action:")
250
+ thought_match = response.match(/Thought:\s*(.*?)(?=Action:|$)/m)
251
+ action_match = response.match(/Action:\s*(\w+)/)
252
+ input_match = response.match(/Action Input:\s*(.*?)(?=\n|$)/m)
253
+
254
+ action_input = input_match&.[](1)&.strip
255
+
256
+ # Clean up action input - remove markdown code blocks if present
257
+ if action_input&.start_with?("```")
258
+ action_input = action_input.gsub(/^```\w*\n/, "").gsub(/\n```$/, "")
259
+ end
260
+
261
+ {
262
+ thought: thought_match&.[](1)&.strip || response,
263
+ action: action_match&.[](1),
264
+ action_input: action_input
265
+ }
266
+ else
267
+ # Fallback: treat entire response as thought
268
+ {
269
+ thought: response,
270
+ action: nil,
271
+ action_input: nil
272
+ }
273
+ end
274
+ end
275
+
276
+ # Extract final answer from reasoning trace
277
+ # @param trace [Array] reasoning trace
278
+ # @return [String] final answer
279
+ def extract_final_answer(trace)
280
+ # Look for FINAL ANSWER in the last reasoning step
281
+ last_step = trace.last
282
+ return nil unless last_step
283
+
284
+ thought = last_step[:thought]
285
+ if thought.include?("FINAL ANSWER:")
286
+ thought.match(/FINAL ANSWER:\s*(.*)/m)&.[](1)&.strip
287
+ elsif last_step[:action_result]&.[](:success)
288
+ result = last_step[:action_result][:formatted]
289
+ # If result is JSON with error, try to get a better answer
290
+ if result.include?("error")
291
+ # Look for successful results in previous steps
292
+ trace.reverse.each do |step|
293
+ if step[:action_result]&.[](:success) && !step[:action_result][:formatted].include?("error")
294
+ return step[:action_result][:formatted]
295
+ end
296
+ end
297
+ return "Unable to complete task after #{trace.length} attempts"
298
+ else
299
+ result
300
+ end
301
+ else
302
+ thought
303
+ end
304
+ end
305
+ end
306
+ end
307
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'interfaces/agent'
4
+ require_relative 'agents/agent_factory'
5
+ require_relative 'agents/react_agent'
6
+ require_relative 'agents/planner_agent'
7
+ require_relative 'agents/composite_agent'
8
+
9
+ module LLMChain
10
+ module Agents
11
+ # Register built-in agents
12
+ AgentFactory.register(:react, ReActAgent, description: "ReAct agent with reasoning and acting capabilities")
13
+ AgentFactory.register(:planner, PlannerAgent, description: "Planner agent that decomposes complex tasks into atomic steps.")
14
+ AgentFactory.register(:composite, CompositeAgent, description: "Composite agent with planning and execution capabilities for complex multi-step tasks")
15
+
16
+ # Add more built-in agents here as they are implemented
17
+ # AgentFactory.register(:tool_using, ToolUsingAgent, "Simple tool-using agent")
18
+ end
19
+ end
@@ -4,6 +4,7 @@ require 'json'
4
4
  module LLMChain
5
5
  module Clients
6
6
  class Gemma3 < OllamaBase
7
+ class InvalidModelVersion < StandardError; end unless const_defined?(:InvalidModelVersion)
7
8
  # Доступные версии моделей Gemma3
8
9
  MODEL_VERSIONS = {
9
10
  gemma3: {
@@ -135,9 +136,10 @@ module LLMChain
135
136
 
136
137
  def clean_response(text)
137
138
  tags = INTERNAL_TAGS[:common].merge(INTERNAL_TAGS[model_version] || {})
138
- tags.values.reduce(text) do |processed, regex|
139
- processed.gsub(regex, '')
140
- end.gsub(/\n{3,}/, "\n\n").strip
139
+ processed = tags.values.reduce(text) do |acc, regex|
140
+ acc.gsub(regex, "\n")
141
+ end
142
+ processed.gsub(/\n{2,}/, "\n\n").strip
141
143
  end
142
144
  end
143
145
  end
@@ -21,7 +21,7 @@ module LLMChain
21
21
  @default_options = DEFAULT_OPTIONS.merge(options)
22
22
  end
23
23
 
24
- def chat(messages, stream: false, **options)
24
+ def chat(messages, stream: false, **options, &block)
25
25
  params = build_request_params(messages, stream: stream, **options)
26
26
 
27
27
  if stream
@@ -4,6 +4,7 @@ require 'json'
4
4
  module LLMChain
5
5
  module Clients
6
6
  class Qwen < OllamaBase
7
+ class InvalidModelVersion < StandardError; end unless defined?(InvalidModelVersion)
7
8
  # Доступные версии моделей
8
9
  MODEL_VERSIONS = {
9
10
  qwen: {
@@ -47,7 +48,11 @@ module LLMChain
47
48
  reasoning: /<reasoning>.*?<\/reasoning>\s*/mi
48
49
  },
49
50
  qwen: {
50
- system: /<\|system\|>.*?<\|im_end\|>\s*/mi
51
+ system: /<\|system\|>.*?<\|im_end\|>\s*/mi,
52
+ qwen_meta: /<qwen_meta>.*?<\/qwen_meta>\s*/mi
53
+ },
54
+ qwen2: {
55
+ qwen_meta: /<qwen_meta>.*?<\/qwen_meta>\s*/mi
51
56
  },
52
57
  qwen3: {
53
58
  qwen_meta: /<qwen_meta>.*?<\/qwen_meta>\s*/mi
@@ -55,11 +60,9 @@ module LLMChain
55
60
  }.freeze
56
61
 
57
62
  def initialize(model: nil, base_url: nil, **options)
58
- model ||= detect_default_model
59
-
63
+ model ||= detect_default_model_from(model)
60
64
  @model = model
61
65
  validate_model_version(@model)
62
-
63
66
  super(
64
67
  model: @model,
65
68
  base_url: base_url,
@@ -105,18 +108,22 @@ module LLMChain
105
108
 
106
109
  private
107
110
 
111
+ def model_version_for(model)
112
+ return :qwen3 if model&.start_with?('qwen3:')
113
+ return :qwen2 if model&.start_with?('qwen2:')
114
+ :qwen
115
+ end
116
+
108
117
  def model_version
109
- if @model.start_with?('qwen3:')
110
- :qwen3
111
- elsif @model.start_with?('qwen2:')
112
- :qwen2
113
- else
114
- :qwen
115
- end
118
+ model_version_for(@model)
119
+ end
120
+
121
+ def detect_default_model_from(model)
122
+ MODEL_VERSIONS[model_version_for(model)][:default]
116
123
  end
117
124
 
118
125
  def detect_default_model
119
- MODEL_VERSIONS[model_version][:default]
126
+ detect_default_model_from(nil)
120
127
  end
121
128
 
122
129
  def validate_model_version(model)
@@ -151,9 +158,14 @@ module LLMChain
151
158
 
152
159
  def clean_response(text)
153
160
  tags = INTERNAL_TAGS[:common].merge(INTERNAL_TAGS[model_version] || {})
154
- tags.values.reduce(text) do |processed, regex|
155
- processed.gsub(regex, '')
156
- end.gsub(/\n{3,}/, "\n\n").strip
161
+ # Добавляем <|system|>...<|im_end|> для qwen3, если не было
162
+ if model_version == :qwen3 && !tags.key?(:system)
163
+ tags[:system] = /<\|system\|>.*?<\|im_end\|>\s*/mi
164
+ end
165
+ processed = tags.values.reduce(text) do |acc, regex|
166
+ acc.gsub(regex, "\n")
167
+ end
168
+ processed.gsub(/\n{2,}/, "\n\n").strip
157
169
  end
158
170
  end
159
171
  end
@@ -62,6 +62,8 @@ module LLMChain
62
62
  else
63
63
  add_warning("Unknown model type: #{model}. Proceeding with default settings.")
64
64
  end
65
+
66
+ validate_client_availability!(model)
65
67
  end
66
68
 
67
69
  def validate_openai_requirements!(model)
@@ -97,6 +99,8 @@ module LLMChain
97
99
  else
98
100
  add_warning("OpenAI API returned status #{response.code}. Service may be temporarily unavailable.")
99
101
  end
102
+ rescue ValidationError
103
+ raise
100
104
  rescue => e
101
105
  add_warning("Cannot verify OpenAI API availability: #{e.message}")
102
106
  end
@@ -300,8 +304,8 @@ module LLMChain
300
304
 
301
305
  def check_python_availability
302
306
  begin
303
- output = `python3 --version 2>&1`
304
- $?.success? && output.include?('Python')
307
+ out, status = Open3.capture2('python3 --version')
308
+ status.success? && out.include?('Python')
305
309
  rescue
306
310
  false
307
311
  end
@@ -309,8 +313,8 @@ module LLMChain
309
313
 
310
314
  def check_node_availability
311
315
  begin
312
- output = `node --version 2>&1`
313
- $?.success? && output.include?('v')
316
+ out, status = Open3.capture2('node --version')
317
+ status.success? && out.include?('v')
314
318
  rescue
315
319
  false
316
320
  end
@@ -318,9 +322,8 @@ module LLMChain
318
322
 
319
323
  def check_internet_connectivity
320
324
  begin
321
- require 'socket'
322
- Socket.tcp("8.8.8.8", 53, connect_timeout: 3) {}
323
- true
325
+ resp = Net::HTTP.get_response(URI('https://www.google.com'))
326
+ resp.code == '200'
324
327
  rescue
325
328
  false
326
329
  end
@@ -335,6 +338,7 @@ module LLMChain
335
338
  end
336
339
 
337
340
  def add_warning(message)
341
+ @warnings ||= []
338
342
  @warnings << message
339
343
  end
340
344
 
@@ -55,6 +55,8 @@ module LLMChain
55
55
  def parse_response(response)
56
56
  data = JSON.parse(response.body)
57
57
  data['embedding'] or raise EmbeddingError, "No embedding in response"
58
+ rescue JSON::ParserError => e
59
+ raise EmbeddingError, "Invalid JSON: #{e.message}"
58
60
  end
59
61
 
60
62
  class EmbeddingError < StandardError; end
@@ -35,12 +35,12 @@ module LLMChain
35
35
 
36
36
  # Поиск по семантическому сходству
37
37
  def semantic_search(query, limit: 3, certainty: 0.7)
38
- near_vector = "{ vector: #{@embedder.embed(query)}, certainty: 0.7 }"
38
+ near_vector = "{ vector: #{@embedder.embed(query)}, certainty: #{certainty} }"
39
39
 
40
40
  @client.query.get(
41
41
  class_name: @class_name,
42
42
  fields: "content metadata text",
43
- limit: "1",
43
+ limit: limit.to_s,
44
44
  offset: "1",
45
45
  near_vector: near_vector,
46
46
  )
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLMChain
4
+ module Interfaces
5
+ # Abstract interface for all LLMChain agents
6
+ #
7
+ # All agents must implement the run method and provide access to their
8
+ # model, tools, and memory. The constructor is left to individual implementations
9
+ # to allow for flexibility in parameter requirements.
10
+ #
11
+ # @abstract
12
+ class Agent
13
+ # Execute a task using the agent's capabilities
14
+ # @param task [String] the task to accomplish
15
+ # @param stream [Boolean] whether to stream reasoning steps
16
+ # @yield [Hash] reasoning step information (when streaming)
17
+ # @return [Hash] execution result with reasoning trace
18
+ def run(task, stream: false, &block)
19
+ raise NotImplementedError, "Implement in subclass"
20
+ end
21
+
22
+ # Get the model identifier used by this agent
23
+ # @return [String] model name
24
+ def model
25
+ raise NotImplementedError, "Implement in subclass"
26
+ end
27
+
28
+ # Get the tool manager available to this agent
29
+ # @return [LLMChain::Interfaces::ToolManager] tool manager
30
+ def tools
31
+ raise NotImplementedError, "Implement in subclass"
32
+ end
33
+
34
+ # Get the memory system used by this agent
35
+ # @return [LLMChain::Interfaces::Memory] memory backend
36
+ def memory
37
+ raise NotImplementedError, "Implement in subclass"
38
+ end
39
+
40
+ # Check if the agent can handle the given task
41
+ # @param task [String] task description
42
+ # @return [Boolean] whether the agent can handle this task
43
+ def can_handle?(task)
44
+ raise NotImplementedError, "Implement in subclass"
45
+ end
46
+
47
+ # Get a description of the agent's capabilities
48
+ # @return [String] agent description
49
+ def description
50
+ raise NotImplementedError, "Implement in subclass"
51
+ end
52
+ end
53
+ end
54
+ end