llm_chain 0.3.0 → 0.4.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/README.md +3 -1
- data/lib/llm_chain/chain.rb +42 -40
- data/lib/llm_chain/client_registry.rb +2 -0
- data/lib/llm_chain/clients/gemma3.rb +144 -0
- data/lib/llm_chain/clients/qwen.rb +1 -2
- data/lib/llm_chain/version.rb +1 -1
- data/lib/llm_chain.rb +1 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8edbb57db3e5fe7b44c7e4ebec1e026dea81bc1e30fc140e9ac35549b5c862e8
|
4
|
+
data.tar.gz: 2c1dd32185e96ea57bdae128934726b556bfbaaa8a913cb0062d53021cf7df2f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aa19f25581b568ca3cd2f5d9569afd0f8beab706f56ce63b043a6bd81965a9386efb4b27aff0563bc9530576aaa836a5db092864f852962c47ea5f19fb859880
|
7
|
+
data.tar.gz: 4d38b4daf714d28b298a41c555add9739faee9cdf7979de52b3ff944f2ed275694aa597f1faafed21fb844002b1915591ef043bcd6686483d931a59128ca62e7
|
data/README.md
CHANGED
@@ -46,7 +46,9 @@ basic example:
|
|
46
46
|
require 'llm_chain'
|
47
47
|
|
48
48
|
memory = LLMChain::Memory::Array.new(max_size: 1)
|
49
|
-
chain = LLMChain::Chain.new(model: "qwen3:1.7b", memory: memory)
|
49
|
+
chain = LLMChain::Chain.new(model: "qwen3:1.7b", memory: memory, retriever: false)
|
50
|
+
# retriever: false is required when you don't use a vector database to store context or external data
|
51
|
+
# reitriever: - is set to WeaviateRetriever.new as default so you need to pass an external params to set Weaviate host
|
50
52
|
puts chain.ask("What is 2+2?")
|
51
53
|
```
|
52
54
|
|
data/lib/llm_chain/chain.rb
CHANGED
@@ -13,7 +13,13 @@ module LLMChain
|
|
13
13
|
@model = model
|
14
14
|
@memory = memory || Memory::Array.new
|
15
15
|
@tools = tools
|
16
|
-
@retriever = retriever
|
16
|
+
@retriever = if retriever.nil?
|
17
|
+
Embeddings::Clients::Local::WeaviateRetriever.new
|
18
|
+
elsif retriever == false
|
19
|
+
nil
|
20
|
+
else
|
21
|
+
retriever
|
22
|
+
end
|
17
23
|
@client = ClientRegistry.client_for(model, **client_options)
|
18
24
|
end
|
19
25
|
|
@@ -24,27 +30,20 @@ module LLMChain
|
|
24
30
|
# @param rag_options [Hash] Опции для RAG-поиска
|
25
31
|
# @yield [String] Передает чанки ответа если stream=true
|
26
32
|
def ask(prompt, stream: false, rag_context: false, rag_options: {}, &block)
|
27
|
-
|
28
|
-
|
29
|
-
tool_responses = process_tools(prompt)
|
30
|
-
rag_documents = retrieve_rag_context(prompt, rag_options) if rag_context
|
31
|
-
|
32
|
-
# 2. Построение промпта
|
33
|
-
full_prompt = build_prompt(
|
34
|
-
prompt: prompt,
|
35
|
-
memory_context: context,
|
36
|
-
tool_responses: tool_responses,
|
37
|
-
rag_documents: rag_documents
|
38
|
-
)
|
39
|
-
|
40
|
-
# 3. Генерация ответа
|
33
|
+
context = collect_context(prompt, rag_context, rag_options)
|
34
|
+
full_prompt = build_prompt(prompt: prompt, **context)
|
41
35
|
response = generate_response(full_prompt, stream: stream, &block)
|
42
|
-
|
43
|
-
# 4. Сохранение в память
|
44
36
|
memory.store(prompt, response)
|
45
37
|
response
|
46
38
|
end
|
47
39
|
|
40
|
+
def collect_context(prompt, rag_context, rag_options)
|
41
|
+
context = memory.recall(prompt)
|
42
|
+
tool_responses = process_tools(prompt)
|
43
|
+
rag_documents = retrieve_rag_context(prompt, rag_options) if rag_context
|
44
|
+
{ memory_context: context, tool_responses: tool_responses, rag_documents: rag_documents }
|
45
|
+
end
|
46
|
+
|
48
47
|
private
|
49
48
|
|
50
49
|
def retrieve_rag_context(query, options = {})
|
@@ -53,8 +52,7 @@ module LLMChain
|
|
53
52
|
limit = options[:limit] || 3
|
54
53
|
@retriever.search(query, limit: limit)
|
55
54
|
rescue => e
|
56
|
-
|
57
|
-
[]
|
55
|
+
raise Error, "Cannot retrieve rag context"
|
58
56
|
end
|
59
57
|
|
60
58
|
def process_tools(prompt)
|
@@ -68,33 +66,37 @@ module LLMChain
|
|
68
66
|
|
69
67
|
def build_prompt(prompt:, memory_context: nil, tool_responses: {}, rag_documents: nil)
|
70
68
|
parts = []
|
69
|
+
parts << build_memory_context(memory_context) if memory_context&.any?
|
70
|
+
parts << build_rag_documents(rag_documents) if rag_documents&.any?
|
71
|
+
parts << build_tool_responses(tool_responses) unless tool_responses.empty?
|
72
|
+
parts << "Сurrent question: #{prompt}"
|
73
|
+
parts.join("\n\n")
|
74
|
+
end
|
71
75
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
end
|
76
|
+
def build_memory_context(memory_context)
|
77
|
+
parts = ["Dialogue history:"]
|
78
|
+
memory_context.each do |item|
|
79
|
+
parts << "User: #{item[:prompt]}"
|
80
|
+
parts << "Assistant: #{item[:response]}"
|
78
81
|
end
|
82
|
+
parts.join("\n")
|
83
|
+
end
|
79
84
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
end
|
85
|
+
def build_rag_documents(rag_documents)
|
86
|
+
parts = ["Relevant documents:"]
|
87
|
+
rag_documents.each_with_index do |doc, i|
|
88
|
+
parts << "Document #{i + 1}: #{doc['content']}"
|
89
|
+
parts << "Metadata: #{doc['metadata'].to_json}" if doc['metadata']
|
86
90
|
end
|
91
|
+
parts.join("\n")
|
92
|
+
end
|
87
93
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
end
|
94
|
+
def build_tool_responses(tool_responses)
|
95
|
+
parts = ["Tool results:"]
|
96
|
+
tool_responses.each do |name, response|
|
97
|
+
parts << "#{name}: #{response}"
|
93
98
|
end
|
94
|
-
|
95
|
-
parts << "Qurrent question: #{prompt}"
|
96
|
-
|
97
|
-
parts.join("\n\n")
|
99
|
+
parts.join("\n")
|
98
100
|
end
|
99
101
|
|
100
102
|
def generate_response(prompt, stream: false, &block)
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module LLMChain
|
5
|
+
module Clients
|
6
|
+
class Gemma3 < OllamaBase
|
7
|
+
# Доступные версии моделей Gemma3
|
8
|
+
MODEL_VERSIONS = {
|
9
|
+
gemma3: {
|
10
|
+
default: "gemma3:2b",
|
11
|
+
versions: [
|
12
|
+
"gemma3:2b", "gemma3:8b", "gemma3:27b",
|
13
|
+
"gemma3:2b-instruct", "gemma3:8b-instruct", "gemma3:27b-instruct", "gemma3:4b"
|
14
|
+
]
|
15
|
+
}
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
# Общие настройки по умолчанию для Gemma3
|
19
|
+
COMMON_DEFAULT_OPTIONS = {
|
20
|
+
temperature: 0.7,
|
21
|
+
top_p: 0.9,
|
22
|
+
top_k: 40,
|
23
|
+
repeat_penalty: 1.1,
|
24
|
+
num_ctx: 8192
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
# Специфичные настройки для разных версий
|
28
|
+
VERSION_SPECIFIC_OPTIONS = {
|
29
|
+
gemma3: {
|
30
|
+
stop: ["<|im_end|>", "<|endoftext|>", "<|user|>", "<|assistant|>"]
|
31
|
+
}
|
32
|
+
}.freeze
|
33
|
+
|
34
|
+
# Внутренние теги для очистки ответов
|
35
|
+
INTERNAL_TAGS = {
|
36
|
+
common: {
|
37
|
+
think: /<think>.*?<\/think>\s*/mi,
|
38
|
+
reasoning: /<reasoning>.*?<\/reasoning>\s*/mi
|
39
|
+
},
|
40
|
+
gemma3: {
|
41
|
+
system: /<\|system\|>.*?<\|im_end\|>\s*/mi,
|
42
|
+
user: /<\|user\|>.*?<\|im_end\|>\s*/mi,
|
43
|
+
assistant: /<\|assistant\|>.*?<\|im_end\|>\s*/mi
|
44
|
+
}
|
45
|
+
}.freeze
|
46
|
+
|
47
|
+
def initialize(model: nil, base_url: nil, **options)
|
48
|
+
model ||= detect_default_model
|
49
|
+
|
50
|
+
@model = model
|
51
|
+
validate_model_version(@model)
|
52
|
+
|
53
|
+
super(
|
54
|
+
model: @model,
|
55
|
+
base_url: base_url,
|
56
|
+
default_options: default_options_for(@model).merge(options)
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
60
|
+
def chat(prompt, show_internal: false, stream: false, **options, &block)
|
61
|
+
if stream
|
62
|
+
stream_chat(prompt, show_internal: show_internal, **options, &block)
|
63
|
+
else
|
64
|
+
response = super(prompt, **options)
|
65
|
+
process_response(response, show_internal: show_internal)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def stream_chat(prompt, show_internal: false, **options, &block)
|
70
|
+
buffer = ""
|
71
|
+
connection.post(API_ENDPOINT) do |req|
|
72
|
+
req.headers['Content-Type'] = 'application/json'
|
73
|
+
req.body = build_request_body(prompt, options.merge(stream: true))
|
74
|
+
|
75
|
+
req.options.on_data = Proc.new do |chunk, _bytes, _env|
|
76
|
+
processed = process_stream_chunk(chunk)
|
77
|
+
next unless processed
|
78
|
+
|
79
|
+
buffer << processed
|
80
|
+
block.call(processed) if block_given?
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
process_response(buffer, show_internal: show_internal)
|
85
|
+
end
|
86
|
+
|
87
|
+
protected
|
88
|
+
|
89
|
+
def build_request_body(prompt, options)
|
90
|
+
body = super
|
91
|
+
version_specific_options = VERSION_SPECIFIC_OPTIONS[model_version]
|
92
|
+
body[:options].merge!(version_specific_options) if version_specific_options
|
93
|
+
body
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def model_version
|
99
|
+
:gemma3
|
100
|
+
end
|
101
|
+
|
102
|
+
def detect_default_model
|
103
|
+
MODEL_VERSIONS[model_version][:default]
|
104
|
+
end
|
105
|
+
|
106
|
+
def validate_model_version(model)
|
107
|
+
valid_models = MODEL_VERSIONS.values.flat_map { |v| v[:versions] }
|
108
|
+
unless valid_models.include?(model)
|
109
|
+
raise InvalidModelVersion, "Invalid Gemma3 model version. Available: #{valid_models.join(', ')}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def default_options_for(model)
|
114
|
+
COMMON_DEFAULT_OPTIONS.merge(
|
115
|
+
VERSION_SPECIFIC_OPTIONS[model_version] || {}
|
116
|
+
)
|
117
|
+
end
|
118
|
+
|
119
|
+
def process_stream_chunk(chunk)
|
120
|
+
parsed = JSON.parse(chunk)
|
121
|
+
parsed["response"] if parsed.is_a?(Hash) && parsed["response"]
|
122
|
+
rescue JSON::ParserError
|
123
|
+
nil
|
124
|
+
end
|
125
|
+
|
126
|
+
def process_response(response, show_internal: false)
|
127
|
+
return response unless response.is_a?(String)
|
128
|
+
|
129
|
+
if show_internal
|
130
|
+
response
|
131
|
+
else
|
132
|
+
clean_response(response)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def clean_response(text)
|
137
|
+
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
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -92,9 +92,8 @@ module LLMChain
|
|
92
92
|
|
93
93
|
def build_request_body(prompt, options)
|
94
94
|
body = super
|
95
|
-
version_specific_options = VERSION_SPECIFIC_OPTIONS[model_version]
|
95
|
+
version_specific_options = VERSION_SPECIFIC_OPTIONS[model_version]
|
96
96
|
body[:options].merge!(version_specific_options) if version_specific_options
|
97
|
-
puts body
|
98
97
|
body
|
99
98
|
end
|
100
99
|
|
data/lib/llm_chain/version.rb
CHANGED
data/lib/llm_chain.rb
CHANGED
@@ -17,6 +17,7 @@ require "llm_chain/clients/ollama_base"
|
|
17
17
|
require "llm_chain/clients/openai"
|
18
18
|
require "llm_chain/clients/qwen"
|
19
19
|
require "llm_chain/clients/llama2"
|
20
|
+
require "llm_chain/clients/gemma3"
|
20
21
|
require "llm_chain/client_registry"
|
21
22
|
require "llm_chain/memory/array"
|
22
23
|
require "llm_chain/memory/redis"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: llm_chain
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- FuryCow
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-06-
|
11
|
+
date: 2025-06-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: httparty
|
@@ -125,6 +125,7 @@ files:
|
|
125
125
|
- lib/llm_chain/chain.rb
|
126
126
|
- lib/llm_chain/client_registry.rb
|
127
127
|
- lib/llm_chain/clients/base.rb
|
128
|
+
- lib/llm_chain/clients/gemma3.rb
|
128
129
|
- lib/llm_chain/clients/llama2.rb
|
129
130
|
- lib/llm_chain/clients/ollama_base.rb
|
130
131
|
- lib/llm_chain/clients/openai.rb
|
@@ -160,5 +161,5 @@ requirements: []
|
|
160
161
|
rubygems_version: 3.4.10
|
161
162
|
signing_key:
|
162
163
|
specification_version: 4
|
163
|
-
summary: Ruby
|
164
|
+
summary: Ruby-analog LangChain to work with LLM
|
164
165
|
test_files: []
|