ruby_llm 2.0.0 → 2.1.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 +65 -8
- data/lib/generators/ruby_llm/generator_helpers.rb +68 -6
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +1 -0
- data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +36 -3
- data/lib/generators/ruby_llm/provider/scaffold.rb +32 -22
- data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +1 -5
- data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +1 -1
- data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +1 -1
- data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +1 -1
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_1.rb.tt +78 -0
- data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +6 -163
- data/lib/ruby_llm/accounting/usage.rb +86 -49
- data/lib/ruby_llm/active_record/acts_as.rb +3 -0
- data/lib/ruby_llm/active_record/attachment_helpers.rb +50 -25
- data/lib/ruby_llm/active_record/batch.rb +1 -1
- data/lib/ruby_llm/active_record/chat_methods.rb +136 -40
- data/lib/ruby_llm/active_record/mcp_credential.rb +41 -0
- data/lib/ruby_llm/active_record/message_methods.rb +32 -7
- data/lib/ruby_llm/active_record/model.rb +1 -1
- data/lib/ruby_llm/active_record/provider_file.rb +64 -0
- data/lib/ruby_llm/active_record/record.rb +19 -0
- data/lib/ruby_llm/active_record/tool_call.rb +1 -1
- data/lib/ruby_llm/active_record/usage.rb +53 -3
- data/lib/ruby_llm/agent.rb +198 -33
- data/lib/ruby_llm/aliases.json +66 -8
- data/lib/ruby_llm/attachment.rb +54 -10
- data/lib/ruby_llm/batch.rb +26 -3
- data/lib/ruby_llm/chat/tool_concurrency.rb +47 -23
- data/lib/ruby_llm/chat/tool_search.rb +26 -0
- data/lib/ruby_llm/chat.rb +488 -82
- data/lib/ruby_llm/choice.rb +38 -0
- data/lib/ruby_llm/configuration.rb +36 -11
- data/lib/ruby_llm/context.rb +45 -32
- data/lib/ruby_llm/cost.rb +42 -1
- data/lib/ruby_llm/embedding.rb +9 -2
- data/lib/ruby_llm/error.rb +34 -2
- data/lib/ruby_llm/evaluation/assertions.rb +40 -0
- data/lib/ruby_llm/evaluation/case.rb +52 -0
- data/lib/ruby_llm/evaluation/dataset.rb +65 -0
- data/lib/ruby_llm/evaluation/evaluator.rb +126 -0
- data/lib/ruby_llm/evaluation/evidence.rb +125 -0
- data/lib/ruby_llm/evaluation/minitest.rb +26 -0
- data/lib/ruby_llm/evaluation/report.rb +103 -0
- data/lib/ruby_llm/evaluation/result.rb +75 -0
- data/lib/ruby_llm/evaluation/reviewer.rb +20 -0
- data/lib/ruby_llm/evaluation/rspec.rb +23 -0
- data/lib/ruby_llm/evaluation/runner.rb +93 -0
- data/lib/ruby_llm/evaluation/trial.rb +101 -0
- data/lib/ruby_llm/evaluation.rb +289 -0
- data/lib/ruby_llm/image.rb +12 -4
- data/lib/ruby_llm/judge/builder.rb +51 -0
- data/lib/ruby_llm/judge/data.rb +43 -0
- data/lib/ruby_llm/judge/question.rb +101 -0
- data/lib/ruby_llm/judge.rb +179 -0
- data/lib/ruby_llm/judgment.rb +125 -0
- data/lib/ruby_llm/mcp/apps.rb +25 -0
- data/lib/ruby_llm/mcp/client.rb +213 -0
- data/lib/ruby_llm/mcp/collection.rb +56 -0
- data/lib/ruby_llm/mcp/content.rb +44 -0
- data/lib/ruby_llm/mcp/error.rb +27 -0
- data/lib/ruby_llm/mcp/http.rb +379 -0
- data/lib/ruby_llm/mcp/input_request.rb +118 -0
- data/lib/ruby_llm/mcp/input_required_error.rb +29 -0
- data/lib/ruby_llm/mcp/listener.rb +168 -0
- data/lib/ruby_llm/mcp/oauth/key.rb +78 -0
- data/lib/ruby_llm/mcp/oauth/memory_store.rb +28 -0
- data/lib/ruby_llm/mcp/oauth/proofs.rb +39 -0
- data/lib/ruby_llm/mcp/oauth.rb +653 -0
- data/lib/ruby_llm/mcp/param_headers.rb +83 -0
- data/lib/ruby_llm/mcp/prompt.rb +71 -0
- data/lib/ruby_llm/mcp/resource.rb +84 -0
- data/lib/ruby_llm/mcp/resource_template.rb +99 -0
- data/lib/ruby_llm/mcp/result.rb +76 -0
- data/lib/ruby_llm/mcp/session_expired.rb +10 -0
- data/lib/ruby_llm/mcp/stdio.rb +250 -0
- data/lib/ruby_llm/mcp/task.rb +151 -0
- data/lib/ruby_llm/mcp/tool.rb +130 -0
- data/lib/ruby_llm/mcp.rb +1101 -0
- data/lib/ruby_llm/message.rb +41 -6
- data/lib/ruby_llm/model.rb +2 -1
- data/lib/ruby_llm/models/aliases.rb +5 -2
- data/lib/ruby_llm/models/lookup.rb +80 -0
- data/lib/ruby_llm/models/registry.rb +2 -2
- data/lib/ruby_llm/models/schema.rb +1 -1
- data/lib/ruby_llm/models.json +23082 -10158
- data/lib/ruby_llm/models.rb +20 -69
- data/lib/ruby_llm/moderation.rb +7 -2
- data/lib/ruby_llm/ocr.rb +7 -2
- data/lib/ruby_llm/open_telemetry/attributes.rb +111 -0
- data/lib/ruby_llm/open_telemetry.rb +82 -0
- data/lib/ruby_llm/probability.rb +30 -0
- data/lib/ruby_llm/progress.rb +44 -0
- data/lib/ruby_llm/prompt/context.rb +55 -0
- data/lib/ruby_llm/prompt.rb +1 -1
- data/lib/ruby_llm/protocol/binary_streaming.rb +1 -1
- data/lib/ruby_llm/protocol/request_shapes/analysis.rb +190 -0
- data/lib/ruby_llm/protocol/request_shapes.rb +183 -0
- data/lib/ruby_llm/protocol/stored_uploads.rb +62 -0
- data/lib/ruby_llm/protocol/stream_accumulator.rb +9 -25
- data/lib/ruby_llm/protocol/streaming.rb +38 -31
- data/lib/ruby_llm/protocol.rb +192 -40
- data/lib/ruby_llm/protocols/anthropic/chat.rb +39 -32
- data/lib/ruby_llm/protocols/anthropic/media.rb +6 -2
- data/lib/ruby_llm/protocols/anthropic/models.rb +16 -1
- data/lib/ruby_llm/protocols/anthropic/request_shapes.rb +70 -0
- data/lib/ruby_llm/protocols/anthropic/streaming.rb +23 -9
- data/lib/ruby_llm/protocols/anthropic/tools.rb +43 -0
- data/lib/ruby_llm/protocols/anthropic.rb +20 -1
- data/lib/ruby_llm/protocols/bedrock/async_videos.rb +9 -17
- data/lib/ruby_llm/protocols/bedrock/guardrails.rb +8 -9
- data/lib/ruby_llm/protocols/bedrock/rerank.rb +1 -17
- data/lib/ruby_llm/protocols/chat_completions/chat.rb +5 -7
- data/lib/ruby_llm/protocols/chat_completions/media.rb +34 -8
- data/lib/ruby_llm/protocols/chat_completions/request_shapes.rb +121 -0
- data/lib/ruby_llm/protocols/chat_completions/streaming.rb +3 -0
- data/lib/ruby_llm/protocols/chat_completions.rb +1 -0
- data/lib/ruby_llm/protocols/cohere/batch_requests.rb +0 -5
- data/lib/ruby_llm/protocols/cohere/batches.rb +3 -7
- data/lib/ruby_llm/protocols/cohere/chat.rb +8 -0
- data/lib/ruby_llm/protocols/cohere/embeddings.rb +7 -4
- data/lib/ruby_llm/protocols/cohere/media.rb +6 -2
- data/lib/ruby_llm/protocols/cohere/ocr.rb +1 -1
- data/lib/ruby_llm/protocols/cohere/streaming.rb +1 -0
- data/lib/ruby_llm/protocols/cohere/tools.rb +5 -1
- data/lib/ruby_llm/protocols/cohere.rb +1 -0
- data/lib/ruby_llm/protocols/converse/chat.rb +22 -131
- data/lib/ruby_llm/protocols/converse/media.rb +16 -2
- data/lib/ruby_llm/protocols/converse/reasoning.rb +194 -0
- data/lib/ruby_llm/protocols/converse/request_shapes.rb +99 -0
- data/lib/ruby_llm/protocols/converse/streaming.rb +2 -2
- data/lib/ruby_llm/protocols/converse/thinking_stream.rb +2 -1
- data/lib/ruby_llm/protocols/converse.rb +3 -5
- data/lib/ruby_llm/protocols/deepseek/files.rb +2 -9
- data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +1 -7
- data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +11 -17
- data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +1 -4
- data/lib/ruby_llm/protocols/gemini/chat.rb +50 -21
- data/lib/ruby_llm/protocols/gemini/embeddings.rb +2 -2
- data/lib/ruby_llm/protocols/gemini/files.rb +1 -1
- data/lib/ruby_llm/protocols/gemini/images.rb +5 -1
- data/lib/ruby_llm/protocols/gemini/media.rb +15 -3
- data/lib/ruby_llm/protocols/gemini/request_shapes.rb +111 -0
- data/lib/ruby_llm/protocols/gemini/streaming.rb +2 -1
- data/lib/ruby_llm/protocols/gemini/tools.rb +17 -11
- data/lib/ruby_llm/protocols/gemini/transcription.rb +13 -0
- data/lib/ruby_llm/protocols/gemini/videos.rb +7 -2
- data/lib/ruby_llm/protocols/gemini.rb +1 -0
- data/lib/ruby_llm/protocols/gpustack/videos.rb +7 -9
- data/lib/ruby_llm/protocols/interactions/chat.rb +46 -18
- data/lib/ruby_llm/protocols/interactions/content.rb +9 -2
- data/lib/ruby_llm/protocols/interactions/request_shapes.rb +62 -0
- data/lib/ruby_llm/protocols/interactions/streaming.rb +15 -0
- data/lib/ruby_llm/protocols/interactions/tools.rb +28 -4
- data/lib/ruby_llm/protocols/interactions/transcription.rb +1 -11
- data/lib/ruby_llm/protocols/interactions.rb +1 -0
- data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +2 -11
- data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +2 -2
- data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +2 -6
- data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +2 -8
- data/lib/ruby_llm/protocols/invoke_model.rb +1 -4
- data/lib/ruby_llm/protocols/mistral/content.rb +13 -0
- data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +11 -1
- data/lib/ruby_llm/protocols/mistral/conversations/images.rb +5 -1
- data/lib/ruby_llm/protocols/mistral/conversations/request_shapes.rb +45 -0
- data/lib/ruby_llm/protocols/mistral/conversations.rb +1 -0
- data/lib/ruby_llm/protocols/mistral/multi_completion.rb +11 -1
- data/lib/ruby_llm/protocols/openai/decisions.rb +135 -0
- data/lib/ruby_llm/protocols/openai/files.rb +0 -7
- data/lib/ruby_llm/protocols/openrouter/batches.rb +13 -8
- data/lib/ruby_llm/protocols/openrouter/responses.rb +5 -1
- data/lib/ruby_llm/protocols/perplexity/agent.rb +116 -0
- data/lib/ruby_llm/protocols/perplexity/router.rb +2 -43
- data/lib/ruby_llm/protocols/responses/chat.rb +56 -22
- data/lib/ruby_llm/protocols/responses/media.rb +11 -7
- data/lib/ruby_llm/protocols/responses/request_shapes.rb +70 -0
- data/lib/ruby_llm/protocols/responses/streaming.rb +23 -7
- data/lib/ruby_llm/protocols/responses/token_counting.rb +2 -2
- data/lib/ruby_llm/protocols/responses/tools.rb +27 -0
- data/lib/ruby_llm/protocols/responses.rb +1 -0
- data/lib/ruby_llm/protocols/system_one/judgments.rb +42 -0
- data/lib/ruby_llm/protocols/system_one/models.rb +27 -0
- data/lib/ruby_llm/protocols/system_one/responses.rb +55 -0
- data/lib/ruby_llm/protocols/system_one.rb +27 -0
- data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +2 -2
- data/lib/ruby_llm/protocols/vertexai/files.rb +34 -6
- data/lib/ruby_llm/protocols/vertexai/ranking.rb +9 -30
- data/lib/ruby_llm/protocols/vertexai/research.rb +9 -41
- data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +2 -5
- data/lib/ruby_llm/provider.rb +87 -15
- data/lib/ruby_llm/providers/anthropic.rb +23 -0
- data/lib/ruby_llm/providers/azure/chat.rb +4 -0
- data/lib/ruby_llm/providers/azure/images.rb +2 -2
- data/lib/ruby_llm/providers/azure/media.rb +8 -3
- data/lib/ruby_llm/providers/azure/videos.rb +7 -2
- data/lib/ruby_llm/providers/azure.rb +24 -4
- data/lib/ruby_llm/providers/bedrock/auth.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +1 -3
- data/lib/ruby_llm/providers/bedrock/mantle.rb +5 -9
- data/lib/ruby_llm/providers/bedrock.rb +38 -6
- data/lib/ruby_llm/providers/deepseek/chat.rb +6 -1
- data/lib/ruby_llm/providers/deepseek/responses.rb +6 -0
- data/lib/ruby_llm/providers/deepseek.rb +4 -0
- data/lib/ruby_llm/providers/gemini.rb +4 -0
- data/lib/ruby_llm/providers/gpustack/media.rb +8 -5
- data/lib/ruby_llm/providers/hetzner/chat.rb +39 -0
- data/lib/ruby_llm/providers/hetzner.rb +41 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +16 -12
- data/lib/ruby_llm/providers/mistral/media.rb +8 -4
- data/lib/ruby_llm/providers/mistral/ocr.rb +1 -5
- data/lib/ruby_llm/providers/ollama/judgments.rb +26 -0
- data/lib/ruby_llm/providers/ollama/media.rb +6 -2
- data/lib/ruby_llm/providers/ollama/models.rb +2 -0
- data/lib/ruby_llm/providers/ollama.rb +11 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +3 -1
- data/lib/ruby_llm/providers/openai.rb +14 -0
- data/lib/ruby_llm/providers/openrouter/chat.rb +20 -24
- data/lib/ruby_llm/providers/openrouter/embeddings.rb +2 -0
- data/lib/ruby_llm/providers/openrouter/media.rb +5 -1
- data/lib/ruby_llm/providers/openrouter/streaming.rb +4 -1
- data/lib/ruby_llm/providers/openrouter/videos.rb +10 -5
- data/lib/ruby_llm/providers/openrouter.rb +4 -0
- data/lib/ruby_llm/providers/perplexity/chat.rb +4 -0
- data/lib/ruby_llm/providers/perplexity/media.rb +5 -11
- data/lib/ruby_llm/providers/perplexity/models.rb +8 -4
- data/lib/ruby_llm/providers/perplexity.rb +11 -0
- data/lib/ruby_llm/providers/typesafe.rb +32 -0
- data/lib/ruby_llm/providers/vertexai/credentials.rb +63 -0
- data/lib/ruby_llm/providers/vertexai/gemini.rb +2 -2
- data/lib/ruby_llm/providers/vertexai/transcription.rb +1 -0
- data/lib/ruby_llm/providers/vertexai.rb +27 -28
- data/lib/ruby_llm/providers/xai/chat.rb +6 -0
- data/lib/ruby_llm/providers/xai/images.rb +9 -10
- data/lib/ruby_llm/providers/xai/responses.rb +10 -8
- data/lib/ruby_llm/providers/xai/videos.rb +11 -9
- data/lib/ruby_llm/providers/xai.rb +4 -0
- data/lib/ruby_llm/railtie.rb +11 -0
- data/lib/ruby_llm/request_shape/part.rb +117 -0
- data/lib/ruby_llm/request_shape/problem.rb +63 -0
- data/lib/ruby_llm/request_shape/tool_round.rb +64 -0
- data/lib/ruby_llm/request_shape/turn.rb +54 -0
- data/lib/ruby_llm/request_shape.rb +160 -0
- data/lib/ruby_llm/rerank.rb +9 -2
- data/lib/ruby_llm/research_job.rb +21 -5
- data/lib/ruby_llm/score.rb +42 -0
- data/lib/ruby_llm/search_results.rb +2 -2
- data/lib/ruby_llm/server_tool_call.rb +10 -2
- data/lib/ruby_llm/speech.rb +8 -1
- data/lib/ruby_llm/support/cancellation.rb +39 -0
- data/lib/ruby_llm/support/inspectable.rb +3 -2
- data/lib/ruby_llm/support/instrumentation.rb +38 -1
- data/lib/ruby_llm/support/process_cache.rb +60 -0
- data/lib/ruby_llm/support/progress_reporter.rb +33 -0
- data/lib/ruby_llm/thinking.rb +2 -1
- data/lib/ruby_llm/tokens.rb +57 -14
- data/lib/ruby_llm/tool.rb +50 -3
- data/lib/ruby_llm/tool_call.rb +17 -2
- data/lib/ruby_llm/transcription.rb +10 -3
- data/lib/ruby_llm/transport/connection.rb +127 -44
- data/lib/ruby_llm/transport/error_middleware.rb +90 -24
- data/lib/ruby_llm/transport/event_stream_parser.rb +121 -0
- data/lib/ruby_llm/transport/json_response.rb +33 -0
- data/lib/ruby_llm/transport/signing_middleware.rb +29 -0
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm/video.rb +5 -3
- data/lib/ruby_llm/video_job.rb +48 -5
- data/lib/ruby_llm.rb +106 -5
- data/lib/tasks/ruby_llm/evaluations.rb +43 -0
- data/lib/tasks/ruby_llm.rake +6 -9
- data/skills/rubyllm/SKILL.md +33 -12
- metadata +95 -37
- data/lib/generators/ruby_llm/upgrade/legacy_content_sql.rb +0 -34
- data/lib/generators/ruby_llm/upgrade/online_copy_migration/data.rb +0 -341
- data/lib/generators/ruby_llm/upgrade/online_copy_migration/journal.rb +0 -171
- data/lib/generators/ruby_llm/upgrade/online_copy_migration/verification.rb +0 -197
- data/lib/generators/ruby_llm/upgrade/online_copy_migration.rb +0 -174
- data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +0 -468
- data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +0 -101
- data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +0 -247
- data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +0 -662
- data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +0 -251
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +0 -13
- data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +0 -359
data/lib/ruby_llm/models.rb
CHANGED
|
@@ -16,15 +16,18 @@ module RubyLLM
|
|
|
16
16
|
# Models.find delegate to the global registry.
|
|
17
17
|
class Models
|
|
18
18
|
include Enumerable
|
|
19
|
+
include Lookup
|
|
19
20
|
|
|
20
21
|
MODELS_DEV_PROVIDER_MAP = { # :nodoc:
|
|
21
22
|
'openai' => 'openai',
|
|
22
23
|
'anthropic' => 'anthropic',
|
|
24
|
+
'azure' => 'azure',
|
|
23
25
|
'google' => 'gemini',
|
|
24
26
|
'google-vertex' => 'vertexai',
|
|
25
27
|
'amazon-bedrock' => 'bedrock',
|
|
26
28
|
'cohere' => 'cohere',
|
|
27
29
|
'deepseek' => 'deepseek',
|
|
30
|
+
'hetzner' => 'hetzner',
|
|
28
31
|
'mistral' => 'mistral',
|
|
29
32
|
'ollama-cloud' => 'ollama_cloud',
|
|
30
33
|
'openrouter' => 'openrouter',
|
|
@@ -33,24 +36,11 @@ module RubyLLM
|
|
|
33
36
|
'xai' => 'xai'
|
|
34
37
|
}.freeze
|
|
35
38
|
MODELS_DEV_INPUT_MODALITIES = %w[text image audio pdf video file].freeze # :nodoc:
|
|
36
|
-
MODELS_DEV_OUTPUT_MODALITIES = %w[text image audio video embeddings moderation rerank].freeze # :nodoc:
|
|
39
|
+
MODELS_DEV_OUTPUT_MODALITIES = %w[text image audio video embeddings moderation rerank judgment].freeze # :nodoc:
|
|
37
40
|
# First-party providers outrank the aggregators that resell their models.
|
|
38
41
|
PROVIDER_PREFERENCE = %w[
|
|
39
|
-
openai
|
|
40
|
-
|
|
41
|
-
gemini
|
|
42
|
-
deepseek
|
|
43
|
-
mistral
|
|
44
|
-
cohere
|
|
45
|
-
perplexity
|
|
46
|
-
xai
|
|
47
|
-
vertexai
|
|
48
|
-
bedrock
|
|
49
|
-
openrouter
|
|
50
|
-
azure
|
|
51
|
-
ollama_cloud
|
|
52
|
-
ollama
|
|
53
|
-
gpustack
|
|
42
|
+
openai anthropic gemini deepseek mistral cohere typesafe perplexity xai
|
|
43
|
+
vertexai bedrock openrouter azure hetzner ollama_cloud ollama gpustack
|
|
54
44
|
].freeze # :nodoc:
|
|
55
45
|
INSTANCE_DELEGATES = (Enumerable.instance_methods(false) + %i[
|
|
56
46
|
all
|
|
@@ -68,6 +58,8 @@ module RubyLLM
|
|
|
68
58
|
load_from_store
|
|
69
59
|
save_to_json
|
|
70
60
|
]).uniq.freeze # :nodoc:
|
|
61
|
+
INSTANCE_LOCK = Mutex.new
|
|
62
|
+
private_constant :INSTANCE_LOCK
|
|
71
63
|
|
|
72
64
|
class << self
|
|
73
65
|
# The providers whose model list could not be fetched during the last
|
|
@@ -82,7 +74,7 @@ module RubyLLM
|
|
|
82
74
|
end
|
|
83
75
|
|
|
84
76
|
def instance # :nodoc:
|
|
85
|
-
@instance ||= new
|
|
77
|
+
@instance || INSTANCE_LOCK.synchronize { @instance ||= new }
|
|
86
78
|
end
|
|
87
79
|
|
|
88
80
|
def bundled_registry_file # :nodoc:
|
|
@@ -229,7 +221,7 @@ module RubyLLM
|
|
|
229
221
|
|
|
230
222
|
connection = Transport::Connection.basic do |f|
|
|
231
223
|
f.request :json
|
|
232
|
-
f.
|
|
224
|
+
f.use Transport::JsonResponse, parser_options: { symbolize_names: true }
|
|
233
225
|
end
|
|
234
226
|
{ models: parse_models_dev_catalog(connection.get('https://models.dev/api.json').body), fetched: true }
|
|
235
227
|
rescue StandardError => e
|
|
@@ -540,7 +532,7 @@ module RubyLLM
|
|
|
540
532
|
# :startdoc:
|
|
541
533
|
|
|
542
534
|
def initialize(models = nil) # :nodoc:
|
|
543
|
-
|
|
535
|
+
replace_models(models || self.class.load_models)
|
|
544
536
|
end
|
|
545
537
|
|
|
546
538
|
# Replaces the models in this registry with those read from the JSON
|
|
@@ -548,7 +540,7 @@ module RubyLLM
|
|
|
548
540
|
# <tt>RubyLLM.config.model_registry_file</tt>. A missing or invalid
|
|
549
541
|
# file falls back to the registry bundled with the gem.
|
|
550
542
|
def load_from_json(file = RubyLLM.config.model_registry_file)
|
|
551
|
-
|
|
543
|
+
replace_models(self.class.models_from_file(file) || self.class.models_from_bundle)
|
|
552
544
|
self
|
|
553
545
|
end
|
|
554
546
|
|
|
@@ -558,7 +550,7 @@ module RubyLLM
|
|
|
558
550
|
store = RubyLLM.config.model_registry_store
|
|
559
551
|
raise ModelRegistryError, 'No model registry store is configured' unless store
|
|
560
552
|
|
|
561
|
-
|
|
553
|
+
replace_models(Array(store.read))
|
|
562
554
|
self
|
|
563
555
|
end
|
|
564
556
|
|
|
@@ -680,14 +672,14 @@ module RubyLLM
|
|
|
680
672
|
merged_models = self.class.merge_models(self.class.models_from_provider_gems, main_models)
|
|
681
673
|
persisted_models = RubyLLM.config.model_registry_store ? merged_models : main_models
|
|
682
674
|
persist_registry!(persisted_models, published:)
|
|
683
|
-
|
|
675
|
+
replace_models(stored_models || merged_models)
|
|
684
676
|
payload.merge!(model_count: all.size, not_modified: published.not_modified)
|
|
685
677
|
end
|
|
686
678
|
self
|
|
687
679
|
end
|
|
688
680
|
|
|
689
681
|
def refresh_from_providers(remote_only: false) # :nodoc:
|
|
690
|
-
|
|
682
|
+
replace_models(self.class.fetch_merged_models(remote_only: remote_only))
|
|
691
683
|
self
|
|
692
684
|
end
|
|
693
685
|
|
|
@@ -697,6 +689,11 @@ module RubyLLM
|
|
|
697
689
|
|
|
698
690
|
private
|
|
699
691
|
|
|
692
|
+
def replace_models(models)
|
|
693
|
+
@models = models.dup
|
|
694
|
+
@model_index = nil
|
|
695
|
+
end
|
|
696
|
+
|
|
700
697
|
# Filters keep the unlisted entries so #find and #unlisted still see them
|
|
701
698
|
# after a chain such as by_provider(:openai).unlisted.
|
|
702
699
|
def select_models(&)
|
|
@@ -792,51 +789,5 @@ module RubyLLM
|
|
|
792
789
|
|
|
793
790
|
store.respond_to?(:description) ? store.description : store.class.name
|
|
794
791
|
end
|
|
795
|
-
|
|
796
|
-
def find_with_provider(model_id, provider, config = nil)
|
|
797
|
-
resolved_id = Aliases.resolve(model_id, provider)
|
|
798
|
-
resolved_id = resolve_provider_registry_id(resolved_id, provider, config)
|
|
799
|
-
all_including_unlisted.find { |m| m.id == resolved_id && m.provider == provider.to_s } ||
|
|
800
|
-
all_including_unlisted.find { |m| m.id == model_id && m.provider == provider.to_s } ||
|
|
801
|
-
raise_model_not_found(model_id, provider: provider)
|
|
802
|
-
end
|
|
803
|
-
|
|
804
|
-
def resolve_provider_registry_id(model_id, provider, config = nil)
|
|
805
|
-
provider_class = Provider.resolve(provider)
|
|
806
|
-
return model_id unless provider_class
|
|
807
|
-
|
|
808
|
-
provider_class.resolve_registry_id(model_id, self, config || RubyLLM.config)
|
|
809
|
-
end
|
|
810
|
-
|
|
811
|
-
# A name can be one provider's exact id and another's alias:
|
|
812
|
-
# claude-opus-4 is exact on vertexai, an alias on anthropic.
|
|
813
|
-
# Provider preference settles it, not the kind of match.
|
|
814
|
-
def find_without_provider(model_id)
|
|
815
|
-
resolved_id = Aliases.resolve(model_id)
|
|
816
|
-
matches = all_including_unlisted.select { |m| [model_id, resolved_id].include?(m.id) }
|
|
817
|
-
.sort_by { |m| m.id == model_id ? 0 : 1 }
|
|
818
|
-
|
|
819
|
-
preferred_match(matches) || raise_model_not_found(model_id)
|
|
820
|
-
end
|
|
821
|
-
|
|
822
|
-
def raise_model_not_found(model_id, provider: nil)
|
|
823
|
-
message = "Unknown model: #{model_id.inspect}"
|
|
824
|
-
message = "#{message} for provider: #{provider.inspect}" if provider
|
|
825
|
-
|
|
826
|
-
raise ModelNotFoundError, "#{message}. #{refresh_registry_guidance}"
|
|
827
|
-
end
|
|
828
|
-
|
|
829
|
-
def refresh_registry_guidance
|
|
830
|
-
'If the model exists at the provider, refresh the registry with `RubyLLM.models.refresh`.'
|
|
831
|
-
end
|
|
832
|
-
|
|
833
|
-
def preferred_match(candidates)
|
|
834
|
-
return candidates.first if candidates.size == 1
|
|
835
|
-
|
|
836
|
-
candidates.min_by do |model|
|
|
837
|
-
index = PROVIDER_PREFERENCE.index(model.provider)
|
|
838
|
-
[model.unlisted? ? 1 : 0, index || PROVIDER_PREFERENCE.length]
|
|
839
|
-
end
|
|
840
|
-
end
|
|
841
792
|
end
|
|
842
793
|
end
|
data/lib/ruby_llm/moderation.rb
CHANGED
|
@@ -81,6 +81,8 @@ module RubyLLM
|
|
|
81
81
|
# +model+ is not given. Pass +provider:+ and <tt>assume_model_exists: true</tt>
|
|
82
82
|
# to use a model that is not in the registry. An explicitly selected
|
|
83
83
|
# provider may instead use a configured resource without a model.
|
|
84
|
+
# +owner:+ attributes the usage to a record, such as a user, and wins
|
|
85
|
+
# over RubyLLM.with_usage_owner.
|
|
84
86
|
#
|
|
85
87
|
# RubyLLM.moderate("User message")
|
|
86
88
|
# RubyLLM.moderate(["First comment", "Second comment"]).results
|
|
@@ -93,7 +95,8 @@ module RubyLLM
|
|
|
93
95
|
assume_model_exists: false,
|
|
94
96
|
context: nil,
|
|
95
97
|
provider_options: {},
|
|
96
|
-
metadata: nil
|
|
98
|
+
metadata: nil,
|
|
99
|
+
owner: nil)
|
|
97
100
|
attachments = Attachment.wrap(with)
|
|
98
101
|
raise ArgumentError, 'must provide input text, image attachment, or both' if input.nil? && attachments.empty?
|
|
99
102
|
|
|
@@ -116,7 +119,9 @@ module RubyLLM
|
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
RubyLLM.instrument('moderation.ruby_llm', payload, config: config) do |event|
|
|
119
|
-
result =
|
|
122
|
+
result = Accounting::Usage.owned_by(owner) do
|
|
123
|
+
provider_instance.moderate(input, model:, with: attachments, provider_options:)
|
|
124
|
+
end
|
|
120
125
|
event[:result] = result
|
|
121
126
|
event[:flagged] = result.flagged?
|
|
122
127
|
event[:tokens] = result.tokens
|
data/lib/ruby_llm/ocr.rb
CHANGED
|
@@ -41,6 +41,8 @@ module RubyLLM
|
|
|
41
41
|
# the given zero-based page indexes. +provider_options:+ merges options
|
|
42
42
|
# into the request in the provider's own vocabulary, such as Mistral's
|
|
43
43
|
# +include_image_base64:+ or +table_format:+.
|
|
44
|
+
# +owner:+ attributes the usage to a record, such as a user, and wins
|
|
45
|
+
# over RubyLLM.with_usage_owner.
|
|
44
46
|
#
|
|
45
47
|
# RubyLLM.ocr("report.pdf")
|
|
46
48
|
# RubyLLM.ocr("https://example.com/scan.png")
|
|
@@ -55,7 +57,8 @@ module RubyLLM
|
|
|
55
57
|
context: nil,
|
|
56
58
|
pages: nil,
|
|
57
59
|
provider_options: {},
|
|
58
|
-
metadata: nil
|
|
60
|
+
metadata: nil,
|
|
61
|
+
owner: nil)
|
|
59
62
|
config = context&.config || RubyLLM.config
|
|
60
63
|
model ||= config.default_ocr_model
|
|
61
64
|
model, provider_instance = Models.resolve(model, provider: provider, assume_model_exists: assume_model_exists,
|
|
@@ -71,7 +74,9 @@ module RubyLLM
|
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
RubyLLM.instrument('ocr.ruby_llm', payload, config: config) do |event|
|
|
74
|
-
result =
|
|
77
|
+
result = Accounting::Usage.owned_by(owner) do
|
|
78
|
+
provider_instance.ocr(file, model:, pages:, provider_options:)
|
|
79
|
+
end
|
|
75
80
|
event[:result] = result
|
|
76
81
|
event[:response_model] = result.model
|
|
77
82
|
result
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class OpenTelemetry
|
|
5
|
+
module Attributes # :nodoc:
|
|
6
|
+
OPERATIONS = {
|
|
7
|
+
'chat.ruby_llm' => 'chat',
|
|
8
|
+
'embedding.ruby_llm' => 'embeddings',
|
|
9
|
+
'image.ruby_llm' => 'generate_content',
|
|
10
|
+
'speech.ruby_llm' => 'generate_content',
|
|
11
|
+
'transcription.ruby_llm' => 'transcription',
|
|
12
|
+
'ocr.ruby_llm' => 'ocr',
|
|
13
|
+
'rerank.ruby_llm' => 'rerank',
|
|
14
|
+
'moderation.ruby_llm' => 'moderation',
|
|
15
|
+
'judgment.ruby_llm' => 'judgment',
|
|
16
|
+
'tool_call.ruby_llm' => 'execute_tool',
|
|
17
|
+
'workflow.ruby_llm' => 'invoke_workflow',
|
|
18
|
+
'workflow_step.ruby_llm' => 'ruby_llm.workflow_step'
|
|
19
|
+
}.freeze
|
|
20
|
+
PROVIDERS = {
|
|
21
|
+
'bedrock' => 'aws.bedrock',
|
|
22
|
+
'azure' => 'azure.ai.openai',
|
|
23
|
+
'gemini' => 'gcp.gen_ai',
|
|
24
|
+
'vertexai' => 'gcp.vertex_ai',
|
|
25
|
+
'mistral' => 'mistral_ai',
|
|
26
|
+
'xai' => 'x_ai'
|
|
27
|
+
}.freeze
|
|
28
|
+
INTERNAL_OPERATIONS = %w[execute_tool invoke_workflow ruby_llm.workflow_step].freeze
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
def span_name(operation, payload)
|
|
33
|
+
target = case operation
|
|
34
|
+
when 'execute_tool' then payload[:tool_name]
|
|
35
|
+
when 'invoke_workflow' then payload[:workflow_name]
|
|
36
|
+
when 'ruby_llm.workflow_step' then payload[:workflow_step_name]
|
|
37
|
+
else payload[:model]
|
|
38
|
+
end
|
|
39
|
+
[operation, target].compact.join(' ')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def span_kind(operation)
|
|
43
|
+
INTERNAL_OPERATIONS.include?(operation) ? :internal : :client
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def request(operation, payload, event:)
|
|
47
|
+
return workflow(operation, payload) if %w[invoke_workflow ruby_llm.workflow_step].include?(operation)
|
|
48
|
+
return tool(payload) if operation == 'execute_tool'
|
|
49
|
+
|
|
50
|
+
{
|
|
51
|
+
'gen_ai.operation.name' => operation,
|
|
52
|
+
'gen_ai.provider.name' => PROVIDERS.fetch(payload[:provider], payload[:provider]),
|
|
53
|
+
'gen_ai.request.model' => payload[:model],
|
|
54
|
+
'gen_ai.request.temperature' => payload[:temperature],
|
|
55
|
+
'gen_ai.request.max_tokens' => payload[:max_output_tokens],
|
|
56
|
+
'gen_ai.request.stream' => (true if payload[:streaming]),
|
|
57
|
+
'gen_ai.embeddings.dimension.count' => payload[:dimensions],
|
|
58
|
+
'gen_ai.output.type' => output_type(event, payload)
|
|
59
|
+
}.compact
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def output_type(event, payload)
|
|
63
|
+
return 'json' if payload[:schema]
|
|
64
|
+
return 'image' if event == 'image.ruby_llm'
|
|
65
|
+
return 'speech' if event == 'speech.ruby_llm'
|
|
66
|
+
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def response(payload)
|
|
71
|
+
response = payload[:response]
|
|
72
|
+
attributes = {
|
|
73
|
+
'gen_ai.response.model' => payload[:response_model],
|
|
74
|
+
'gen_ai.response.finish_reasons' => ([response.finish_reason.to_s] if response&.finish_reason)
|
|
75
|
+
}.compact
|
|
76
|
+
attributes.merge!(tokens(payload[:response_tokens] || payload[:tokens])) if payload[:tokens]
|
|
77
|
+
attributes
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def tokens(tokens)
|
|
81
|
+
input = [tokens.input, tokens.cache_read, tokens.cache_write].compact
|
|
82
|
+
{
|
|
83
|
+
'gen_ai.usage.input_tokens' => (input.sum unless input.empty?),
|
|
84
|
+
'gen_ai.usage.output_tokens' => tokens.output,
|
|
85
|
+
'gen_ai.usage.cache_read.input_tokens' => tokens.cache_read,
|
|
86
|
+
'gen_ai.usage.cache_write.input_tokens' => tokens.cache_write,
|
|
87
|
+
'gen_ai.usage.reasoning.output_tokens' => tokens.thinking
|
|
88
|
+
}.compact
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def tool(payload)
|
|
92
|
+
{
|
|
93
|
+
'gen_ai.operation.name' => 'execute_tool',
|
|
94
|
+
'gen_ai.tool.name' => payload[:tool_name],
|
|
95
|
+
'gen_ai.tool.call.id' => payload[:tool_call_id],
|
|
96
|
+
'gen_ai.tool.type' => 'function'
|
|
97
|
+
}.compact
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def workflow(operation, payload)
|
|
101
|
+
{
|
|
102
|
+
'gen_ai.operation.name' => (operation unless operation == 'ruby_llm.workflow_step'),
|
|
103
|
+
'gen_ai.workflow.name' => payload[:workflow_name],
|
|
104
|
+
'ruby_llm.workflow.id' => payload[:workflow_id],
|
|
105
|
+
'ruby_llm.workflow.step.name' => payload[:workflow_step_name],
|
|
106
|
+
'ruby_llm.workflow.step.id' => payload[:workflow_step_id]
|
|
107
|
+
}.compact
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# Optional OpenTelemetry tracing through RubyLLM's instrumentation events.
|
|
5
|
+
# Configure your application's SDK and exporters before calling .enable.
|
|
6
|
+
class OpenTelemetry
|
|
7
|
+
@mutex = Mutex.new
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
# Enables tracing without replacing the configured instrumenter.
|
|
11
|
+
# Requires the optional opentelemetry-api gem. Repeated calls are harmless.
|
|
12
|
+
# The application owns the tracer provider, SDK, and exporters.
|
|
13
|
+
def enable
|
|
14
|
+
require 'opentelemetry-api'
|
|
15
|
+
@mutex.synchronize do
|
|
16
|
+
@subscriber ||= new
|
|
17
|
+
Support::Instrumentation.subscribe(@subscriber)
|
|
18
|
+
end
|
|
19
|
+
nil
|
|
20
|
+
rescue LoadError
|
|
21
|
+
raise LoadError, "OpenTelemetry tracing requires the 'opentelemetry-api' gem. Add it to your Gemfile."
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Disables tracing for new operations without shutting down the SDK.
|
|
25
|
+
# Operations already running finish their spans normally.
|
|
26
|
+
def disable
|
|
27
|
+
@mutex.synchronize { Support::Instrumentation.unsubscribe(@subscriber) if @subscriber }
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def instrument(name, payload) # :nodoc:
|
|
33
|
+
operation = Attributes::OPERATIONS[name]
|
|
34
|
+
return yield unless operation
|
|
35
|
+
|
|
36
|
+
span = safely do
|
|
37
|
+
tracer.start_span(Attributes.span_name(operation, payload),
|
|
38
|
+
kind: Attributes.span_kind(operation),
|
|
39
|
+
attributes: Attributes.request(operation, payload, event: name))
|
|
40
|
+
end
|
|
41
|
+
return yield unless span
|
|
42
|
+
|
|
43
|
+
::OpenTelemetry::Trace.with_span(span) do
|
|
44
|
+
yield
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
record_error(span, e)
|
|
47
|
+
raise
|
|
48
|
+
ensure
|
|
49
|
+
safely { span.add_attributes(Attributes.response(payload)) }
|
|
50
|
+
safely { span.finish }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def capture_context # :nodoc:
|
|
55
|
+
::OpenTelemetry::Context.current
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def with_context(context, &) # :nodoc:
|
|
59
|
+
::OpenTelemetry::Context.with_current(context, &)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def record_error(span, error)
|
|
65
|
+
safely do
|
|
66
|
+
span.set_attribute('error.type', error.class.name)
|
|
67
|
+
span.status = ::OpenTelemetry::Trace::Status.error
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def tracer
|
|
72
|
+
::OpenTelemetry.tracer_provider.tracer('ruby_llm', RubyLLM::VERSION)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def safely
|
|
76
|
+
yield
|
|
77
|
+
rescue StandardError => e
|
|
78
|
+
RubyLLM.logger.warn("OpenTelemetry instrumentation failed (#{e.class})")
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# The probability that a judgment's yes/no question is true.
|
|
5
|
+
class Probability
|
|
6
|
+
include Support::Inspectable
|
|
7
|
+
|
|
8
|
+
# Returns the probability of yes, between 0 and 1.
|
|
9
|
+
attr_reader :probability
|
|
10
|
+
|
|
11
|
+
def initialize(probability:) # :nodoc:
|
|
12
|
+
@probability = probability
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Returns :probability.
|
|
17
|
+
def type
|
|
18
|
+
:probability
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Returns the answer as a Hash.
|
|
22
|
+
def to_h
|
|
23
|
+
{ type:, probability: }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def inspect_attributes # :nodoc:
|
|
27
|
+
{ probability: }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# Progress a tool or an MCP server reports while it works. A Tool reports
|
|
5
|
+
# it with Tool#progress; Chat#after_tool_progress and MCP.after_progress
|
|
6
|
+
# callbacks receive it.
|
|
7
|
+
#
|
|
8
|
+
# chat.after_tool_progress do |tool_call, progress|
|
|
9
|
+
# puts "#{tool_call.name}: #{progress.message} #{progress.value}/#{progress.total}"
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
class Progress
|
|
13
|
+
include Support::Inspectable
|
|
14
|
+
|
|
15
|
+
# How much work is done, or +nil+ when only a message was reported. It
|
|
16
|
+
# only grows, but its unit is the reporter's.
|
|
17
|
+
attr_reader :value
|
|
18
|
+
|
|
19
|
+
# How much work there is in total, or +nil+ when the reporter does not
|
|
20
|
+
# know.
|
|
21
|
+
attr_reader :total
|
|
22
|
+
|
|
23
|
+
# What the tool or server is doing, or +nil+.
|
|
24
|
+
attr_reader :message
|
|
25
|
+
|
|
26
|
+
def initialize(value: nil, total: nil, message: nil) # :nodoc:
|
|
27
|
+
@value = value
|
|
28
|
+
@total = total
|
|
29
|
+
@message = message
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns the share of work done, from 0.0 to 1.0, or +nil+ without a
|
|
33
|
+
# value and a total.
|
|
34
|
+
def fraction
|
|
35
|
+
value.to_f / total if value && total&.positive?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def inspect_attributes
|
|
41
|
+
{ value:, total:, message: }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The object a prompt template is evaluated in. Locals become local
|
|
4
|
+
# variables of the template, and +render+ inserts a partial.
|
|
5
|
+
#
|
|
6
|
+
# The class is declared with its full name so the template binding has no
|
|
7
|
+
# RubyLLM lexical scope: +Chat+ in a template is the application's model,
|
|
8
|
+
# not RubyLLM::Chat.
|
|
9
|
+
class RubyLLM::Prompt::Context # rubocop:disable Style/ClassAndModuleChildren
|
|
10
|
+
LOCAL_VARIABLE_NAME = /\A(?![A-Z0-9])(?:[[:alnum:]_]|[^\0-\177])+\z/
|
|
11
|
+
private_constant :LOCAL_VARIABLE_NAME
|
|
12
|
+
|
|
13
|
+
# Returns the locals passed to the current prompt or partial. Use it to
|
|
14
|
+
# read an optional local, or a local whose name is not a valid Ruby
|
|
15
|
+
# variable name:
|
|
16
|
+
#
|
|
17
|
+
# <% if local_assigns[:product_name] %>
|
|
18
|
+
#
|
|
19
|
+
attr_reader :local_assigns
|
|
20
|
+
|
|
21
|
+
def initialize(prompt, local_assigns) # :nodoc:
|
|
22
|
+
@prompt = prompt
|
|
23
|
+
@local_assigns = local_assigns
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Renders a partial with locals. A bare name is looked up next to the
|
|
27
|
+
# current prompt only. A name with a path is looked up in the prompt roots.
|
|
28
|
+
#
|
|
29
|
+
# <%= render "tone" %>
|
|
30
|
+
# <%= render "shared/safety", product_name: product_name %>
|
|
31
|
+
# <%= render partial: "shared/safety", locals: { product_name: product_name } %>
|
|
32
|
+
#
|
|
33
|
+
def render(options = {}, locals = {})
|
|
34
|
+
return partial(options).render(**locals) unless options.is_a?(Hash)
|
|
35
|
+
|
|
36
|
+
partial(options.fetch(:partial)).render(**(options[:locals] || {}))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def scope # :nodoc:
|
|
40
|
+
@local_assigns.each_with_object(binding) do |(name, value), scope|
|
|
41
|
+
scope.local_variable_set(name, value) if name.to_s.match?(LOCAL_VARIABLE_NAME)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def partial(name)
|
|
48
|
+
name = name.to_s
|
|
49
|
+
partial = name.sub(%r{([^/]+)\z}, '_\\1')
|
|
50
|
+
directory = File.dirname(@prompt.name)
|
|
51
|
+
return RubyLLM::Prompt.new(partial) if name.include?('/') || directory == '.'
|
|
52
|
+
|
|
53
|
+
RubyLLM::Prompt.new("#{directory}/#{partial}")
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/ruby_llm/prompt.rb
CHANGED
|
@@ -50,7 +50,7 @@ module RubyLLM
|
|
|
50
50
|
def render(**locals) # :nodoc:
|
|
51
51
|
raise PromptNotFoundError, "Prompt file not found: #{path}" unless File.exist?(path)
|
|
52
52
|
|
|
53
|
-
ERB.new(File.read(path)).
|
|
53
|
+
ERB.new(File.read(path)).result(Context.new(self, locals).scope)
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
def self.render(name, **locals) # :nodoc:
|
|
@@ -8,7 +8,7 @@ module RubyLLM
|
|
|
8
8
|
fallback = Streaming::StreamState.new
|
|
9
9
|
on_data = binary_on_data(fallback, progress, &)
|
|
10
10
|
|
|
11
|
-
response = @connection.post(url, payload, usage: @usage_tracker) do |request|
|
|
11
|
+
response = @connection.post(url, payload, usage: @usage_tracker, stream: true) do |request|
|
|
12
12
|
(request.options.context ||= {})[Transport::Connection::STREAM_PROGRESS_KEY] = progress
|
|
13
13
|
if faraday_1?
|
|
14
14
|
request.options[:on_data] = on_data
|