llm_chain 0.5.0 → 0.5.2
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 +74 -0
- data/README.md +225 -26
- data/lib/llm_chain/chain.rb +22 -2
- data/lib/llm_chain/client_registry.rb +0 -1
- data/lib/llm_chain/configuration_validator.rb +349 -0
- data/lib/llm_chain/tools/calculator.rb +24 -13
- data/lib/llm_chain/tools/code_interpreter.rb +178 -18
- data/lib/llm_chain/tools/web_search.rb +344 -95
- data/lib/llm_chain/version.rb +1 -1
- data/lib/llm_chain.rb +107 -21
- metadata +4 -2
@@ -0,0 +1,349 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module LLMChain
|
6
|
+
class ConfigurationValidator
|
7
|
+
class ValidationError < Error; end
|
8
|
+
class ValidationWarning < StandardError; end
|
9
|
+
|
10
|
+
def self.validate_chain_config!(model: nil, **options)
|
11
|
+
new.validate_chain_config!(model: model, **options)
|
12
|
+
end
|
13
|
+
|
14
|
+
def validate_chain_config!(model: nil, **options)
|
15
|
+
@warnings = []
|
16
|
+
|
17
|
+
begin
|
18
|
+
validate_model!(model) if model
|
19
|
+
validate_client_availability!(model) if model
|
20
|
+
validate_tools!(options[:tools]) if options[:tools]
|
21
|
+
validate_memory!(options[:memory]) if options[:memory]
|
22
|
+
validate_retriever!(options[:retriever]) if options[:retriever]
|
23
|
+
|
24
|
+
# Выводим предупреждения, если есть
|
25
|
+
@warnings.each { |warning| warn_user(warning) } if @warnings.any?
|
26
|
+
|
27
|
+
true
|
28
|
+
rescue => e
|
29
|
+
raise ValidationError, "Configuration validation failed: #{e.message}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.validate_environment
|
34
|
+
new.validate_environment
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate_environment
|
38
|
+
@warnings = []
|
39
|
+
results = {}
|
40
|
+
|
41
|
+
results[:ollama] = check_ollama_availability
|
42
|
+
results[:ruby] = check_ruby_version
|
43
|
+
results[:python] = check_python_availability
|
44
|
+
results[:node] = check_node_availability
|
45
|
+
results[:internet] = check_internet_connectivity
|
46
|
+
results[:apis] = check_api_keys
|
47
|
+
|
48
|
+
results[:warnings] = @warnings
|
49
|
+
results
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def validate_model!(model)
|
55
|
+
return if model.nil?
|
56
|
+
|
57
|
+
case model.to_s
|
58
|
+
when /^gpt/
|
59
|
+
validate_openai_requirements!(model)
|
60
|
+
when /qwen|llama|gemma/
|
61
|
+
validate_ollama_requirements!(model)
|
62
|
+
else
|
63
|
+
add_warning("Unknown model type: #{model}. Proceeding with default settings.")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def validate_openai_requirements!(model)
|
68
|
+
api_key = ENV['OPENAI_API_KEY']
|
69
|
+
unless api_key
|
70
|
+
raise ValidationError, "OpenAI API key required for model '#{model}'. Set OPENAI_API_KEY environment variable."
|
71
|
+
end
|
72
|
+
|
73
|
+
if api_key.length < 20
|
74
|
+
raise ValidationError, "OpenAI API key appears to be invalid (too short)."
|
75
|
+
end
|
76
|
+
|
77
|
+
# Проверяем доступность OpenAI API
|
78
|
+
begin
|
79
|
+
uri = URI('https://api.openai.com/v1/models')
|
80
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
81
|
+
http.use_ssl = true
|
82
|
+
http.open_timeout = 5
|
83
|
+
http.read_timeout = 5
|
84
|
+
|
85
|
+
request = Net::HTTP::Get.new(uri)
|
86
|
+
request['Authorization'] = "Bearer #{api_key}"
|
87
|
+
|
88
|
+
response = http.request(request)
|
89
|
+
|
90
|
+
case response.code
|
91
|
+
when '200'
|
92
|
+
# OK
|
93
|
+
when '401'
|
94
|
+
raise ValidationError, "OpenAI API key is invalid or expired."
|
95
|
+
when '429'
|
96
|
+
add_warning("OpenAI API rate limit reached. Service may be temporarily unavailable.")
|
97
|
+
else
|
98
|
+
add_warning("OpenAI API returned status #{response.code}. Service may be temporarily unavailable.")
|
99
|
+
end
|
100
|
+
rescue => e
|
101
|
+
add_warning("Cannot verify OpenAI API availability: #{e.message}")
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def validate_ollama_requirements!(model)
|
106
|
+
unless check_ollama_availability
|
107
|
+
raise ValidationError, "Ollama is not running. Please start Ollama server with: ollama serve"
|
108
|
+
end
|
109
|
+
|
110
|
+
unless model_available_in_ollama?(model)
|
111
|
+
raise ValidationError, "Model '#{model}' not found in Ollama. Available models: #{list_ollama_models.join(', ')}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def validate_client_availability!(model)
|
116
|
+
case model.to_s
|
117
|
+
when /qwen|llama|gemma/
|
118
|
+
unless check_ollama_availability
|
119
|
+
raise ValidationError, "Ollama server is not running for model '#{model}'"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def validate_tools!(tools)
|
125
|
+
return unless tools
|
126
|
+
|
127
|
+
if tools.respond_to?(:tools) # ToolManager
|
128
|
+
tools.tools.each { |tool| validate_single_tool!(tool) }
|
129
|
+
elsif tools.is_a?(Array)
|
130
|
+
tools.each { |tool| validate_single_tool!(tool) }
|
131
|
+
else
|
132
|
+
validate_single_tool!(tools)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def validate_single_tool!(tool)
|
137
|
+
case tool.class.name
|
138
|
+
when /WebSearch/
|
139
|
+
validate_web_search_tool!(tool)
|
140
|
+
when /CodeInterpreter/
|
141
|
+
validate_code_interpreter_tool!(tool)
|
142
|
+
when /Calculator/
|
143
|
+
# Calculator не требует дополнительной валидации
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def validate_web_search_tool!(tool)
|
148
|
+
# Проверяем доступность Google Search API
|
149
|
+
if ENV['GOOGLE_API_KEY'] && ENV['GOOGLE_SEARCH_ENGINE_ID']
|
150
|
+
# Есть API ключи, но проверим их валидность
|
151
|
+
begin
|
152
|
+
# Простая проверка доступности
|
153
|
+
uri = URI('https://www.googleapis.com/customsearch/v1')
|
154
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
155
|
+
http.use_ssl = true
|
156
|
+
http.open_timeout = 3
|
157
|
+
http.read_timeout = 3
|
158
|
+
|
159
|
+
response = http.get('/')
|
160
|
+
# Если получили любой ответ, значит API доступен
|
161
|
+
rescue => e
|
162
|
+
add_warning("Google Search API may be unavailable: #{e.message}")
|
163
|
+
end
|
164
|
+
else
|
165
|
+
add_warning("Google Search API not configured. Search will use fallback methods.")
|
166
|
+
end
|
167
|
+
|
168
|
+
# Проверяем доступность интернета для fallback поиска
|
169
|
+
unless check_internet_connectivity
|
170
|
+
add_warning("No internet connection detected. Search functionality will be limited.")
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def validate_code_interpreter_tool!(tool)
|
175
|
+
# Проверяем доступность языков программирования
|
176
|
+
languages = tool.instance_variable_get(:@allowed_languages) || ['ruby']
|
177
|
+
|
178
|
+
languages.each do |lang|
|
179
|
+
case lang
|
180
|
+
when 'ruby'
|
181
|
+
unless check_ruby_version
|
182
|
+
add_warning("Ruby interpreter not found or outdated.")
|
183
|
+
end
|
184
|
+
when 'python'
|
185
|
+
unless check_python_availability
|
186
|
+
add_warning("Python interpreter not found.")
|
187
|
+
end
|
188
|
+
when 'javascript'
|
189
|
+
unless check_node_availability
|
190
|
+
add_warning("Node.js interpreter not found.")
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def validate_memory!(memory)
|
197
|
+
return unless memory
|
198
|
+
|
199
|
+
case memory.class.name
|
200
|
+
when /Redis/
|
201
|
+
validate_redis_memory!(memory)
|
202
|
+
when /Array/
|
203
|
+
# Array memory не требует дополнительной валидации
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def validate_redis_memory!(memory)
|
208
|
+
begin
|
209
|
+
# Проверяем подключение к Redis
|
210
|
+
redis_client = memory.instance_variable_get(:@redis) || memory.redis
|
211
|
+
if redis_client.respond_to?(:ping)
|
212
|
+
redis_client.ping
|
213
|
+
end
|
214
|
+
rescue => e
|
215
|
+
raise ValidationError, "Redis connection failed: #{e.message}"
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def validate_retriever!(retriever)
|
220
|
+
return unless retriever
|
221
|
+
return if retriever == false
|
222
|
+
|
223
|
+
case retriever.class.name
|
224
|
+
when /Weaviate/
|
225
|
+
validate_weaviate_retriever!(retriever)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def validate_weaviate_retriever!(retriever)
|
230
|
+
# Проверяем доступность Weaviate
|
231
|
+
begin
|
232
|
+
# Попытка подключения к Weaviate
|
233
|
+
uri = URI('http://localhost:8080/v1/.well-known/ready')
|
234
|
+
response = Net::HTTP.get_response(uri)
|
235
|
+
|
236
|
+
unless response.code == '200'
|
237
|
+
raise ValidationError, "Weaviate server is not ready. Please start Weaviate."
|
238
|
+
end
|
239
|
+
rescue => e
|
240
|
+
raise ValidationError, "Cannot connect to Weaviate: #{e.message}"
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# Вспомогательные методы для проверки системы
|
245
|
+
|
246
|
+
def check_ollama_availability
|
247
|
+
begin
|
248
|
+
uri = URI('http://localhost:11434/api/tags')
|
249
|
+
response = Net::HTTP.get_response(uri)
|
250
|
+
response.code == '200'
|
251
|
+
rescue
|
252
|
+
false
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def model_available_in_ollama?(model)
|
257
|
+
begin
|
258
|
+
uri = URI('http://localhost:11434/api/tags')
|
259
|
+
response = Net::HTTP.get_response(uri)
|
260
|
+
return false unless response.code == '200'
|
261
|
+
|
262
|
+
data = JSON.parse(response.body)
|
263
|
+
models = data['models'] || []
|
264
|
+
models.any? { |m| m['name'].include?(model.to_s.split(':').first) }
|
265
|
+
rescue
|
266
|
+
false
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def list_ollama_models
|
271
|
+
begin
|
272
|
+
uri = URI('http://localhost:11434/api/tags')
|
273
|
+
response = Net::HTTP.get_response(uri)
|
274
|
+
return [] unless response.code == '200'
|
275
|
+
|
276
|
+
data = JSON.parse(response.body)
|
277
|
+
models = data['models'] || []
|
278
|
+
models.map { |m| m['name'] }
|
279
|
+
rescue
|
280
|
+
[]
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
def check_ruby_version
|
285
|
+
begin
|
286
|
+
version = RUBY_VERSION
|
287
|
+
major, minor, patch = version.split('.').map(&:to_i)
|
288
|
+
|
289
|
+
# Требуем Ruby >= 3.1.0
|
290
|
+
if major > 3 || (major == 3 && minor >= 1)
|
291
|
+
true
|
292
|
+
else
|
293
|
+
add_warning("Ruby version #{version} detected. Minimum required: 3.1.0")
|
294
|
+
false
|
295
|
+
end
|
296
|
+
rescue
|
297
|
+
false
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
def check_python_availability
|
302
|
+
begin
|
303
|
+
output = `python3 --version 2>&1`
|
304
|
+
$?.success? && output.include?('Python')
|
305
|
+
rescue
|
306
|
+
false
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
def check_node_availability
|
311
|
+
begin
|
312
|
+
output = `node --version 2>&1`
|
313
|
+
$?.success? && output.include?('v')
|
314
|
+
rescue
|
315
|
+
false
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
def check_internet_connectivity
|
320
|
+
begin
|
321
|
+
require 'socket'
|
322
|
+
Socket.tcp("8.8.8.8", 53, connect_timeout: 3) {}
|
323
|
+
true
|
324
|
+
rescue
|
325
|
+
false
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
def check_api_keys
|
330
|
+
keys = {}
|
331
|
+
keys[:openai] = !ENV['OPENAI_API_KEY'].nil?
|
332
|
+
keys[:google_search] = !ENV['GOOGLE_API_KEY'].nil? && !ENV['GOOGLE_SEARCH_ENGINE_ID'].nil?
|
333
|
+
keys[:bing_search] = !ENV['BING_API_KEY'].nil?
|
334
|
+
keys
|
335
|
+
end
|
336
|
+
|
337
|
+
def add_warning(message)
|
338
|
+
@warnings << message
|
339
|
+
end
|
340
|
+
|
341
|
+
def warn_user(message)
|
342
|
+
if defined?(Rails) && Rails.logger
|
343
|
+
Rails.logger.warn "[LLMChain] #{message}"
|
344
|
+
else
|
345
|
+
warn "[LLMChain] Warning: #{message}"
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
@@ -66,6 +66,14 @@ module LLMChain
|
|
66
66
|
quoted = prompt.match(/"([^"]+)"/) || prompt.match(/'([^']+)'/)
|
67
67
|
return quoted[1] if quoted
|
68
68
|
|
69
|
+
# Пробуем найти простое выражение в тексте сначала (более точно)
|
70
|
+
math_expr = prompt.match(/(\d+(?:\.\d+)?\s*[+\-*\/]\s*\d+(?:\.\d+)?(?:\s*[+\-*\/]\s*\d+(?:\.\d+)?)*)/)
|
71
|
+
return math_expr[1].strip if math_expr
|
72
|
+
|
73
|
+
# Ищем функции
|
74
|
+
func_expr = prompt.match(/\b(sqrt|sin|cos|tan|log|ln|exp|abs|round|ceil|floor)\s*\([^)]+\)/i)
|
75
|
+
return func_expr[0] if func_expr
|
76
|
+
|
69
77
|
# Ищем выражение после ключевых слов
|
70
78
|
KEYWORDS.each do |keyword|
|
71
79
|
if prompt.downcase.include?(keyword)
|
@@ -74,27 +82,30 @@ module LLMChain
|
|
74
82
|
if after_keyword
|
75
83
|
# Извлекаем математическое выражение
|
76
84
|
expr = after_keyword.strip.split(/[.!?]/).first
|
77
|
-
|
85
|
+
if expr
|
86
|
+
cleaned = clean_expression(expr)
|
87
|
+
return cleaned unless cleaned.empty?
|
88
|
+
end
|
78
89
|
end
|
79
90
|
end
|
80
91
|
end
|
81
92
|
|
82
|
-
# Пробуем найти простое выражение в тексте
|
83
|
-
math_expr = prompt.match(/(\d+(?:\.\d+)?\s*[+\-*\/]\s*\d+(?:\.\d+)?(?:\s*[+\-*\/]\s*\d+(?:\.\d+)?)*)/)
|
84
|
-
return math_expr[1] if math_expr
|
85
|
-
|
86
|
-
# Ищем функции
|
87
|
-
func_expr = prompt.match(/\b(sqrt|sin|cos|tan|log|ln|exp|abs|round|ceil|floor)\s*\([^)]+\)/i)
|
88
|
-
return func_expr[0] if func_expr
|
89
|
-
|
90
93
|
""
|
91
94
|
end
|
92
95
|
|
93
96
|
def clean_expression(expr)
|
94
|
-
# Удаляем лишние слова
|
95
|
-
expr.gsub(/\b(is|what|equals?|result|answer)\b/i, '')
|
96
|
-
|
97
|
-
|
97
|
+
# Удаляем лишние слова но оставляем числа и операторы
|
98
|
+
cleaned = expr.gsub(/\b(is|what|equals?|result|answer|the)\b/i, '')
|
99
|
+
.gsub(/[^\d+\-*\/().\s]/, ' ') # заменяем на пробелы, не удаляем
|
100
|
+
.gsub(/\s+/, ' ') # убираем множественные пробелы
|
101
|
+
.strip
|
102
|
+
|
103
|
+
# Проверяем что результат похож на математическое выражение
|
104
|
+
if cleaned.match?(/\d+(?:\.\d+)?\s*[+\-*\/]\s*\d+(?:\.\d+)?/)
|
105
|
+
cleaned
|
106
|
+
else
|
107
|
+
""
|
108
|
+
end
|
98
109
|
end
|
99
110
|
|
100
111
|
def evaluate_expression(expression)
|
@@ -108,32 +108,192 @@ module LLMChain
|
|
108
108
|
end
|
109
109
|
|
110
110
|
def extract_code(prompt)
|
111
|
-
#
|
112
|
-
|
113
|
-
|
111
|
+
# Нормализуем line endings
|
112
|
+
normalized_prompt = normalize_line_endings(prompt)
|
113
|
+
|
114
|
+
# 1. Пробуем различные паттерны markdown блоков
|
115
|
+
code = extract_markdown_code_blocks(normalized_prompt)
|
116
|
+
return clean_code(code) if code && !code.empty?
|
117
|
+
|
118
|
+
# 2. Ищем код после ключевых команд в одной строке
|
119
|
+
code = extract_inline_code_commands(normalized_prompt)
|
120
|
+
return clean_code(code) if code && !code.empty?
|
121
|
+
|
122
|
+
# 3. Ищем код после ключевых слов в разных строках
|
123
|
+
code = extract_multiline_code_blocks(normalized_prompt)
|
124
|
+
return clean_code(code) if code && !code.empty?
|
125
|
+
|
126
|
+
# 4. Ищем строки, которые выглядят как код
|
127
|
+
code = extract_code_like_lines(normalized_prompt)
|
128
|
+
return clean_code(code) if code && !code.empty?
|
129
|
+
|
130
|
+
# 5. Последняя попытка - весь текст после первого кода
|
131
|
+
code = extract_fallback_code(normalized_prompt)
|
132
|
+
clean_code(code)
|
133
|
+
end
|
114
134
|
|
115
|
-
|
135
|
+
private
|
136
|
+
|
137
|
+
def normalize_line_endings(text)
|
138
|
+
text.gsub(/\r\n/, "\n").gsub(/\r/, "\n")
|
139
|
+
end
|
140
|
+
|
141
|
+
def extract_markdown_code_blocks(prompt)
|
142
|
+
# Различные паттерны для markdown блоков
|
143
|
+
patterns = [
|
144
|
+
# Стандартный markdown с указанием языка
|
145
|
+
/```(?:ruby|python|javascript|js)\s*\n(.*?)\n```/mi,
|
146
|
+
# Markdown без указания языка
|
147
|
+
/```\s*\n(.*?)\n```/mi,
|
148
|
+
# Markdown с любым языком
|
149
|
+
/```\w*\s*\n(.*?)\n```/mi,
|
150
|
+
# Тильды вместо backticks
|
151
|
+
/~~~(?:ruby|python|javascript|js)?\s*\n(.*?)\n~~~/mi,
|
152
|
+
# Без переносов строк
|
153
|
+
/```(?:ruby|python|javascript|js)?(.*?)```/mi,
|
154
|
+
# Четыре пробела (indented code blocks)
|
155
|
+
/^ (.+)$/m
|
156
|
+
]
|
157
|
+
|
158
|
+
patterns.each do |pattern|
|
159
|
+
match = prompt.match(pattern)
|
160
|
+
return match[1] if match && match[1].strip.length > 0
|
161
|
+
end
|
162
|
+
|
163
|
+
nil
|
164
|
+
end
|
165
|
+
|
166
|
+
def extract_inline_code_commands(prompt)
|
167
|
+
# Команды в одной строке
|
168
|
+
inline_patterns = [
|
169
|
+
/execute\s+code:\s*(.+)/i,
|
170
|
+
/run\s+code:\s*(.+)/i,
|
171
|
+
/run\s+this:\s*(.+)/i,
|
172
|
+
/execute:\s*(.+)/i,
|
173
|
+
/run:\s*(.+)/i,
|
174
|
+
/code:\s*(.+)/i
|
175
|
+
]
|
176
|
+
|
177
|
+
inline_patterns.each do |pattern|
|
178
|
+
match = prompt.match(pattern)
|
179
|
+
return match[1] if match && match[1].strip.length > 0
|
180
|
+
end
|
181
|
+
|
182
|
+
nil
|
183
|
+
end
|
184
|
+
|
185
|
+
def extract_multiline_code_blocks(prompt)
|
186
|
+
lines = prompt.split("\n")
|
187
|
+
|
116
188
|
KEYWORDS.each do |keyword|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
189
|
+
keyword_line_index = lines.find_index { |line| line.downcase.include?(keyword.downcase) }
|
190
|
+
next unless keyword_line_index
|
191
|
+
|
192
|
+
# Берем строки после ключевого слова
|
193
|
+
code_lines = lines[(keyword_line_index + 1)..-1]
|
194
|
+
next unless code_lines
|
195
|
+
|
196
|
+
# Найдем первую непустую строку
|
197
|
+
first_code_line = code_lines.find_index { |line| !line.strip.empty? }
|
198
|
+
next unless first_code_line
|
199
|
+
|
200
|
+
# Берем все строки начиная с первой непустой
|
201
|
+
relevant_lines = code_lines[first_code_line..-1]
|
202
|
+
|
203
|
+
# Определяем отступ первой строки кода
|
204
|
+
first_line = relevant_lines.first
|
205
|
+
indent = first_line.match(/^(\s*)/)[1].length
|
206
|
+
|
207
|
+
# Собираем все строки с таким же или большим отступом
|
208
|
+
code_block = []
|
209
|
+
relevant_lines.each do |line|
|
210
|
+
if line.strip.empty?
|
211
|
+
code_block << "" # Сохраняем пустые строки
|
212
|
+
elsif line.match(/^(\s*)/)[1].length >= indent
|
213
|
+
code_block << line
|
214
|
+
else
|
215
|
+
break # Прекращаем при уменьшении отступа
|
125
216
|
end
|
126
217
|
end
|
218
|
+
|
219
|
+
return code_block.join("\n") if code_block.any? { |line| !line.strip.empty? }
|
127
220
|
end
|
221
|
+
|
222
|
+
nil
|
223
|
+
end
|
128
224
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
line.strip
|
225
|
+
def extract_code_like_lines(prompt)
|
226
|
+
lines = prompt.split("\n")
|
227
|
+
|
228
|
+
code_lines = lines.select do |line|
|
229
|
+
stripped = line.strip
|
230
|
+
next false if stripped.empty?
|
231
|
+
|
232
|
+
# Проверяем различные паттерны кода
|
233
|
+
stripped.match?(/^(def|class|function|var|let|const|print|puts|console\.log)/i) ||
|
234
|
+
stripped.match?(/^\w+\s*[=+\-*\/]\s*/) ||
|
235
|
+
stripped.match?(/^\s*(if|for|while|return|import|require)[\s(]/i) ||
|
236
|
+
stripped.match?(/puts\s+/) ||
|
237
|
+
stripped.match?(/print\s*\(/) ||
|
238
|
+
stripped.match?(/^\w+\(.*\)/) ||
|
239
|
+
stripped.match?(/^\s*#.*/) || # Комментарии
|
240
|
+
stripped.match?(/^\s*\/\/.*/) || # JS комментарии
|
241
|
+
stripped.match?(/^\s*\/\*.*\*\//) # Блочные комментарии
|
134
242
|
end
|
243
|
+
|
244
|
+
code_lines.join("\n") if code_lines.any?
|
245
|
+
end
|
135
246
|
|
136
|
-
|
247
|
+
def extract_fallback_code(prompt)
|
248
|
+
# Последняя попытка - ищем что-то похожее на код
|
249
|
+
lines = prompt.split("\n")
|
250
|
+
|
251
|
+
# Найдем первую строку, которая выглядит как код
|
252
|
+
start_index = lines.find_index do |line|
|
253
|
+
stripped = line.strip
|
254
|
+
stripped.match?(/^(def|class|function|puts|print|console\.log|var|let|const)/i) ||
|
255
|
+
stripped.include?('=') ||
|
256
|
+
stripped.include?(';')
|
257
|
+
end
|
258
|
+
|
259
|
+
return nil unless start_index
|
260
|
+
|
261
|
+
# Берем все строки после найденной
|
262
|
+
code_lines = lines[start_index..-1]
|
263
|
+
|
264
|
+
# Останавливаемся на первой строке, которая явно не код
|
265
|
+
end_index = code_lines.find_index do |line|
|
266
|
+
stripped = line.strip
|
267
|
+
stripped.match?(/^(что|как|где|когда|зачем|почему|what|how|where|when|why)/i) ||
|
268
|
+
stripped.length > 100 # Слишком длинная строка
|
269
|
+
end
|
270
|
+
|
271
|
+
relevant_lines = end_index ? code_lines[0...end_index] : code_lines
|
272
|
+
relevant_lines.join("\n")
|
273
|
+
end
|
274
|
+
|
275
|
+
def clean_code(code)
|
276
|
+
return "" unless code
|
277
|
+
|
278
|
+
lines = code.strip.lines
|
279
|
+
|
280
|
+
# Удаляем только комментарии, которые не являются частью кода
|
281
|
+
cleaned_lines = lines.reject do |line|
|
282
|
+
stripped = line.strip
|
283
|
+
# Удаляем только строки, которые содержат ТОЛЬКО комментарии
|
284
|
+
stripped.match?(/^\s*#[^{]*$/) || # Ruby комментарии (но не интерполяция)
|
285
|
+
stripped.match?(/^\s*\/\/.*$/) || # JS комментарии
|
286
|
+
stripped.match?(/^\s*\/\*.*\*\/\s*$/) # Блочные комментарии
|
287
|
+
end
|
288
|
+
|
289
|
+
# Убираем пустые строки в начале и конце, но сохраняем внутри
|
290
|
+
start_index = cleaned_lines.find_index { |line| !line.strip.empty? }
|
291
|
+
return "" unless start_index
|
292
|
+
|
293
|
+
end_index = cleaned_lines.rindex { |line| !line.strip.empty? }
|
294
|
+
return "" unless end_index
|
295
|
+
|
296
|
+
cleaned_lines[start_index..end_index].join
|
137
297
|
end
|
138
298
|
|
139
299
|
def detect_language(code, prompt)
|