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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: efda7d777bfd7333f75b65ccc74d46abaff6c70e2a30a31709988811ed54cfff
4
- data.tar.gz: 4ea72695094eff0ee6d1a1acc029f66ea34be3a0052d39d33d70a9c6f5a1e1d7
3
+ metadata.gz: 8edbb57db3e5fe7b44c7e4ebec1e026dea81bc1e30fc140e9ac35549b5c862e8
4
+ data.tar.gz: 2c1dd32185e96ea57bdae128934726b556bfbaaa8a913cb0062d53021cf7df2f
5
5
  SHA512:
6
- metadata.gz: 94423d0dc1cc4d6305aeb94dfc06fe50882726df1d1f72d03596317db4c37cb517fc9ad272236717439878fac7ca46d23354911e64bd1801b9e250d61d237a4b
7
- data.tar.gz: a1ca64366d7343ae2ad8622372d19c97291a0046cf7056af0e9f79902c31e113d8dac8008e687ee53c9bd32f5f24243ded5b74d7dd256f6c23ba9aedb319862d
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
 
@@ -13,7 +13,13 @@ module LLMChain
13
13
  @model = model
14
14
  @memory = memory || Memory::Array.new
15
15
  @tools = tools
16
- @retriever = retriever || Embeddings::Clients::Local::WeaviateRetriever.new
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
- # 1. Сбор контекста
28
- context = memory.recall(prompt)
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
- puts "[RAG Error] #{e.message}"
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
- if memory_context&.any?
73
- parts << "Dialogue history:"
74
- memory_context.each do |item|
75
- parts << "User: #{item[:prompt]}"
76
- parts << "Assistant: #{item[:response]}"
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
- if rag_documents&.any?
81
- parts << "Relevant documents:"
82
- rag_documents.each_with_index do |doc, i|
83
- parts << "Document #{i + 1}: #{doc['content']}"
84
- parts << "Metadata: #{doc['metadata'].to_json}" if doc['metadata']
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
- unless tool_responses.empty?
89
- parts << "Tool results:"
90
- tool_responses.each do |name, response|
91
- parts << "#{name}: #{response}"
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)
@@ -15,6 +15,8 @@ module LLMChain
15
15
  Clients::Qwen
16
16
  when /llama2/
17
17
  Clients::Llama2
18
+ when /gemma3/
19
+ Clients::Gemma3
18
20
  else
19
21
  raise UnknownModelError, "Unknown model: #{model}"
20
22
  end
@@ -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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LlmChain
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
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.3.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-16 00:00:00.000000000 Z
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-аналог LangChain для работы с LLM
164
+ summary: Ruby-analog LangChain to work with LLM
164
165
  test_files: []