dify_llm 1.6.4
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +157 -0
- data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install/templates/create_chats_legacy_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_legacy_migration.rb.tt +16 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +16 -0
- data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +43 -0
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +15 -0
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +9 -0
- data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +4 -0
- data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install_generator.rb +184 -0
- data/lib/generators/ruby_llm/migrate_model_fields/templates/migration.rb.tt +142 -0
- data/lib/generators/ruby_llm/migrate_model_fields_generator.rb +84 -0
- data/lib/ruby_llm/active_record/acts_as.rb +137 -0
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +398 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +315 -0
- data/lib/ruby_llm/active_record/message_methods.rb +72 -0
- data/lib/ruby_llm/active_record/model_methods.rb +84 -0
- data/lib/ruby_llm/aliases.json +274 -0
- data/lib/ruby_llm/aliases.rb +38 -0
- data/lib/ruby_llm/attachment.rb +191 -0
- data/lib/ruby_llm/chat.rb +212 -0
- data/lib/ruby_llm/chunk.rb +6 -0
- data/lib/ruby_llm/configuration.rb +69 -0
- data/lib/ruby_llm/connection.rb +137 -0
- data/lib/ruby_llm/content.rb +50 -0
- data/lib/ruby_llm/context.rb +29 -0
- data/lib/ruby_llm/embedding.rb +29 -0
- data/lib/ruby_llm/error.rb +76 -0
- data/lib/ruby_llm/image.rb +49 -0
- data/lib/ruby_llm/message.rb +76 -0
- data/lib/ruby_llm/mime_type.rb +67 -0
- data/lib/ruby_llm/model/info.rb +103 -0
- data/lib/ruby_llm/model/modalities.rb +22 -0
- data/lib/ruby_llm/model/pricing.rb +48 -0
- data/lib/ruby_llm/model/pricing_category.rb +46 -0
- data/lib/ruby_llm/model/pricing_tier.rb +33 -0
- data/lib/ruby_llm/model.rb +7 -0
- data/lib/ruby_llm/models.json +31418 -0
- data/lib/ruby_llm/models.rb +235 -0
- data/lib/ruby_llm/models_schema.json +168 -0
- data/lib/ruby_llm/provider.rb +215 -0
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +134 -0
- data/lib/ruby_llm/providers/anthropic/chat.rb +106 -0
- data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
- data/lib/ruby_llm/providers/anthropic/media.rb +91 -0
- data/lib/ruby_llm/providers/anthropic/models.rb +48 -0
- data/lib/ruby_llm/providers/anthropic/streaming.rb +43 -0
- data/lib/ruby_llm/providers/anthropic/tools.rb +107 -0
- data/lib/ruby_llm/providers/anthropic.rb +36 -0
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +63 -0
- data/lib/ruby_llm/providers/bedrock/media.rb +60 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +98 -0
- data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +51 -0
- data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +56 -0
- data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +67 -0
- data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +78 -0
- data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +78 -0
- data/lib/ruby_llm/providers/bedrock/streaming.rb +18 -0
- data/lib/ruby_llm/providers/bedrock.rb +82 -0
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +130 -0
- data/lib/ruby_llm/providers/deepseek/chat.rb +16 -0
- data/lib/ruby_llm/providers/deepseek.rb +30 -0
- data/lib/ruby_llm/providers/dify/capabilities.rb +16 -0
- data/lib/ruby_llm/providers/dify/chat.rb +59 -0
- data/lib/ruby_llm/providers/dify/media.rb +37 -0
- data/lib/ruby_llm/providers/dify/streaming.rb +28 -0
- data/lib/ruby_llm/providers/dify.rb +48 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +276 -0
- data/lib/ruby_llm/providers/gemini/chat.rb +171 -0
- data/lib/ruby_llm/providers/gemini/embeddings.rb +37 -0
- data/lib/ruby_llm/providers/gemini/images.rb +47 -0
- data/lib/ruby_llm/providers/gemini/media.rb +54 -0
- data/lib/ruby_llm/providers/gemini/models.rb +40 -0
- data/lib/ruby_llm/providers/gemini/streaming.rb +61 -0
- data/lib/ruby_llm/providers/gemini/tools.rb +77 -0
- data/lib/ruby_llm/providers/gemini.rb +36 -0
- data/lib/ruby_llm/providers/gpustack/chat.rb +27 -0
- data/lib/ruby_llm/providers/gpustack/media.rb +45 -0
- data/lib/ruby_llm/providers/gpustack/models.rb +90 -0
- data/lib/ruby_llm/providers/gpustack.rb +34 -0
- data/lib/ruby_llm/providers/mistral/capabilities.rb +155 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +24 -0
- data/lib/ruby_llm/providers/mistral/embeddings.rb +33 -0
- data/lib/ruby_llm/providers/mistral/models.rb +48 -0
- data/lib/ruby_llm/providers/mistral.rb +32 -0
- data/lib/ruby_llm/providers/ollama/chat.rb +27 -0
- data/lib/ruby_llm/providers/ollama/media.rb +45 -0
- data/lib/ruby_llm/providers/ollama/models.rb +36 -0
- data/lib/ruby_llm/providers/ollama.rb +30 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +291 -0
- data/lib/ruby_llm/providers/openai/chat.rb +83 -0
- data/lib/ruby_llm/providers/openai/embeddings.rb +33 -0
- data/lib/ruby_llm/providers/openai/images.rb +38 -0
- data/lib/ruby_llm/providers/openai/media.rb +80 -0
- data/lib/ruby_llm/providers/openai/models.rb +39 -0
- data/lib/ruby_llm/providers/openai/streaming.rb +41 -0
- data/lib/ruby_llm/providers/openai/tools.rb +78 -0
- data/lib/ruby_llm/providers/openai.rb +42 -0
- data/lib/ruby_llm/providers/openrouter/models.rb +73 -0
- data/lib/ruby_llm/providers/openrouter.rb +26 -0
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +137 -0
- data/lib/ruby_llm/providers/perplexity/chat.rb +16 -0
- data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
- data/lib/ruby_llm/providers/perplexity.rb +48 -0
- data/lib/ruby_llm/providers/vertexai/chat.rb +14 -0
- data/lib/ruby_llm/providers/vertexai/embeddings.rb +32 -0
- data/lib/ruby_llm/providers/vertexai/models.rb +130 -0
- data/lib/ruby_llm/providers/vertexai/streaming.rb +14 -0
- data/lib/ruby_llm/providers/vertexai.rb +55 -0
- data/lib/ruby_llm/railtie.rb +41 -0
- data/lib/ruby_llm/stream_accumulator.rb +97 -0
- data/lib/ruby_llm/streaming.rb +153 -0
- data/lib/ruby_llm/tool.rb +83 -0
- data/lib/ruby_llm/tool_call.rb +22 -0
- data/lib/ruby_llm/utils.rb +45 -0
- data/lib/ruby_llm/version.rb +5 -0
- data/lib/ruby_llm.rb +97 -0
- data/lib/tasks/models.rake +525 -0
- data/lib/tasks/release.rake +67 -0
- data/lib/tasks/ruby_llm.rake +15 -0
- data/lib/tasks/vcr.rake +92 -0
- metadata +291 -0
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
class VertexAI
|
6
|
+
# Models methods for the Vertex AI integration
|
7
|
+
module Models
|
8
|
+
# Gemini and other Google models that aren't returned by the API
|
9
|
+
KNOWN_GOOGLE_MODELS = %w[
|
10
|
+
gemini-2.5-flash-lite
|
11
|
+
gemini-2.5-pro
|
12
|
+
gemini-2.5-flash
|
13
|
+
gemini-2.0-flash-lite-001
|
14
|
+
gemini-2.0-flash-001
|
15
|
+
gemini-2.0-flash
|
16
|
+
gemini-2.0-flash-exp
|
17
|
+
gemini-1.5-pro-002
|
18
|
+
gemini-1.5-pro
|
19
|
+
gemini-1.5-flash-002
|
20
|
+
gemini-1.5-flash
|
21
|
+
gemini-1.5-flash-8b
|
22
|
+
gemini-pro
|
23
|
+
gemini-pro-vision
|
24
|
+
gemini-exp-1206
|
25
|
+
gemini-exp-1121
|
26
|
+
gemini-embedding-001
|
27
|
+
text-embedding-005
|
28
|
+
text-embedding-004
|
29
|
+
text-multilingual-embedding-002
|
30
|
+
].freeze
|
31
|
+
|
32
|
+
def list_models
|
33
|
+
all_models = []
|
34
|
+
page_token = nil
|
35
|
+
|
36
|
+
all_models.concat(build_known_models)
|
37
|
+
|
38
|
+
loop do
|
39
|
+
response = @connection.get('publishers/google/models') do |req|
|
40
|
+
req.headers['x-goog-user-project'] = @config.vertexai_project_id
|
41
|
+
req.params = { pageSize: 100 }
|
42
|
+
req.params[:pageToken] = page_token if page_token
|
43
|
+
end
|
44
|
+
|
45
|
+
publisher_models = response.body['publisherModels'] || []
|
46
|
+
publisher_models.each do |model_data|
|
47
|
+
next if model_data['launchStage'] == 'DEPRECATED'
|
48
|
+
|
49
|
+
model_id = extract_model_id_from_path(model_data['name'])
|
50
|
+
all_models << build_model_from_api_data(model_data, model_id)
|
51
|
+
end
|
52
|
+
|
53
|
+
page_token = response.body['nextPageToken']
|
54
|
+
break unless page_token
|
55
|
+
end
|
56
|
+
|
57
|
+
all_models
|
58
|
+
rescue StandardError => e
|
59
|
+
RubyLLM.logger.debug "Error fetching Vertex AI models: #{e.message}"
|
60
|
+
build_known_models
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def build_known_models
|
66
|
+
KNOWN_GOOGLE_MODELS.map do |model_id|
|
67
|
+
Model::Info.new(
|
68
|
+
id: model_id,
|
69
|
+
name: model_id,
|
70
|
+
provider: slug,
|
71
|
+
family: determine_model_family(model_id),
|
72
|
+
created_at: nil,
|
73
|
+
context_window: nil,
|
74
|
+
max_output_tokens: nil,
|
75
|
+
modalities: nil,
|
76
|
+
capabilities: %w[streaming function_calling],
|
77
|
+
pricing: nil,
|
78
|
+
metadata: {
|
79
|
+
source: 'known_models'
|
80
|
+
}
|
81
|
+
)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def build_model_from_api_data(model_data, model_id)
|
86
|
+
Model::Info.new(
|
87
|
+
id: model_id,
|
88
|
+
name: model_id,
|
89
|
+
provider: slug,
|
90
|
+
family: determine_model_family(model_id),
|
91
|
+
created_at: nil,
|
92
|
+
context_window: nil,
|
93
|
+
max_output_tokens: nil,
|
94
|
+
modalities: nil,
|
95
|
+
capabilities: extract_capabilities(model_data),
|
96
|
+
pricing: nil,
|
97
|
+
metadata: {
|
98
|
+
version_id: model_data['versionId'],
|
99
|
+
open_source_category: model_data['openSourceCategory'],
|
100
|
+
launch_stage: model_data['launchStage'],
|
101
|
+
supported_actions: model_data['supportedActions'],
|
102
|
+
publisher_model_template: model_data['publisherModelTemplate']
|
103
|
+
}
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
def extract_model_id_from_path(path)
|
108
|
+
path.split('/').last
|
109
|
+
end
|
110
|
+
|
111
|
+
def determine_model_family(model_id)
|
112
|
+
case model_id
|
113
|
+
when /^gemini-2\.\d+/ then 'gemini-2'
|
114
|
+
when /^gemini-1\.\d+/ then 'gemini-1.5'
|
115
|
+
when /^text-embedding/ then 'text-embedding'
|
116
|
+
when /bison/ then 'palm'
|
117
|
+
else 'gemini'
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def extract_capabilities(model_data)
|
122
|
+
capabilities = ['streaming']
|
123
|
+
model_name = model_data['name']
|
124
|
+
capabilities << 'function_calling' if model_name.include?('gemini')
|
125
|
+
capabilities.uniq
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
class VertexAI
|
6
|
+
# Streaming methods for the Vertex AI implementation
|
7
|
+
module Streaming
|
8
|
+
def stream_url
|
9
|
+
"projects/#{@config.vertexai_project_id}/locations/#{@config.vertexai_location}/publishers/google/models/#{@model}:streamGenerateContent?alt=sse" # rubocop:disable Layout/LineLength
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
# Google Vertex AI implementation
|
6
|
+
class VertexAI < Gemini
|
7
|
+
include VertexAI::Chat
|
8
|
+
include VertexAI::Streaming
|
9
|
+
include VertexAI::Embeddings
|
10
|
+
include VertexAI::Models
|
11
|
+
|
12
|
+
def initialize(config)
|
13
|
+
super
|
14
|
+
@authorizer = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def api_base
|
18
|
+
"https://#{@config.vertexai_location}-aiplatform.googleapis.com/v1beta1"
|
19
|
+
end
|
20
|
+
|
21
|
+
def headers
|
22
|
+
{
|
23
|
+
'Authorization' => "Bearer #{access_token}"
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
class << self
|
28
|
+
def configuration_requirements
|
29
|
+
%i[vertexai_project_id vertexai_location]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def access_token
|
36
|
+
return 'test-token' if defined?(VCR) && !VCR.current_cassette.recording?
|
37
|
+
|
38
|
+
initialize_authorizer unless @authorizer
|
39
|
+
@authorizer.fetch_access_token!['access_token']
|
40
|
+
end
|
41
|
+
|
42
|
+
def initialize_authorizer
|
43
|
+
require 'googleauth'
|
44
|
+
@authorizer = ::Google::Auth.get_application_default(
|
45
|
+
scope: [
|
46
|
+
'https://www.googleapis.com/auth/cloud-platform',
|
47
|
+
'https://www.googleapis.com/auth/generative-language.retriever'
|
48
|
+
]
|
49
|
+
)
|
50
|
+
rescue LoadError
|
51
|
+
raise Error, 'The googleauth gem is required for Vertex AI. Please add it to your Gemfile: gem "googleauth"'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
# Rails integration for RubyLLM
|
5
|
+
class Railtie < Rails::Railtie
|
6
|
+
initializer 'ruby_llm.inflections' do
|
7
|
+
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
8
|
+
inflect.acronym 'RubyLLM'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
initializer 'ruby_llm.active_record' do
|
13
|
+
ActiveSupport.on_load :active_record do
|
14
|
+
model_registry_class = RubyLLM.config.model_registry_class
|
15
|
+
|
16
|
+
if model_registry_class
|
17
|
+
require 'ruby_llm/active_record/acts_as'
|
18
|
+
::ActiveRecord::Base.include RubyLLM::ActiveRecord::ActsAs
|
19
|
+
else
|
20
|
+
require 'ruby_llm/active_record/acts_as_legacy'
|
21
|
+
::ActiveRecord::Base.include RubyLLM::ActiveRecord::ActsAsLegacy
|
22
|
+
|
23
|
+
Rails.logger.warn(
|
24
|
+
'RubyLLM: String-based model fields are deprecated and will be removed in RubyLLM 2.0.0. ' \
|
25
|
+
'Please migrate to the DB-backed model registry. ' \
|
26
|
+
"Run 'rails generate ruby_llm:migrate_model_fields' to upgrade."
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
generators do
|
33
|
+
require 'generators/ruby_llm/install_generator'
|
34
|
+
require 'generators/ruby_llm/migrate_model_fields_generator'
|
35
|
+
end
|
36
|
+
|
37
|
+
rake_tasks do
|
38
|
+
load 'tasks/ruby_llm.rake'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
# Assembles streaming responses from LLMs into complete messages.
|
5
|
+
class StreamAccumulator
|
6
|
+
attr_reader :content, :model_id, :conversation_id, :tool_calls
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@content = +''
|
10
|
+
@tool_calls = {}
|
11
|
+
@input_tokens = 0
|
12
|
+
@output_tokens = 0
|
13
|
+
@latest_tool_call_id = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def add(chunk)
|
17
|
+
RubyLLM.logger.debug chunk.inspect if RubyLLM.config.log_stream_debug
|
18
|
+
@model_id ||= chunk.model_id
|
19
|
+
@conversation_id ||= chunk.conversation_id
|
20
|
+
|
21
|
+
if chunk.tool_call?
|
22
|
+
accumulate_tool_calls chunk.tool_calls
|
23
|
+
else
|
24
|
+
@content << (chunk.content || '')
|
25
|
+
end
|
26
|
+
|
27
|
+
count_tokens chunk
|
28
|
+
RubyLLM.logger.debug inspect if RubyLLM.config.log_stream_debug
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_message(response)
|
32
|
+
Message.new(
|
33
|
+
role: :assistant,
|
34
|
+
content: content.empty? ? nil : content,
|
35
|
+
model_id: model_id,
|
36
|
+
conversation_id: conversation_id,
|
37
|
+
tool_calls: tool_calls_from_stream,
|
38
|
+
input_tokens: @input_tokens.positive? ? @input_tokens : nil,
|
39
|
+
output_tokens: @output_tokens.positive? ? @output_tokens : nil,
|
40
|
+
raw: response
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def tool_calls_from_stream
|
47
|
+
tool_calls.transform_values do |tc|
|
48
|
+
arguments = if tc.arguments.is_a?(String) && !tc.arguments.empty?
|
49
|
+
JSON.parse(tc.arguments)
|
50
|
+
elsif tc.arguments.is_a?(String)
|
51
|
+
{}
|
52
|
+
else
|
53
|
+
tc.arguments
|
54
|
+
end
|
55
|
+
|
56
|
+
ToolCall.new(
|
57
|
+
id: tc.id,
|
58
|
+
name: tc.name,
|
59
|
+
arguments: arguments
|
60
|
+
)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def accumulate_tool_calls(new_tool_calls)
|
65
|
+
RubyLLM.logger.debug "Accumulating tool calls: #{new_tool_calls}" if RubyLLM.config.log_stream_debug
|
66
|
+
new_tool_calls.each_value do |tool_call|
|
67
|
+
if tool_call.id
|
68
|
+
tool_call_id = tool_call.id.empty? ? SecureRandom.uuid : tool_call.id
|
69
|
+
tool_call_arguments = tool_call.arguments.empty? ? +'' : tool_call.arguments
|
70
|
+
@tool_calls[tool_call.id] = ToolCall.new(
|
71
|
+
id: tool_call_id,
|
72
|
+
name: tool_call.name,
|
73
|
+
arguments: tool_call_arguments
|
74
|
+
)
|
75
|
+
@latest_tool_call_id = tool_call.id
|
76
|
+
else
|
77
|
+
existing = @tool_calls[@latest_tool_call_id]
|
78
|
+
existing.arguments << tool_call.arguments if existing
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def find_tool_call(tool_call_id)
|
84
|
+
if tool_call_id.nil?
|
85
|
+
@tool_calls[@latest_tool_call]
|
86
|
+
else
|
87
|
+
@latest_tool_call_id = tool_call_id
|
88
|
+
@tool_calls[tool_call_id]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def count_tokens(chunk)
|
93
|
+
@input_tokens = chunk.input_tokens if chunk.input_tokens
|
94
|
+
@output_tokens = chunk.output_tokens if chunk.output_tokens
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
# Handles streaming responses from AI providers.
|
5
|
+
module Streaming
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def stream_response(connection, payload, additional_headers = {}, &block)
|
9
|
+
accumulator = StreamAccumulator.new
|
10
|
+
|
11
|
+
response = connection.post stream_url, payload do |req|
|
12
|
+
req.headers = additional_headers.merge(req.headers) unless additional_headers.empty?
|
13
|
+
if faraday_1?
|
14
|
+
req.options[:on_data] = handle_stream do |chunk|
|
15
|
+
accumulator.add chunk
|
16
|
+
block.call chunk
|
17
|
+
end
|
18
|
+
else
|
19
|
+
req.options.on_data = handle_stream do |chunk|
|
20
|
+
accumulator.add chunk
|
21
|
+
block.call chunk
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
message = accumulator.to_message(response)
|
27
|
+
RubyLLM.logger.debug "Stream completed: #{message.content}"
|
28
|
+
message
|
29
|
+
end
|
30
|
+
|
31
|
+
def handle_stream(&block)
|
32
|
+
to_json_stream do |data|
|
33
|
+
block.call(build_chunk(data)) if data
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def faraday_1?
|
40
|
+
Faraday::VERSION.start_with?('1')
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_json_stream(&)
|
44
|
+
buffer = +''
|
45
|
+
parser = EventStreamParser::Parser.new
|
46
|
+
|
47
|
+
create_stream_processor(parser, buffer, &)
|
48
|
+
end
|
49
|
+
|
50
|
+
def create_stream_processor(parser, buffer, &)
|
51
|
+
if faraday_1?
|
52
|
+
legacy_stream_processor(parser, &)
|
53
|
+
else
|
54
|
+
stream_processor(parser, buffer, &)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def process_stream_chunk(chunk, parser, env, &)
|
59
|
+
RubyLLM.logger.debug "Received chunk: #{chunk}" if RubyLLM.config.log_stream_debug
|
60
|
+
|
61
|
+
if error_chunk?(chunk)
|
62
|
+
handle_error_chunk(chunk, env)
|
63
|
+
else
|
64
|
+
yield handle_sse(chunk, parser, env, &)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def legacy_stream_processor(parser, &block)
|
69
|
+
proc do |chunk, _size|
|
70
|
+
process_stream_chunk(chunk, parser, nil, &block)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def stream_processor(parser, buffer, &block)
|
75
|
+
proc do |chunk, _bytes, env|
|
76
|
+
if env&.status == 200
|
77
|
+
process_stream_chunk(chunk, parser, env, &block)
|
78
|
+
else
|
79
|
+
handle_failed_response(chunk, buffer, env)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def error_chunk?(chunk)
|
85
|
+
chunk.start_with?('event: error')
|
86
|
+
end
|
87
|
+
|
88
|
+
def handle_error_chunk(chunk, env)
|
89
|
+
error_data = chunk.split("\n")[1].delete_prefix('data: ')
|
90
|
+
status, _message = parse_streaming_error(error_data)
|
91
|
+
parsed_data = JSON.parse(error_data)
|
92
|
+
|
93
|
+
error_response = if faraday_1?
|
94
|
+
Struct.new(:body, :status).new(parsed_data, status)
|
95
|
+
else
|
96
|
+
env.merge(body: parsed_data, status: status)
|
97
|
+
end
|
98
|
+
|
99
|
+
ErrorMiddleware.parse_error(provider: self, response: error_response)
|
100
|
+
rescue JSON::ParserError => e
|
101
|
+
RubyLLM.logger.debug "Failed to parse error chunk: #{e.message}"
|
102
|
+
end
|
103
|
+
|
104
|
+
def handle_failed_response(chunk, buffer, env)
|
105
|
+
buffer << chunk
|
106
|
+
error_data = JSON.parse(buffer)
|
107
|
+
error_response = env.merge(body: error_data)
|
108
|
+
ErrorMiddleware.parse_error(provider: self, response: error_response)
|
109
|
+
rescue JSON::ParserError
|
110
|
+
RubyLLM.logger.debug "Accumulating error chunk: #{chunk}"
|
111
|
+
end
|
112
|
+
|
113
|
+
def handle_sse(chunk, parser, env, &block)
|
114
|
+
parser.feed(chunk) do |type, data|
|
115
|
+
case type.to_sym
|
116
|
+
when :error
|
117
|
+
handle_error_event(data, env)
|
118
|
+
else
|
119
|
+
yield handle_data(data, &block) unless data == '[DONE]'
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def handle_data(data)
|
125
|
+
JSON.parse(data)
|
126
|
+
rescue JSON::ParserError => e
|
127
|
+
RubyLLM.logger.debug "Failed to parse data chunk: #{e.message}"
|
128
|
+
end
|
129
|
+
|
130
|
+
def handle_error_event(data, env)
|
131
|
+
status, _message = parse_streaming_error(data)
|
132
|
+
parsed_data = JSON.parse(data)
|
133
|
+
|
134
|
+
error_response = if faraday_1?
|
135
|
+
Struct.new(:body, :status).new(parsed_data, status)
|
136
|
+
else
|
137
|
+
env.merge(body: parsed_data, status: status)
|
138
|
+
end
|
139
|
+
|
140
|
+
ErrorMiddleware.parse_error(provider: self, response: error_response)
|
141
|
+
rescue JSON::ParserError => e
|
142
|
+
RubyLLM.logger.debug "Failed to parse error event: #{e.message}"
|
143
|
+
end
|
144
|
+
|
145
|
+
def parse_streaming_error(data)
|
146
|
+
error_data = JSON.parse(data)
|
147
|
+
[500, error_data['message'] || 'Unknown streaming error']
|
148
|
+
rescue JSON::ParserError => e
|
149
|
+
RubyLLM.logger.debug "Failed to parse streaming error: #{e.message}"
|
150
|
+
[500, "Failed to parse error: #{data}"]
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
# Parameter definition for Tool methods.
|
5
|
+
class Parameter
|
6
|
+
attr_reader :name, :type, :description, :required
|
7
|
+
|
8
|
+
def initialize(name, type: 'string', desc: nil, required: true)
|
9
|
+
@name = name
|
10
|
+
@type = type
|
11
|
+
@description = desc
|
12
|
+
@required = required
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Base class for creating tools that AI models can use
|
17
|
+
class Tool
|
18
|
+
# Stops conversation continuation after tool execution
|
19
|
+
class Halt
|
20
|
+
attr_reader :content
|
21
|
+
|
22
|
+
def initialize(content)
|
23
|
+
@content = content
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_s
|
27
|
+
@content.to_s
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class << self
|
32
|
+
def description(text = nil)
|
33
|
+
return @description unless text
|
34
|
+
|
35
|
+
@description = text
|
36
|
+
end
|
37
|
+
|
38
|
+
def param(name, **options)
|
39
|
+
parameters[name] = Parameter.new(name, **options)
|
40
|
+
end
|
41
|
+
|
42
|
+
def parameters
|
43
|
+
@parameters ||= {}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def name
|
48
|
+
klass_name = self.class.name
|
49
|
+
normalized = klass_name.to_s.dup.force_encoding('UTF-8').unicode_normalize(:nfkd)
|
50
|
+
normalized.encode('ASCII', replace: '')
|
51
|
+
.gsub(/[^a-zA-Z0-9_-]/, '-')
|
52
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
53
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
54
|
+
.downcase
|
55
|
+
.delete_suffix('_tool')
|
56
|
+
end
|
57
|
+
|
58
|
+
def description
|
59
|
+
self.class.description
|
60
|
+
end
|
61
|
+
|
62
|
+
def parameters
|
63
|
+
self.class.parameters
|
64
|
+
end
|
65
|
+
|
66
|
+
def call(args)
|
67
|
+
RubyLLM.logger.debug "Tool #{name} called with: #{args.inspect}"
|
68
|
+
result = execute(**args.transform_keys(&:to_sym))
|
69
|
+
RubyLLM.logger.debug "Tool #{name} returned: #{result.inspect}"
|
70
|
+
result
|
71
|
+
end
|
72
|
+
|
73
|
+
def execute(...)
|
74
|
+
raise NotImplementedError, 'Subclasses must implement #execute'
|
75
|
+
end
|
76
|
+
|
77
|
+
protected
|
78
|
+
|
79
|
+
def halt(message)
|
80
|
+
Halt.new(message)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
# Represents a function call from an AI model to a Tool.
|
5
|
+
class ToolCall
|
6
|
+
attr_reader :id, :name, :arguments
|
7
|
+
|
8
|
+
def initialize(id:, name:, arguments: {})
|
9
|
+
@id = id
|
10
|
+
@name = name
|
11
|
+
@arguments = arguments
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_h
|
15
|
+
{
|
16
|
+
id: @id,
|
17
|
+
name: @name,
|
18
|
+
arguments: @arguments
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
# Provides utility functions for data manipulation within the RubyLLM library
|
5
|
+
module Utils
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def hash_get(hash, key)
|
9
|
+
hash[key.to_sym] || hash[key.to_s]
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_safe_array(item)
|
13
|
+
case item
|
14
|
+
when Array
|
15
|
+
item
|
16
|
+
when Hash
|
17
|
+
[item]
|
18
|
+
else
|
19
|
+
Array(item)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_time(value)
|
24
|
+
return unless value
|
25
|
+
|
26
|
+
value.is_a?(Time) ? value : Time.parse(value.to_s)
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_date(value)
|
30
|
+
return unless value
|
31
|
+
|
32
|
+
value.is_a?(Date) ? value : Date.parse(value.to_s)
|
33
|
+
end
|
34
|
+
|
35
|
+
def deep_merge(original, overrides)
|
36
|
+
original.merge(overrides) do |_key, original_value, overrides_value|
|
37
|
+
if original_value.is_a?(Hash) && overrides_value.is_a?(Hash)
|
38
|
+
deep_merge(original_value, overrides_value)
|
39
|
+
else
|
40
|
+
overrides_value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|