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.
Files changed (129) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +157 -0
  4. data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
  5. data/lib/generators/ruby_llm/install/templates/create_chats_legacy_migration.rb.tt +8 -0
  6. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +8 -0
  7. data/lib/generators/ruby_llm/install/templates/create_messages_legacy_migration.rb.tt +16 -0
  8. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +16 -0
  9. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +43 -0
  10. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +15 -0
  11. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +9 -0
  12. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +4 -0
  13. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +3 -0
  14. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +3 -0
  15. data/lib/generators/ruby_llm/install_generator.rb +184 -0
  16. data/lib/generators/ruby_llm/migrate_model_fields/templates/migration.rb.tt +142 -0
  17. data/lib/generators/ruby_llm/migrate_model_fields_generator.rb +84 -0
  18. data/lib/ruby_llm/active_record/acts_as.rb +137 -0
  19. data/lib/ruby_llm/active_record/acts_as_legacy.rb +398 -0
  20. data/lib/ruby_llm/active_record/chat_methods.rb +315 -0
  21. data/lib/ruby_llm/active_record/message_methods.rb +72 -0
  22. data/lib/ruby_llm/active_record/model_methods.rb +84 -0
  23. data/lib/ruby_llm/aliases.json +274 -0
  24. data/lib/ruby_llm/aliases.rb +38 -0
  25. data/lib/ruby_llm/attachment.rb +191 -0
  26. data/lib/ruby_llm/chat.rb +212 -0
  27. data/lib/ruby_llm/chunk.rb +6 -0
  28. data/lib/ruby_llm/configuration.rb +69 -0
  29. data/lib/ruby_llm/connection.rb +137 -0
  30. data/lib/ruby_llm/content.rb +50 -0
  31. data/lib/ruby_llm/context.rb +29 -0
  32. data/lib/ruby_llm/embedding.rb +29 -0
  33. data/lib/ruby_llm/error.rb +76 -0
  34. data/lib/ruby_llm/image.rb +49 -0
  35. data/lib/ruby_llm/message.rb +76 -0
  36. data/lib/ruby_llm/mime_type.rb +67 -0
  37. data/lib/ruby_llm/model/info.rb +103 -0
  38. data/lib/ruby_llm/model/modalities.rb +22 -0
  39. data/lib/ruby_llm/model/pricing.rb +48 -0
  40. data/lib/ruby_llm/model/pricing_category.rb +46 -0
  41. data/lib/ruby_llm/model/pricing_tier.rb +33 -0
  42. data/lib/ruby_llm/model.rb +7 -0
  43. data/lib/ruby_llm/models.json +31418 -0
  44. data/lib/ruby_llm/models.rb +235 -0
  45. data/lib/ruby_llm/models_schema.json +168 -0
  46. data/lib/ruby_llm/provider.rb +215 -0
  47. data/lib/ruby_llm/providers/anthropic/capabilities.rb +134 -0
  48. data/lib/ruby_llm/providers/anthropic/chat.rb +106 -0
  49. data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
  50. data/lib/ruby_llm/providers/anthropic/media.rb +91 -0
  51. data/lib/ruby_llm/providers/anthropic/models.rb +48 -0
  52. data/lib/ruby_llm/providers/anthropic/streaming.rb +43 -0
  53. data/lib/ruby_llm/providers/anthropic/tools.rb +107 -0
  54. data/lib/ruby_llm/providers/anthropic.rb +36 -0
  55. data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
  56. data/lib/ruby_llm/providers/bedrock/chat.rb +63 -0
  57. data/lib/ruby_llm/providers/bedrock/media.rb +60 -0
  58. data/lib/ruby_llm/providers/bedrock/models.rb +98 -0
  59. data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
  60. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +51 -0
  61. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +56 -0
  62. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +67 -0
  63. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +78 -0
  64. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +78 -0
  65. data/lib/ruby_llm/providers/bedrock/streaming.rb +18 -0
  66. data/lib/ruby_llm/providers/bedrock.rb +82 -0
  67. data/lib/ruby_llm/providers/deepseek/capabilities.rb +130 -0
  68. data/lib/ruby_llm/providers/deepseek/chat.rb +16 -0
  69. data/lib/ruby_llm/providers/deepseek.rb +30 -0
  70. data/lib/ruby_llm/providers/dify/capabilities.rb +16 -0
  71. data/lib/ruby_llm/providers/dify/chat.rb +59 -0
  72. data/lib/ruby_llm/providers/dify/media.rb +37 -0
  73. data/lib/ruby_llm/providers/dify/streaming.rb +28 -0
  74. data/lib/ruby_llm/providers/dify.rb +48 -0
  75. data/lib/ruby_llm/providers/gemini/capabilities.rb +276 -0
  76. data/lib/ruby_llm/providers/gemini/chat.rb +171 -0
  77. data/lib/ruby_llm/providers/gemini/embeddings.rb +37 -0
  78. data/lib/ruby_llm/providers/gemini/images.rb +47 -0
  79. data/lib/ruby_llm/providers/gemini/media.rb +54 -0
  80. data/lib/ruby_llm/providers/gemini/models.rb +40 -0
  81. data/lib/ruby_llm/providers/gemini/streaming.rb +61 -0
  82. data/lib/ruby_llm/providers/gemini/tools.rb +77 -0
  83. data/lib/ruby_llm/providers/gemini.rb +36 -0
  84. data/lib/ruby_llm/providers/gpustack/chat.rb +27 -0
  85. data/lib/ruby_llm/providers/gpustack/media.rb +45 -0
  86. data/lib/ruby_llm/providers/gpustack/models.rb +90 -0
  87. data/lib/ruby_llm/providers/gpustack.rb +34 -0
  88. data/lib/ruby_llm/providers/mistral/capabilities.rb +155 -0
  89. data/lib/ruby_llm/providers/mistral/chat.rb +24 -0
  90. data/lib/ruby_llm/providers/mistral/embeddings.rb +33 -0
  91. data/lib/ruby_llm/providers/mistral/models.rb +48 -0
  92. data/lib/ruby_llm/providers/mistral.rb +32 -0
  93. data/lib/ruby_llm/providers/ollama/chat.rb +27 -0
  94. data/lib/ruby_llm/providers/ollama/media.rb +45 -0
  95. data/lib/ruby_llm/providers/ollama/models.rb +36 -0
  96. data/lib/ruby_llm/providers/ollama.rb +30 -0
  97. data/lib/ruby_llm/providers/openai/capabilities.rb +291 -0
  98. data/lib/ruby_llm/providers/openai/chat.rb +83 -0
  99. data/lib/ruby_llm/providers/openai/embeddings.rb +33 -0
  100. data/lib/ruby_llm/providers/openai/images.rb +38 -0
  101. data/lib/ruby_llm/providers/openai/media.rb +80 -0
  102. data/lib/ruby_llm/providers/openai/models.rb +39 -0
  103. data/lib/ruby_llm/providers/openai/streaming.rb +41 -0
  104. data/lib/ruby_llm/providers/openai/tools.rb +78 -0
  105. data/lib/ruby_llm/providers/openai.rb +42 -0
  106. data/lib/ruby_llm/providers/openrouter/models.rb +73 -0
  107. data/lib/ruby_llm/providers/openrouter.rb +26 -0
  108. data/lib/ruby_llm/providers/perplexity/capabilities.rb +137 -0
  109. data/lib/ruby_llm/providers/perplexity/chat.rb +16 -0
  110. data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
  111. data/lib/ruby_llm/providers/perplexity.rb +48 -0
  112. data/lib/ruby_llm/providers/vertexai/chat.rb +14 -0
  113. data/lib/ruby_llm/providers/vertexai/embeddings.rb +32 -0
  114. data/lib/ruby_llm/providers/vertexai/models.rb +130 -0
  115. data/lib/ruby_llm/providers/vertexai/streaming.rb +14 -0
  116. data/lib/ruby_llm/providers/vertexai.rb +55 -0
  117. data/lib/ruby_llm/railtie.rb +41 -0
  118. data/lib/ruby_llm/stream_accumulator.rb +97 -0
  119. data/lib/ruby_llm/streaming.rb +153 -0
  120. data/lib/ruby_llm/tool.rb +83 -0
  121. data/lib/ruby_llm/tool_call.rb +22 -0
  122. data/lib/ruby_llm/utils.rb +45 -0
  123. data/lib/ruby_llm/version.rb +5 -0
  124. data/lib/ruby_llm.rb +97 -0
  125. data/lib/tasks/models.rake +525 -0
  126. data/lib/tasks/release.rake +67 -0
  127. data/lib/tasks/ruby_llm.rake +15 -0
  128. data/lib/tasks/vcr.rake +92 -0
  129. 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ VERSION = '1.6.4'
5
+ end