ruby_llm-instrumentation 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 67942ad10a363e6e530277085f6750ea53eb547e88df806f19ddbbebef286adc
4
+ data.tar.gz: 4a2208687976b6744d1cbe6e19aa03d0a5bf9d080f4597ad1341e8d9cf590a4b
5
+ SHA512:
6
+ metadata.gz: f9190dea5c2c5d4c8824f4e93c278a527c770204370b93a0e69d622cd09369597674ceb1dc6fa72e499b5e6858afb95a83d1582d8ed67fa9cac44b53fd94e95f
7
+ data.tar.gz: 254a5922c0a6fa80c8d907de2575e8d43cff803f0d4c9c1892d3285ef16159bf53abb61e73a49552345bdd83566b9837c5509327cad344cce9f99e41bfce2ddf
data/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # RubyLLM::Instrumentation
2
+
3
+ Rails instrumentation for RubyLLM.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem "ruby_llm-instrumentation"
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Now, RubyLLM will instrument all calls to your configured LLM.
20
+
21
+ ## Usage
22
+
23
+ RubyLLM::Instrumentation uses ActiveSupport::Notifications to publish events. You can subscribe to these events to build custom monitoring, logging, or analytics:
24
+
25
+ ```ruby
26
+ # Subscribe to all LLM events
27
+ ActiveSupport::Notifications.subscribe(/ruby_llm/) do |name, start, finish, id, payload|
28
+ duration = finish - start
29
+
30
+ Rails.logger.info "LLM Call: #{payload[:provider]}/#{payload[:model]}"
31
+ Rails.logger.info "Duration: #{duration}ms"
32
+ Rails.logger.info "Input tokens: #{payload[:input_tokens]}"
33
+ Rails.logger.info "Output tokens: #{payload[:output_tokens]}"
34
+ end
35
+ ```
36
+
37
+ ## Available Events
38
+
39
+ ### RubyLLM::Chat
40
+
41
+ #### complete_chat.ruby_llm
42
+
43
+ Triggered when `#ask` is called.
44
+
45
+ | Key | Value |
46
+ | --------------------- | --------------------------------------- |
47
+ | provider | Provider slug |
48
+ | model | Model ID |
49
+ | streaming | Whether streaming was used |
50
+ | chat | The chat, a RubyLLM::Chat object |
51
+ | response | The response, a RubyLLM::Message object |
52
+ | input_tokens | Input tokens consumed |
53
+ | output_tokens | Output tokens consumed |
54
+ | cached_tokens | Cache reads tokens (if supported) |
55
+ | cache_creation_tokens | Cache write tokens (if supported) |
56
+
57
+ #### execute_tool.ruby_llm
58
+
59
+ Triggered when `#execute_tool` is called.
60
+
61
+ | Key | Value |
62
+ | --------- | --------------------------------------------------- |
63
+ | provider | Provider slug |
64
+ | model | Model ID |
65
+ | tool_call | The tool call, a RubyLLM::ToolCall object |
66
+ | tool_name | The tool name |
67
+ | arguments | The arguments |
68
+ | chat | The chat, a RubyLLM::Chat instance |
69
+ | halted | Indicates if the tool stopped the conversation loop |
70
+
71
+ ### RubyLLM::Embedding
72
+
73
+ #### embed_text.ruby_llm
74
+
75
+ Triggered when `.embed` is called.
76
+
77
+ | Key | Value |
78
+ | ------------ | -------------------------------------------------------------- |
79
+ | provider | Provider slug |
80
+ | embedding | The embedding, a RubyLLM::Embedding object |
81
+ | model | Model ID |
82
+ | dimensions | Number of embedding dimensions (or array of sizes if multiple) |
83
+ | input_tokens | Input tokens consumed |
84
+ | vector_count | Number of vectors generated |
85
+
86
+ ### RubyLLM::Image
87
+
88
+ #### paint_image.ruby_llm
89
+
90
+ Triggered when `.paint` is called.
91
+
92
+ | Key | Value |
93
+ | -------- | -------------------------------------------- |
94
+ | provider | Provider slug |
95
+ | size | Image dimensions |
96
+ | image | The inage generated, a RubyLLM::Image object |
97
+ | model | Model ID |
98
+
99
+ ### RubyLLM::Moderation
100
+
101
+ #### moderate_text.ruby_llm
102
+
103
+ Triggered when `.moderate` is called.
104
+
105
+ | Key | Value |
106
+ | ---------- | -------------------------------------------- |
107
+ | provider | Provider slug |
108
+ | moderation | The moderation, a RubyLLM::Moderation object |
109
+ | model | Model ID |
110
+ | flagged | Whether the text was flagged |
111
+
112
+ ### RubyLLM::Transcription
113
+
114
+ #### transcribe_audio.ruby_llm
115
+
116
+ Triggered when `.transcribe` is called.
117
+
118
+ | Key | Value |
119
+ | ------------- | -------------------------------------------------- |
120
+ | provider | Provider slug |
121
+ | transcription | The transcription, a RubyLLM::Transcription object |
122
+ | model | Model ID |
123
+ | input_tokens | Input tokens consumed |
124
+ | output_tokens | Output tokens consumed |
125
+ | duration | Audio duration in seconds (if available) |
126
+
127
+ ## Contributing
128
+
129
+ You can open an issue or a PR in GitHub.
130
+
131
+ ## License
132
+
133
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
@@ -0,0 +1,49 @@
1
+ module RubyLLM
2
+ module Instrumentation
3
+ module Chat
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ alias_method :original_complete, :complete
8
+ def complete(&)
9
+ raw_payload = {
10
+ provider: @provider.slug,
11
+ model: @model.id,
12
+ streaming: block_given?
13
+ }
14
+
15
+ ActiveSupport::Notifications.instrument("complete_chat.ruby_llm", raw_payload) do |payload|
16
+ original_complete(&).tap do |response|
17
+ payload[:response] = response
18
+ %i[input_tokens output_tokens cached_tokens cache_creation_tokens].each do |field|
19
+ value = response.public_send(field)
20
+ payload[field] = value unless value.nil?
21
+ end
22
+ end
23
+ ensure
24
+ payload[:chat] = self
25
+ end
26
+ end
27
+
28
+ alias_method :original_execute_tool, :execute_tool
29
+ def execute_tool(tool_call)
30
+ raw_payload = {
31
+ provider: @provider.slug,
32
+ model: @model.id,
33
+ tool_call: tool_call,
34
+ tool_name: tool_call.name,
35
+ arguments: tool_call.arguments
36
+ }
37
+
38
+ ActiveSupport::Notifications.instrument("execute_tool.ruby_llm", raw_payload) do |payload|
39
+ original_execute_tool(tool_call).tap do |response|
40
+ payload[:halted] = response.is_a?(Tool::Halt)
41
+ end
42
+ ensure
43
+ payload[:chat] = self
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,30 @@
1
+ module RubyLLM
2
+ module Instrumentation
3
+ module Embedding
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class << self
8
+ alias_method :original_embed, :embed
9
+ def embed(text, model: nil, provider: nil, assume_model_exists: false, context: nil, dimensions: nil)
10
+ raw_payload = {
11
+ provider:
12
+ }
13
+
14
+ ActiveSupport::Notifications.instrument("embed_text.ruby_llm", raw_payload) do |payload|
15
+ original_embed(text, model:, provider:, assume_model_exists:, context:, dimensions:).tap do |response|
16
+ payload[:embedding] = response
17
+ payload[:model] = response.model
18
+ payload[:input_tokens] = response.input_tokens unless response.input_tokens.nil?
19
+
20
+ # response.vectors is an array of floats, or an array of an array of floats
21
+ payload[:vector_count] = response.vectors.first.is_a?(Array) ? response.vectors.size : 1
22
+ payload[:dimensions] = response.vectors.first.is_a?(Array) ? response.vectors.map(&:size) : response.vectors.size
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,26 @@
1
+ module RubyLLM
2
+ module Instrumentation
3
+ module Image
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class << self
8
+ alias_method :original_paint, :paint
9
+ def paint(prompt, model: nil, provider: nil, assume_model_exists: false, size: "1024x1024", context: nil)
10
+ raw_payload = {
11
+ provider:,
12
+ size:
13
+ }
14
+
15
+ ActiveSupport::Notifications.instrument("paint_image.ruby_llm", raw_payload) do |payload|
16
+ original_paint(prompt, model:, provider:, assume_model_exists:, size:, context:).tap do |response|
17
+ payload[:image] = response
18
+ payload[:model] = response.model
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ module RubyLLM
2
+ module Instrumentation
3
+ module Moderation
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class << self
8
+ alias_method :original_moderate, :moderate
9
+ def moderate(input, model: nil, provider: nil, assume_model_exists: false, context: nil)
10
+ raw_payload = {
11
+ provider:
12
+ }
13
+
14
+ ActiveSupport::Notifications.instrument("moderate_text.ruby_llm", raw_payload) do |payload|
15
+ original_moderate(input, model:, provider:, assume_model_exists:, context:).tap do |response|
16
+ payload[:moderation] = response
17
+ payload[:model] = response.model
18
+ payload[:flagged] = response.flagged?
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,23 @@
1
+ module RubyLLM
2
+ module Instrumentation
3
+ class Railtie < ::Rails::Railtie
4
+ INFLECTION_OVERRIDES = { "ruby_llm" => "RubyLLM" }.freeze
5
+
6
+ initializer "ruby_llm_instrumentation.inflector", after: "ruby_llm.inflections", before: :set_autoload_paths do
7
+ ActiveSupport::Inflector.inflections(:en) do |inflections|
8
+ # The RubyLLM gem registers "RubyLLM" as an acronym in its railtie,
9
+ # which breaks underscore conversion (RubyLLM.underscore => "rubyllm").
10
+ # We need to remove it and use "LLM" as an acronym instead for proper conversion:
11
+ # * "ruby_llm".camelize => "RubyLLM" (not "RubyLlm")
12
+ # * "RubyLLM".underscore => "ruby_llm" (not "rubyllm")
13
+ inflections.acronyms.delete("rubyllm")
14
+ inflections.acronym("LLM")
15
+ end
16
+
17
+ Rails.autoloaders.each do |loader|
18
+ loader.inflector.inflect(INFLECTION_OVERRIDES)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ module RubyLLM
2
+ module Instrumentation
3
+ module Transcription
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class << self
8
+ alias_method :original_transcribe, :transcribe
9
+ def transcribe(audio_file, **kwargs)
10
+ raw_payload = {
11
+ provider: kwargs[:provider]
12
+ }
13
+
14
+ ActiveSupport::Notifications.instrument("transcribe_audio.ruby_llm", raw_payload) do |payload|
15
+ original_transcribe(audio_file, **kwargs).tap do |response|
16
+ payload[:transcription] = response
17
+ payload[:model] = response.model
18
+ %i[input_tokens output_tokens duration].each do |field|
19
+ value = response.public_send(field)
20
+ payload[field] = value unless value.nil?
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,5 @@
1
+ module RubyLLM
2
+ module Instrumentation
3
+ VERSION = "0.1.1"
4
+ end
5
+ end
@@ -0,0 +1,19 @@
1
+ require "ruby_llm"
2
+ require "ruby_llm/instrumentation/version"
3
+ require "ruby_llm/instrumentation/railtie"
4
+
5
+ module RubyLLM
6
+ module Instrumentation
7
+ autoload :Chat, "ruby_llm/instrumentation/chat"
8
+ autoload :Embedding, "ruby_llm/instrumentation/embedding"
9
+ autoload :Image, "ruby_llm/instrumentation/image"
10
+ autoload :Transcription, "ruby_llm/instrumentation/transcription"
11
+ autoload :Moderation, "ruby_llm/instrumentation/moderation"
12
+ end
13
+ end
14
+
15
+ RubyLLM::Chat.include RubyLLM::Instrumentation::Chat
16
+ RubyLLM::Embedding.include RubyLLM::Instrumentation::Embedding
17
+ RubyLLM::Image.include RubyLLM::Instrumentation::Image
18
+ RubyLLM::Transcription.include RubyLLM::Instrumentation::Transcription
19
+ RubyLLM::Moderation.include RubyLLM::Instrumentation::Moderation
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :ruby_llm_instrumentation do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby_llm-instrumentation
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Patricio Mac Adden
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 7.0.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 7.0.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: ruby_llm
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ description: Rails instrumentation for RubyLLM
41
+ email:
42
+ - patricio.macadden@sinaptia.dev
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - README.md
48
+ - Rakefile
49
+ - lib/ruby_llm/instrumentation.rb
50
+ - lib/ruby_llm/instrumentation/chat.rb
51
+ - lib/ruby_llm/instrumentation/embedding.rb
52
+ - lib/ruby_llm/instrumentation/image.rb
53
+ - lib/ruby_llm/instrumentation/moderation.rb
54
+ - lib/ruby_llm/instrumentation/railtie.rb
55
+ - lib/ruby_llm/instrumentation/transcription.rb
56
+ - lib/ruby_llm/instrumentation/version.rb
57
+ - lib/tasks/ruby_llm/instrumentation_tasks.rake
58
+ homepage: https://github.com/sinaptia/ruby_llm-instrumentation
59
+ licenses: []
60
+ metadata:
61
+ homepage_uri: https://github.com/sinaptia/ruby_llm-instrumentation
62
+ source_code_uri: https://github.com/sinaptia/ruby_llm-instrumentation
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubygems_version: 3.6.9
78
+ specification_version: 4
79
+ summary: Rails instrumentation for RubyLLM
80
+ test_files: []