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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -2
- data/README.md +104 -14
- data/examples/composite_agent_example.rb +121 -0
- data/examples/planner_agent_example.rb +103 -0
- data/examples/quick_demo.rb +0 -6
- data/examples/react_agent_example.rb +64 -0
- data/examples/tools_example.rb +1 -163
- data/lib/llm_chain/agents/agent_factory.rb +120 -0
- data/lib/llm_chain/agents/composite_agent.rb +341 -0
- data/lib/llm_chain/agents/planner_agent.rb +108 -0
- data/lib/llm_chain/agents/react_agent.rb +307 -0
- data/lib/llm_chain/agents.rb +19 -0
- data/lib/llm_chain/clients/gemma3.rb +5 -3
- data/lib/llm_chain/clients/openai.rb +1 -1
- data/lib/llm_chain/clients/qwen.rb +27 -15
- data/lib/llm_chain/configuration_validator.rb +11 -7
- data/lib/llm_chain/embeddings/clients/local/ollama_client.rb +2 -0
- data/lib/llm_chain/embeddings/clients/local/weaviate_vector_store.rb +2 -2
- data/lib/llm_chain/interfaces/agent.rb +54 -0
- data/lib/llm_chain/tools/code_interpreter.rb +10 -3
- data/lib/llm_chain/tools/date_time.rb +63 -7
- data/lib/llm_chain/tools/web_search.rb +83 -22
- data/lib/llm_chain/version.rb +1 -1
- data/lib/llm_chain.rb +1 -0
- metadata +114 -24
@@ -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 |
|
139
|
-
|
140
|
-
end
|
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 ||=
|
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
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
-
|
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
|
-
|
155
|
-
|
156
|
-
|
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
|
-
|
304
|
-
|
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
|
-
|
313
|
-
|
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
|
-
|
322
|
-
|
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:
|
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:
|
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
|