nitro_intelligence 0.0.1 → 1.0.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/docs/README.md +83 -11
- data/lib/nitro_intelligence/agent_server.rb +119 -8
- data/lib/nitro_intelligence/client/base.rb +52 -0
- data/lib/nitro_intelligence/client/client.rb +13 -0
- data/lib/nitro_intelligence/client/factory.rb +53 -0
- data/lib/nitro_intelligence/client/handlers/audio_transcription_handler.rb +38 -0
- data/lib/nitro_intelligence/client/handlers/chat_handler.rb +41 -0
- data/lib/nitro_intelligence/client/handlers/image_handler.rb +61 -0
- data/lib/nitro_intelligence/client/handlers/observed/audio_transcription_handler.rb +90 -0
- data/lib/nitro_intelligence/client/handlers/observed/chat_handler.rb +74 -0
- data/lib/nitro_intelligence/client/handlers/observed/image_handler.rb +109 -0
- data/lib/nitro_intelligence/client/observed.rb +38 -0
- data/lib/nitro_intelligence/client/observers/langfuse_observer.rb +75 -0
- data/lib/nitro_intelligence/configuration.rb +2 -0
- data/lib/nitro_intelligence/langfuse_extension.rb +10 -53
- data/lib/nitro_intelligence/media/audio.rb +50 -0
- data/lib/nitro_intelligence/media/image_generation.rb +4 -2
- data/lib/nitro_intelligence/models/model_catalog.rb +7 -2
- data/lib/nitro_intelligence/observability/project.rb +33 -0
- data/lib/nitro_intelligence/observability/project_client.rb +18 -0
- data/lib/nitro_intelligence/observability/project_client_registry.rb +44 -0
- data/lib/nitro_intelligence/observability/prompt.rb +58 -0
- data/lib/nitro_intelligence/observability/prompt_store.rb +112 -0
- data/lib/nitro_intelligence/observability/upload_handler.rb +138 -0
- data/lib/nitro_intelligence/reporter.rb +43 -0
- data/lib/nitro_intelligence/tool_call_review_validator.rb +69 -0
- data/lib/nitro_intelligence/trace.rb +2 -2
- data/lib/nitro_intelligence/version.rb +1 -1
- data/lib/nitro_intelligence.rb +10 -44
- metadata +26 -10
- data/lib/nitro_intelligence/client.rb +0 -337
- data/lib/nitro_intelligence/media/upload_handler.rb +0 -135
- data/lib/nitro_intelligence/prompt/prompt.rb +0 -56
- data/lib/nitro_intelligence/prompt/prompt_store.rb +0 -110
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dcac30052d3ab8db387569038d73304349941c60d224879e67fe9116247397ff
|
|
4
|
+
data.tar.gz: 9dca9e4c1b4f3669c65d3889c36b20d502b8b910b4b6c0bf001eee40e8eabc9c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1e5eec8f160d35bb7fac0d12b3d47d1a84e0694ad98d2ddb3f301fb6b578f7970895a60240ddcd5eb04af95a9ee9fdcd79959bcc8aadfa17f5cc434351331ca6
|
|
7
|
+
data.tar.gz: a565c1662fbfbee8768c9f21691dad16401f34205d0c2b6f463b0e165ede2f15e602e0f84d569291cd37ccd56a3af7bacb6d1c8a71a91d698beefba57059b867
|
data/docs/README.md
CHANGED
|
@@ -38,16 +38,16 @@ end
|
|
|
38
38
|
|
|
39
39
|
### Configuration Keys
|
|
40
40
|
|
|
41
|
-
| Key
|
|
42
|
-
|
|
43
|
-
| `logger`
|
|
44
|
-
| `environment`
|
|
45
|
-
| `cache_provider`
|
|
46
|
-
| `inference_api_key`
|
|
47
|
-
| `inference_base_url`
|
|
48
|
-
| `observability_base_url` | `String`
|
|
49
|
-
| `observability_projects` | `Array<Hash>` | `[]`
|
|
50
|
-
| `agent_server_config`
|
|
41
|
+
| Key | Type | Default | Description |
|
|
42
|
+
| ------------------------ | ------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
43
|
+
| `logger` | `Logger` | `Logger.new($stdout)` | Logger used for diagnostic output |
|
|
44
|
+
| `environment` | `String` | `"test"` | Runtime environment name |
|
|
45
|
+
| `cache_provider` | cache store | `NullCache` | ActiveSupport-compatible cache store |
|
|
46
|
+
| `inference_api_key` | `String` | `""` | API key for the LLM inference service |
|
|
47
|
+
| `inference_base_url` | `String` | `""` | Base URL for the LLM inference service |
|
|
48
|
+
| `observability_base_url` | `String` | `""` | Base URL for the Langfuse observability service |
|
|
49
|
+
| `observability_projects` | `Array<Hash>` | `[]` | Langfuse project credentials (slug, id, public_key, secret_key) |
|
|
50
|
+
| `agent_server_config` | `Hash` | `{}` | Credentials for `AgentServer.new`. Expected keys: `base_url` (String) — HTTP base URL of the agent server; `api_key` (String) — bearer token; `user_id` (String, default: `"default-user"`) — caller identity |
|
|
51
51
|
|
|
52
52
|
## Basic Usage
|
|
53
53
|
|
|
@@ -70,7 +70,6 @@ client = NitroIntelligence::Client.new
|
|
|
70
70
|
client.chat(parameters: { model: "meta-llama/Llama-3.1-8B-Instruct", messages: [{ role: "user", content: "Why is the sky blue?" }]})
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
|
|
74
73
|
#### Providing Parameters
|
|
75
74
|
|
|
76
75
|
Parameters such as 'max_tokens' and 'temperature' can be passed in under the `parameters` key.
|
|
@@ -82,6 +81,36 @@ client.chat(parameters: { model: "meta-llama/Llama-3.1-8B-Instruct", max_tokens:
|
|
|
82
81
|
|
|
83
82
|
For a full list of supported parameters, see the [API reference here](https://developers.openai.com/api/reference/resources/completions/methods/create).
|
|
84
83
|
|
|
84
|
+
### Audio Transcription
|
|
85
|
+
|
|
86
|
+
Nitro Intelligence can be used to transcribe audio from a file into text.
|
|
87
|
+
|
|
88
|
+
Basic examples of usage:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
client = NitroIntelligence::Client.new
|
|
92
|
+
audio = File.open('sample.m4a', 'rb')
|
|
93
|
+
result = client.transcribe_audio(audio_file: audio)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`result` is a `OpenAI::Models::Audio::Transcription` object:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
<OpenAI::Models::Audio::Transcription:0x2fdc8 {:text=>"Hello, how are you doing today?", :usage=>{:input_tokens=>28, :output_tokens=>10, :total_tokens=>38, :type=>:tokens, :input_token_details=>{:audio_tokens=>28, :text_tokens=>0}}}>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
To use a prompt, simply initialize your client with the `observability_project_slug` and pass `prompt_name` into parameters:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
client = NitroIntelligence::Client.new(observability_project_slug: "my-test-project")
|
|
106
|
+
audio = File.open('sample.m4a', 'rb')
|
|
107
|
+
result = client.transcribe_audio(audio_file: audio, parameters: {prompt_name: "Spanish Converter"})
|
|
108
|
+
|
|
109
|
+
puts result
|
|
110
|
+
|
|
111
|
+
# <OpenAI::Models::Audio::Transcription:0x2fd8c {:text=>"Hola, ¿cómo estás hoy?", :usage=>{:input_tokens=>37, :output_tokens=>9, :total_tokens=>46, :type=>:tokens, :input_token_details=>{:audio_tokens=>28, :text_tokens=>9}}}>
|
|
112
|
+
```
|
|
113
|
+
|
|
85
114
|
### Image Editing and Generation
|
|
86
115
|
|
|
87
116
|
Nitro Intelligence can be used for image editing and generation
|
|
@@ -175,6 +204,43 @@ client.chat(
|
|
|
175
204
|
)
|
|
176
205
|
```
|
|
177
206
|
|
|
207
|
+
### Custom Trace IDs
|
|
208
|
+
|
|
209
|
+
To generate a deterministic trace ID in the observability platform, you can pass `trace_seed` as a parameter. This is useful when you want a stable identifier that is derived from a specific domain value (e.g., document ID). The same `trace_seed` will produce the same trace ID, making it easier to correlate multiple events.
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
document_id = "document-123"
|
|
213
|
+
client = NitroIntelligence::Client.new(observability_project_slug: "fake-feature-project")
|
|
214
|
+
client.chat(
|
|
215
|
+
message: "summarize the document",
|
|
216
|
+
parameters: {
|
|
217
|
+
trace_seed: document_id,
|
|
218
|
+
}
|
|
219
|
+
)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Scoring
|
|
223
|
+
|
|
224
|
+
You can use `NitroIntelligence::Reporter` to evaluate existing traces. Calling `NitroIntelligence::Reporter#score` lets you attach metrics to a trace in the observability platform.
|
|
225
|
+
|
|
226
|
+
#### By Trace ID
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
reporter = NitroIntelligence::Reporter.new(observability_project_slug: "fake-feature-project")
|
|
230
|
+
reporter.score(name: "precision", value: 0.5, trace_id: "2377b0b38acf0f11b95504344fad6152")
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
#### By Trace Seed
|
|
234
|
+
|
|
235
|
+
Knowing the trace seed, you can use `NitroIntelligence::Trace.create_id` to get the trace ID.
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
document_id = "document-123"
|
|
239
|
+
reporter = NitroIntelligence::Reporter.new(observability_project_slug: "fake-feature-project")
|
|
240
|
+
trace_id = NitroIntelligence::Trace.create_id(seed: document_id)
|
|
241
|
+
reporter.score(name: "precision", value: 0.5, trace_id:)
|
|
242
|
+
```
|
|
243
|
+
|
|
178
244
|
### Prompt Variables and Config
|
|
179
245
|
|
|
180
246
|
Prompts are often created with "variables". These variables can be supplied and compiled into the prompt. For example:
|
|
@@ -245,3 +311,9 @@ client.chat(
|
|
|
245
311
|
}
|
|
246
312
|
)
|
|
247
313
|
```
|
|
314
|
+
|
|
315
|
+
## Agent Server
|
|
316
|
+
|
|
317
|
+
The Agent Server is Nitro Intelligence's lightweight SDK for working with hosted agent threads, runs, and human review flows. It is mainly used to initialize conversation threads, trigger agent runs, inspect agent tool calls pending human approval, and resume interrupted threads after human reviews.
|
|
318
|
+
|
|
319
|
+
For the full Agent Server guide, see [AGENT_SERVER.md](AGENT_SERVER.md).
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
require "nitro_intelligence/tool_call_review_validator"
|
|
2
|
+
|
|
1
3
|
module NitroIntelligence
|
|
2
4
|
class AgentServer
|
|
3
5
|
class ConfigurationError < StandardError; end
|
|
4
6
|
class ThreadInitializationError < StandardError; end
|
|
5
7
|
class RunError < StandardError; end
|
|
8
|
+
class ThreadResumptionError < StandardError; end
|
|
6
9
|
|
|
7
10
|
attr_reader :base_url, :user_id
|
|
8
11
|
|
|
@@ -14,19 +17,61 @@ module NitroIntelligence
|
|
|
14
17
|
@base_url = base_url
|
|
15
18
|
@api_key = api_key
|
|
16
19
|
@user_id = user_id
|
|
20
|
+
@tool_call_review_validator = ToolCallReviewValidator.new
|
|
17
21
|
end
|
|
18
22
|
|
|
19
23
|
def await_run(thread_id:, assistant_id:, messages:, context: {})
|
|
20
24
|
raise RunError, "messages cannot be empty" if messages.blank?
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
last_message = messages.first if messages.size == 1
|
|
26
|
+
initial_state = messages[0..-2]
|
|
27
|
+
last_message = messages.last
|
|
25
28
|
|
|
26
29
|
initialize_thread_if_needed(thread_id:, initial_state:)
|
|
27
30
|
trigger_run(thread_id:, assistant_id:, context:, last_message:)
|
|
28
31
|
end
|
|
29
32
|
|
|
33
|
+
def tool_calls_pending_review(thread_id:)
|
|
34
|
+
thread_state = get_thread_state(thread_id:)
|
|
35
|
+
messages = thread_messages(thread_state)
|
|
36
|
+
reviewed_tool_call_ids = tool_messages(messages).map { |message| message["tool_call_id"] }
|
|
37
|
+
|
|
38
|
+
messages.each_with_index.flat_map do |message, index|
|
|
39
|
+
next [] unless message["type"] == "ai"
|
|
40
|
+
|
|
41
|
+
pending_tool_calls(message, reviewed_tool_call_ids).map do |tool_call|
|
|
42
|
+
{
|
|
43
|
+
"previous_message_id" => index.zero? ? nil : messages[index - 1]&.dig("id"),
|
|
44
|
+
"id" => tool_call["id"],
|
|
45
|
+
"name" => tool_call["name"],
|
|
46
|
+
"args" => tool_call["args"] || {},
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def review_tool_calls(thread_id:, assistant_id:, reviewer_id:, tool_calls:, reviewed_at: DateTime.current.iso8601)
|
|
53
|
+
resume = { reviewer_id:, reviewed_at:, tool_calls: }.with_indifferent_access
|
|
54
|
+
thread = get_thread(thread_id:)
|
|
55
|
+
raise ThreadResumptionError, "Thread #{thread_id} is not in the interrupted state" unless interrupted?(thread)
|
|
56
|
+
|
|
57
|
+
thread_state = get_thread_state(thread_id:)
|
|
58
|
+
|
|
59
|
+
@tool_call_review_validator.validate!(
|
|
60
|
+
thread_state:,
|
|
61
|
+
tool_calls: resume[:tool_calls],
|
|
62
|
+
pending_tool_calls: tool_calls_pending_review(thread_id:)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
resume_run(
|
|
66
|
+
thread_id:,
|
|
67
|
+
assistant_id:,
|
|
68
|
+
resume:,
|
|
69
|
+
context: interrupt_context(thread_state)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
30
75
|
private
|
|
31
76
|
|
|
32
77
|
def initialize_thread_if_needed(thread_id:, initial_state:)
|
|
@@ -45,6 +90,22 @@ module NitroIntelligence
|
|
|
45
90
|
thread_response
|
|
46
91
|
end
|
|
47
92
|
|
|
93
|
+
def get_thread_state(thread_id:)
|
|
94
|
+
state_response = get(path: "/threads/#{thread_id}/state")
|
|
95
|
+
|
|
96
|
+
raise ThreadResumptionError, state_response.body if state_response.code != 200
|
|
97
|
+
|
|
98
|
+
state_response
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def get_thread(thread_id:)
|
|
102
|
+
thread_response = get(path: "/threads/#{thread_id}")
|
|
103
|
+
|
|
104
|
+
raise ThreadResumptionError, thread_response.body if thread_response.code != 200
|
|
105
|
+
|
|
106
|
+
thread_response
|
|
107
|
+
end
|
|
108
|
+
|
|
48
109
|
def trigger_run(thread_id:, assistant_id:, last_message:, context: {})
|
|
49
110
|
run_response = post(
|
|
50
111
|
path: "/threads/#{thread_id}/runs/wait",
|
|
@@ -59,18 +120,68 @@ module NitroIntelligence
|
|
|
59
120
|
|
|
60
121
|
raise RunError, run_response.body if run_response.code != 200
|
|
61
122
|
|
|
62
|
-
run_response["messages"].last
|
|
123
|
+
Array(run_response["messages"]).last&.dig("content")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def resume_run(thread_id:, assistant_id:, resume:, context:)
|
|
127
|
+
run_response = post(
|
|
128
|
+
path: "/threads/#{thread_id}/runs/wait",
|
|
129
|
+
body: {
|
|
130
|
+
assistant_id:,
|
|
131
|
+
command: {
|
|
132
|
+
resume:,
|
|
133
|
+
},
|
|
134
|
+
context:,
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
raise ThreadResumptionError, run_response.body if run_response.code != 200
|
|
139
|
+
|
|
140
|
+
run_response
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def interrupted?(thread)
|
|
144
|
+
thread["status"] == "interrupted"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def interrupt_context(thread_state)
|
|
148
|
+
thread_state.dig("interrupts", 0, "value", "context") || {}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def thread_messages(thread_state)
|
|
152
|
+
Array(thread_state.dig("values", "messages"))
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def tool_messages(messages)
|
|
156
|
+
messages.select { |message| message["type"] == "tool" }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def pending_tool_calls(message, reviewed_tool_call_ids)
|
|
160
|
+
Array(message["tool_calls"]).reject do |tool_call|
|
|
161
|
+
reviewed_tool_call_ids.include?(tool_call["id"])
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def get(path:)
|
|
166
|
+
HTTParty.get(
|
|
167
|
+
"#{base_url}#{path}",
|
|
168
|
+
headers: request_headers
|
|
169
|
+
)
|
|
63
170
|
end
|
|
64
171
|
|
|
65
172
|
def post(path:, body:)
|
|
66
173
|
HTTParty.post(
|
|
67
174
|
"#{base_url}#{path}",
|
|
68
|
-
headers:
|
|
69
|
-
"Content-Type" => "application/json",
|
|
70
|
-
"Authorization" => "Bearer #{@api_key}",
|
|
71
|
-
},
|
|
175
|
+
headers: request_headers,
|
|
72
176
|
body: body.to_json
|
|
73
177
|
)
|
|
74
178
|
end
|
|
179
|
+
|
|
180
|
+
def request_headers
|
|
181
|
+
{
|
|
182
|
+
"Content-Type" => "application/json",
|
|
183
|
+
"Authorization" => "Bearer #{@api_key}",
|
|
184
|
+
}
|
|
185
|
+
end
|
|
75
186
|
end
|
|
76
187
|
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require "nitro_intelligence/client/handlers/audio_transcription_handler"
|
|
2
|
+
require "nitro_intelligence/client/handlers/chat_handler"
|
|
3
|
+
require "nitro_intelligence/client/handlers/image_handler"
|
|
4
|
+
|
|
5
|
+
module NitroIntelligence
|
|
6
|
+
module Client
|
|
7
|
+
class Base
|
|
8
|
+
attr_reader :client
|
|
9
|
+
|
|
10
|
+
def initialize(client:)
|
|
11
|
+
@client = client
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def chat(message: "", parameters: {})
|
|
15
|
+
chat_handler.create(message:, parameters:)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Input images should be byte strings. Returns NitroIntelligence::ImageGeneration
|
|
19
|
+
def generate_image(message: "", target_image: nil, reference_images: [], parameters: {})
|
|
20
|
+
image_handler.create(message:, target_image:, reference_images:, parameters:)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Audio file should be file object with extension in the filename
|
|
24
|
+
# Use file_extension for now to prevent a library dependency
|
|
25
|
+
def transcribe_audio(message: +"", audio_file: nil, parameters: {})
|
|
26
|
+
audio_transcription_handler.create(message:, audio_file:, parameters:)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def audio_transcription_handler
|
|
32
|
+
@audio_transcription_handler ||= Handlers::AudioTranscriptionHandler.new(client: @client)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def chat_handler
|
|
36
|
+
@chat_handler ||= Handlers::ChatHandler.new(client: @client)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def image_handler
|
|
40
|
+
@image_handler ||= Handlers::ImageHandler.new(client: @client)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def method_missing(method_name, *, &)
|
|
44
|
+
@client.send(method_name, *, &)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
48
|
+
@client.respond_to?(method_name, include_private) || super
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require "nitro_intelligence/client/factory"
|
|
2
|
+
|
|
3
|
+
module NitroIntelligence
|
|
4
|
+
module Client
|
|
5
|
+
def self.new(observability_project_slug: nil)
|
|
6
|
+
Factory.new(observability_project_slug:).build
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.validate_model(model)
|
|
10
|
+
raise ArgumentError, "Unsupported model: '#{model}'" unless NitroIntelligence.model_catalog.exists?(model)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require "nitro_intelligence/client/base"
|
|
2
|
+
require "nitro_intelligence/client/observed"
|
|
3
|
+
require "nitro_intelligence/client/observers/langfuse_observer"
|
|
4
|
+
require "nitro_intelligence/observability/project"
|
|
5
|
+
|
|
6
|
+
module NitroIntelligence
|
|
7
|
+
module Client
|
|
8
|
+
class Factory
|
|
9
|
+
def initialize(observability_project_slug:)
|
|
10
|
+
@observability_project_slug = observability_project_slug
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def build
|
|
14
|
+
if @observability_project_slug.present?
|
|
15
|
+
begin
|
|
16
|
+
return Client::Observed.new(
|
|
17
|
+
client: inference_client,
|
|
18
|
+
observer: Client::Observers::LangfuseObserver.new(project_client: fetch_project_client)
|
|
19
|
+
)
|
|
20
|
+
rescue NitroIntelligence::Observability::ProjectClient::NotFoundError,
|
|
21
|
+
NitroIntelligence::Observability::Project::NotFoundError => e
|
|
22
|
+
NitroIntelligence.logger.warn(
|
|
23
|
+
"#{self.class} #{e} - Error raised initializing project - Falling back to base client (no observability)"
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
Client::Base.new(
|
|
29
|
+
client: inference_client
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def fetch_project_client
|
|
36
|
+
project_client = NitroIntelligence.project_client_registry.fetch(@observability_project_slug)
|
|
37
|
+
if project_client.nil?
|
|
38
|
+
raise NitroIntelligence::Observability::ProjectClient::NotFoundError,
|
|
39
|
+
"No project session found for slug: #{@observability_project_slug}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
project_client
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def inference_client
|
|
46
|
+
@inference_client ||= OpenAI::Client.new(
|
|
47
|
+
api_key: NitroIntelligence.config.inference_api_key,
|
|
48
|
+
base_url: NitroIntelligence.config.inference_base_url
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require "openai"
|
|
2
|
+
|
|
3
|
+
module NitroIntelligence
|
|
4
|
+
module Client
|
|
5
|
+
module Handlers
|
|
6
|
+
class AudioTranscriptionHandler
|
|
7
|
+
ALLOWED_EXTRA_PARAMETERS = OpenAI::Models::Audio::TranscriptionCreateParams.fields.keys.uniq.freeze
|
|
8
|
+
|
|
9
|
+
def initialize(client:)
|
|
10
|
+
@client = client
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def create(audio_file:, message: "", parameters: {})
|
|
14
|
+
validate_and_resolve!(parameters)
|
|
15
|
+
perform_request(audio_file:, message:, parameters:)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def perform_request(audio_file:, message: "", parameters: {})
|
|
19
|
+
@client.audio.transcriptions.create(
|
|
20
|
+
prompt: message,
|
|
21
|
+
file: audio_file,
|
|
22
|
+
**parameters.slice(*ALLOWED_EXTRA_PARAMETERS)
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def validate_and_resolve!(parameters)
|
|
27
|
+
default_parameters = {
|
|
28
|
+
metadata: {},
|
|
29
|
+
model: NitroIntelligence.model_catalog.default_audio_transcription_model&.name,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
parameters.replace(default_parameters.merge(parameters))
|
|
33
|
+
Client.validate_model(parameters[:model])
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require "openai"
|
|
2
|
+
|
|
3
|
+
module NitroIntelligence
|
|
4
|
+
module Client
|
|
5
|
+
module Handlers
|
|
6
|
+
class ChatHandler
|
|
7
|
+
ALLOWED_EXTRA_PARAMETERS = OpenAI::Models::Chat::CompletionCreateParams.fields.keys.uniq.freeze
|
|
8
|
+
|
|
9
|
+
def initialize(client:)
|
|
10
|
+
@client = client
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def create(message: "", parameters: {})
|
|
14
|
+
validate_and_resolve!(parameters, message)
|
|
15
|
+
perform_request(parameters:)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def perform_request(parameters: {})
|
|
19
|
+
@client.chat.completions.create(**parameters.slice(*ALLOWED_EXTRA_PARAMETERS))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def validate_and_resolve!(parameters, message)
|
|
23
|
+
if parameters[:messages].blank? && message.present?
|
|
24
|
+
parameters[:messages] ||= [{ role: "user",
|
|
25
|
+
content: message }]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
default_parameters = {
|
|
29
|
+
metadata: {},
|
|
30
|
+
messages: [],
|
|
31
|
+
model: NitroIntelligence.model_catalog.default_text_model&.name,
|
|
32
|
+
extra_headers: { "Prefer" => "wait" },
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
parameters.replace(default_parameters.merge(parameters))
|
|
36
|
+
Client.validate_model(parameters[:model])
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
require "openai"
|
|
2
|
+
require "nitro_intelligence/media/image_generation"
|
|
3
|
+
|
|
4
|
+
module NitroIntelligence
|
|
5
|
+
module Client
|
|
6
|
+
module Handlers
|
|
7
|
+
class ImageHandler
|
|
8
|
+
ALLOWED_EXTRA_PARAMETERS = OpenAI::Models::Chat::CompletionCreateParams.fields.keys.uniq.freeze
|
|
9
|
+
|
|
10
|
+
def initialize(client:)
|
|
11
|
+
@client = client
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create(message: "", target_image: nil, reference_images: [], parameters: {})
|
|
15
|
+
image_generation = build_image_generation(message:, target_image:, reference_images:, parameters:)
|
|
16
|
+
|
|
17
|
+
validate_and_resolve!(parameters, image_generation)
|
|
18
|
+
|
|
19
|
+
chat_completion = perform_request(parameters:)
|
|
20
|
+
|
|
21
|
+
image_generation.parse_file(chat_completion)
|
|
22
|
+
image_generation
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def perform_request(parameters: {})
|
|
26
|
+
@client.chat.completions.create(**parameters.slice(*ALLOWED_EXTRA_PARAMETERS))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def validate_and_resolve!(parameters, image_generation)
|
|
30
|
+
default_parameters = {
|
|
31
|
+
image_generation:,
|
|
32
|
+
metadata: {},
|
|
33
|
+
messages: image_generation.messages,
|
|
34
|
+
model: image_generation.config.model,
|
|
35
|
+
extra_headers: { "Prefer" => "wait" },
|
|
36
|
+
request_options: {
|
|
37
|
+
extra_body: {
|
|
38
|
+
image_config: {
|
|
39
|
+
aspect_ratio: image_generation.config.aspect_ratio,
|
|
40
|
+
image_size: image_generation.config.resolution,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
parameters.replace(default_parameters.merge(parameters))
|
|
46
|
+
Client.validate_model(parameters[:model])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def build_image_generation(message:, target_image:, reference_images:, parameters:)
|
|
52
|
+
NitroIntelligence::ImageGeneration.new(message:, target_image:, reference_images:) do |config|
|
|
53
|
+
config.aspect_ratio = parameters[:aspect_ratio] if parameters.key?(:aspect_ratio)
|
|
54
|
+
config.model = parameters[:model] if parameters.key?(:model)
|
|
55
|
+
config.resolution = parameters[:resolution] if parameters.key?(:resolution)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
require "base64"
|
|
2
|
+
require "nitro_intelligence/media/audio"
|
|
3
|
+
|
|
4
|
+
module NitroIntelligence
|
|
5
|
+
module Client
|
|
6
|
+
module Handlers
|
|
7
|
+
module Observed
|
|
8
|
+
class AudioTranscriptionHandler
|
|
9
|
+
class ObservedAudioTranscriptionPromptError < StandardError; end
|
|
10
|
+
|
|
11
|
+
def initialize(base_handler:, observer:)
|
|
12
|
+
@base_handler = base_handler
|
|
13
|
+
@observer = observer
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create(audio_file:, message: +"", parameters: {})
|
|
17
|
+
@base_handler.validate_and_resolve!(parameters)
|
|
18
|
+
|
|
19
|
+
# Modifies message in place
|
|
20
|
+
prompt = handle_prompt(message:, parameters:)
|
|
21
|
+
trace_name = parameters[:trace_name] || prompt&.name || @observer.project_client.project.slug
|
|
22
|
+
|
|
23
|
+
@observer.observe(
|
|
24
|
+
"audio-transcription",
|
|
25
|
+
type: :generation,
|
|
26
|
+
parameters:,
|
|
27
|
+
trace_name:,
|
|
28
|
+
prompt:
|
|
29
|
+
) do |generation|
|
|
30
|
+
workflow(generation:, message:, audio_file:, parameters:)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def handle_prompt(message:, parameters:)
|
|
37
|
+
return nil if parameters[:prompt_name].blank?
|
|
38
|
+
|
|
39
|
+
prompt = @observer.project_client.project.prompt_store.get_prompt(
|
|
40
|
+
prompt_name: parameters[:prompt_name],
|
|
41
|
+
prompt_label: parameters[:prompt_label],
|
|
42
|
+
prompt_version: parameters[:prompt_version]
|
|
43
|
+
)
|
|
44
|
+
prompt_variables = parameters[:prompt_variables] || {}
|
|
45
|
+
|
|
46
|
+
if prompt.present?
|
|
47
|
+
# Prompts for audio transcriptions should only be text
|
|
48
|
+
if prompt.type != "text"
|
|
49
|
+
raise ObservedAudioTranscriptionPromptError,
|
|
50
|
+
"Prompt type for audio transcription must be text: #{prompt.name}"
|
|
51
|
+
end
|
|
52
|
+
interpolated_prompt = prompt.compile(**prompt_variables)
|
|
53
|
+
|
|
54
|
+
message.prepend("#{interpolated_prompt} ").strip!
|
|
55
|
+
parameters.merge!(prompt.config) unless parameters[:prompt_config_disabled]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
prompt
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def workflow(generation:, message:, audio_file:, parameters:)
|
|
62
|
+
audio_transcription = @base_handler.perform_request(audio_file:, message:, parameters:)
|
|
63
|
+
|
|
64
|
+
audio_file.rewind
|
|
65
|
+
upload_handler = NitroIntelligence::Observability::UploadHandler.new(
|
|
66
|
+
auth_token: @observer.project_client.project.auth_token
|
|
67
|
+
)
|
|
68
|
+
upload_handler.upload(
|
|
69
|
+
generation.trace_id,
|
|
70
|
+
upload_queue: Queue.new([NitroIntelligence::Audio.new(audio_file)])
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
trace_attributes = {
|
|
74
|
+
model: parameters[:model], # Model isn't in response object OpenAI::Models::Audio::Transcription
|
|
75
|
+
input: message,
|
|
76
|
+
output: audio_transcription.text,
|
|
77
|
+
usage_details: {
|
|
78
|
+
input_tokens: audio_transcription.usage.input_tokens,
|
|
79
|
+
output_tokens: audio_transcription.usage.output_tokens,
|
|
80
|
+
total_tokens: audio_transcription.usage.total_tokens,
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
[audio_transcription, trace_attributes]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|