little_ghost 0.6.0 → 0.7.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 +3 -3
- data/docs/guides/code_mode.md +2 -0
- data/docs/guides/models_and_providers.md +91 -0
- data/lib/little_ghost/code_mode/ruby/host.rb +50 -10
- data/lib/little_ghost/code_mode/ruby/session.rb +4 -0
- data/lib/little_ghost/code_mode/ruby_engine.rb +1 -0
- data/lib/little_ghost/embeddings/request.rb +103 -0
- data/lib/little_ghost/embeddings/response.rb +55 -0
- data/lib/little_ghost/errors.rb +2 -0
- data/lib/little_ghost/model.rb +20 -0
- data/lib/little_ghost/model_operations.rb +210 -0
- data/lib/little_ghost/providers/base.rb +16 -1
- data/lib/little_ghost/providers/bedrock/aws_protocol.rb +1 -1
- data/lib/little_ghost/providers/bedrock/http_client.rb +28 -7
- data/lib/little_ghost/providers/bedrock.rb +93 -8
- data/lib/little_ghost/providers/openai.rb +80 -4
- data/lib/little_ghost/runtime/hooks/artifacts.rb +8 -0
- data/lib/little_ghost/runtime.rb +23 -0
- data/lib/little_ghost/support/http_client.rb +10 -3
- data/lib/little_ghost/tracing/open_telemetry.rb +6 -4
- data/lib/little_ghost/version.rb +1 -1
- data/lib/little_ghost.rb +25 -0
- metadata +9 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d214ff7dd49c056712968000f9ddc127bc132dd4b97b8f0c89bb3adb2f52f3b0
|
|
4
|
+
data.tar.gz: de2f85339390969db2ae0863bf9056ab386b07e2bf05cc976f23899ae2b318d2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 48004e9b2b4c826877601f31681129f5f0434f544aa979941d2d09840ca677ccbf86e0334cdd9de60081601e039326d445a033064940049dbd3f5e27243f47ac
|
|
7
|
+
data.tar.gz: 88fc87d32cb553b047273e249685f410ff8721e32275a3f3800e00cd79a674fb43188b0ef34d2b0a2ca04c4502a18503a6da791d2634ec92f4bc539d665ad36a
|
data/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Build AI features that feel at home in Ruby
|
|
2
2
|
|
|
3
3
|
> **Using a coding agent?** Start with
|
|
4
|
-
> [`llms.txt`](https://
|
|
5
|
-
> of the guides and API. [`llms-full.txt`](https://
|
|
4
|
+
> [`llms.txt`](https://littleghostai.org/llms.txt) for a concise map
|
|
5
|
+
> of the guides and API. [`llms-full.txt`](https://littleghostai.org/llms-full.txt)
|
|
6
6
|
> contains the complete documentation in one file.
|
|
7
7
|
|
|
8
8
|
LittleGhost is a Ruby library for building AI features with agents and composable assemblies. With `OPENROUTER_API_KEY` set, start with one class, give it a prompt, and call it like the rest of your application code:
|
|
@@ -134,7 +134,7 @@ upgrading, because interfaces may change between releases.
|
|
|
134
134
|
|
|
135
135
|
### For contributors
|
|
136
136
|
|
|
137
|
-
See the [contributing guide](https://github.com/
|
|
137
|
+
See the [contributing guide](https://github.com/littleghostai/little_ghost/blob/main/CONTRIBUTING.md), [Code of Conduct](https://github.com/littleghostai/little_ghost/blob/main/CODE_OF_CONDUCT.md), and [security policy](https://github.com/littleghostai/little_ghost/blob/main/SECURITY.md).
|
|
138
138
|
|
|
139
139
|
```sh
|
|
140
140
|
$ bundle install
|
data/docs/guides/code_mode.md
CHANGED
|
@@ -99,6 +99,8 @@ do not carry into a later `exec`. Within one program, the model can use:
|
|
|
99
99
|
order.
|
|
100
100
|
- `ALL_TOOLS` to inspect the complete runtime catalog.
|
|
101
101
|
- `text(value)` to add user-visible output.
|
|
102
|
+
- Ordinary Ruby output from `puts`, `print`, `printf`, and `p`, which is
|
|
103
|
+
captured as user-visible output and combined into bounded chunks.
|
|
102
104
|
- The program's final expression as the completed value returned by `exec` or
|
|
103
105
|
a later `wait`.
|
|
104
106
|
- `finish(value)` to complete early.
|
|
@@ -120,6 +120,97 @@ choose a structured-result strategy.
|
|
|
120
120
|
Provider capabilities can change. Handle failed Runs and provider errors even
|
|
121
121
|
when the catalog says a feature is supported.
|
|
122
122
|
|
|
123
|
+
## Call a model without an Agent
|
|
124
|
+
|
|
125
|
+
Some application work needs one model response rather than an Agent. Use
|
|
126
|
+
`LittleGhost.generate` for tasks such as classification, extraction, or
|
|
127
|
+
rewriting when your application already owns the surrounding workflow:
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
response = LittleGhost.generate(
|
|
131
|
+
model: :customer_support,
|
|
132
|
+
messages: [
|
|
133
|
+
{role: :system, content: "Classify the request."},
|
|
134
|
+
{role: :user, content: "My transfer is still pending."}
|
|
135
|
+
],
|
|
136
|
+
settings: {temperature: 0}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
response.output
|
|
140
|
+
response.usage.total_tokens
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The operation returns a `LittleGhost::RunResult`, the same result type returned
|
|
144
|
+
by an Agent invocation, so
|
|
145
|
+
application code can read `output`, `usage`, and the final message in the same
|
|
146
|
+
way. Plain generation makes one model request without starting an Agent or
|
|
147
|
+
creating a Run. Structured generation may make one additional repair request.
|
|
148
|
+
|
|
149
|
+
Pass a strict object schema when application code needs checked JSON:
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
response = LittleGhost.generate(
|
|
153
|
+
model: :customer_support,
|
|
154
|
+
messages: [{role: :user, content: "My transfer is still pending."}],
|
|
155
|
+
result_schema: {
|
|
156
|
+
name: "classification",
|
|
157
|
+
description: "Classify one support request",
|
|
158
|
+
schema: {
|
|
159
|
+
type: "object",
|
|
160
|
+
properties: {category: {type: "string"}},
|
|
161
|
+
required: ["category"],
|
|
162
|
+
additionalProperties: false
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
response.output
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
LittleGhost checks the result against the schema and gives the model one repair
|
|
171
|
+
attempt by default. Add `structured_result_repair_attempts: 3` to the call
|
|
172
|
+
above when a checked result warrants additional attempts. The setting accepts
|
|
173
|
+
integers from zero through three.
|
|
174
|
+
|
|
175
|
+
Read the checked value through `response.output`. If every permitted attempt is
|
|
176
|
+
invalid, the call raises `LittleGhost::StructuredResultError`.
|
|
177
|
+
|
|
178
|
+
A schema checks the shape of a value, not whether your application should act
|
|
179
|
+
on it. Check identifiers, permissions, and business rules before using the
|
|
180
|
+
result to change application state.
|
|
181
|
+
|
|
182
|
+
## Create embeddings
|
|
183
|
+
|
|
184
|
+
Use `LittleGhost.embed` when your application needs numeric representations for
|
|
185
|
+
search, clustering, or another similarity-based feature:
|
|
186
|
+
|
|
187
|
+
The example assumes application startup maps `search_embeddings` to an
|
|
188
|
+
embedding model, following the role configuration shown earlier on this page.
|
|
189
|
+
The selected provider adapter's API reference lists the model-specific
|
|
190
|
+
settings.
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
response = LittleGhost.embed(
|
|
194
|
+
model: :search_embeddings,
|
|
195
|
+
inputs: ["Reset a password", "Track a transfer"]
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
response.vectors.length # => 2
|
|
199
|
+
response.dimensions
|
|
200
|
+
response.usage.input_tokens
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The response keeps vectors in the same order as the inputs. LittleGhost rejects
|
|
204
|
+
an incomplete or malformed response instead of returning a partial batch.
|
|
205
|
+
Choose the embedding model and request settings in trusted application
|
|
206
|
+
configuration, and keep each call within a workload size your application can
|
|
207
|
+
retry safely.
|
|
208
|
+
|
|
209
|
+
Embedding text is sent to the selected provider. Choose a provider that is
|
|
210
|
+
appropriate for that data, just as you would for an Agent request. See
|
|
211
|
+
`LittleGhost::Embeddings::Request` and your provider adapter's API reference for
|
|
212
|
+
the supported settings and request bounds.
|
|
213
|
+
|
|
123
214
|
Continue with [Prompts as Views](prompt_views.md) when an Agent's instructions
|
|
124
215
|
outgrow one string. See [Structured Results and Content](structured_outputs_and_content.md)
|
|
125
216
|
when you need checked result shapes, images, or documents.
|
|
@@ -6,6 +6,9 @@ module LittleGhost
|
|
|
6
6
|
module Host # :nodoc:
|
|
7
7
|
SOURCE = <<~'RUBY'
|
|
8
8
|
require "json"
|
|
9
|
+
protocol_output = STDOUT.dup
|
|
10
|
+
protocol_output.sync = true
|
|
11
|
+
STDOUT.reopen(STDERR)
|
|
9
12
|
STDOUT.sync = true
|
|
10
13
|
begin
|
|
11
14
|
read_exactly = lambda do |length|
|
|
@@ -21,9 +24,9 @@ module LittleGhost
|
|
|
21
24
|
write_frame = lambda do |value|
|
|
22
25
|
payload = JSON.generate(value)
|
|
23
26
|
raise "protocol frame too large" if payload.bytesize > 64 * 1024 * 1024
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
protocol_output.write([payload.bytesize].pack("N"))
|
|
28
|
+
protocol_output.write(payload)
|
|
29
|
+
protocol_output.flush
|
|
27
30
|
end
|
|
28
31
|
request = read_frame.call
|
|
29
32
|
catalog = request.fetch("catalog")
|
|
@@ -32,7 +35,40 @@ module LittleGhost
|
|
|
32
35
|
response_queues = {}
|
|
33
36
|
calls = 0
|
|
34
37
|
max_calls = request.fetch("tool_calls")
|
|
35
|
-
|
|
38
|
+
output_buffer = +""
|
|
39
|
+
flush_output = lambda do
|
|
40
|
+
unless output_buffer.empty?
|
|
41
|
+
value = output_buffer.dup
|
|
42
|
+
output_buffer.clear
|
|
43
|
+
write_frame.call(type: "text", value: value)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
emit = lambda do |value|
|
|
47
|
+
write_lock.synchronize do
|
|
48
|
+
flush_output.call
|
|
49
|
+
write_frame.call(value)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
program_output = Object.new
|
|
53
|
+
program_output.define_singleton_method(:write) do |value|
|
|
54
|
+
value = String(value)
|
|
55
|
+
write_lock.synchronize do
|
|
56
|
+
output_buffer << value
|
|
57
|
+
flush_output.call if output_buffer.bytesize >= 16_384 || value.include?("\n")
|
|
58
|
+
end
|
|
59
|
+
value.bytesize
|
|
60
|
+
end
|
|
61
|
+
program_output.define_singleton_method(:flush) do
|
|
62
|
+
write_lock.synchronize { flush_output.call }
|
|
63
|
+
self
|
|
64
|
+
end
|
|
65
|
+
program_output.define_singleton_method(:sync) { false }
|
|
66
|
+
program_output.define_singleton_method(:sync=) do |value|
|
|
67
|
+
flush if value
|
|
68
|
+
value
|
|
69
|
+
end
|
|
70
|
+
program_output.define_singleton_method(:tty?) { false }
|
|
71
|
+
$stdout = program_output
|
|
36
72
|
reader = Thread.new do
|
|
37
73
|
loop do
|
|
38
74
|
response = read_frame.call
|
|
@@ -51,9 +87,7 @@ module LittleGhost
|
|
|
51
87
|
response_queues[id] = queue
|
|
52
88
|
[id, queue]
|
|
53
89
|
end
|
|
54
|
-
|
|
55
|
-
write_frame.call(type: "call", id: id, name: name, arguments: arguments)
|
|
56
|
-
end
|
|
90
|
+
emit.call(type: "call", id: id, name: name, arguments: arguments)
|
|
57
91
|
response = queue.pop
|
|
58
92
|
queues_lock.synchronize { response_queues.delete(id) }
|
|
59
93
|
raise response.fetch("error") if response["error"]
|
|
@@ -75,7 +109,12 @@ module LittleGhost
|
|
|
75
109
|
end
|
|
76
110
|
Object.const_set(:ALL_TOOLS, catalog.freeze) unless Object.const_defined?(:ALL_TOOLS)
|
|
77
111
|
Object.const_set(:FRAME, request["frame"].freeze) if request["frame"] && !Object.const_defined?(:FRAME)
|
|
78
|
-
|
|
112
|
+
evaluation_context = Class.new do
|
|
113
|
+
def evaluate(source)
|
|
114
|
+
instance_eval(source, "(code-mode)", 1)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
context = evaluation_context.new
|
|
79
118
|
finished = false
|
|
80
119
|
finish_value = nil
|
|
81
120
|
context.define_singleton_method(:tools) { tools }
|
|
@@ -85,14 +124,15 @@ module LittleGhost
|
|
|
85
124
|
finish_value = value
|
|
86
125
|
throw :little_ghost_finish
|
|
87
126
|
end
|
|
88
|
-
value = catch(:little_ghost_finish) { context.
|
|
127
|
+
value = catch(:little_ghost_finish) { context.evaluate(request.fetch("source")) }
|
|
89
128
|
value = finish_value if finished
|
|
90
129
|
emit.call(type: "done", value: value)
|
|
91
130
|
rescue SignalException
|
|
92
131
|
exit 0
|
|
93
132
|
rescue Exception => error
|
|
94
133
|
STDERR.puts("#{error.class}: #{error.message}")
|
|
95
|
-
|
|
134
|
+
error_frame = {type: "error", error: "#{error.class}: #{error.message}"}
|
|
135
|
+
emit ? emit.call(error_frame) : write_frame&.call(error_frame)
|
|
96
136
|
exit 1
|
|
97
137
|
end
|
|
98
138
|
RUBY
|
|
@@ -31,6 +31,7 @@ module LittleGhost
|
|
|
31
31
|
@call_mutex = Mutex.new
|
|
32
32
|
@call_tasks = []
|
|
33
33
|
@call_errors = []
|
|
34
|
+
@tool_calls = 0
|
|
34
35
|
@closing_marker = {closing: false}
|
|
35
36
|
@lifecycle_mutex = Mutex.new
|
|
36
37
|
@lifecycle_condition = ConditionVariable.new
|
|
@@ -191,6 +192,7 @@ module LittleGhost
|
|
|
191
192
|
@output = +""
|
|
192
193
|
@output_bytes = 0
|
|
193
194
|
@buffer = +""
|
|
195
|
+
@call_mutex.synchronize { @tool_calls = 0 }
|
|
194
196
|
@lifecycle_mutex.synchronize { @generation = generation }
|
|
195
197
|
start_watchdog(generation, @deadline)
|
|
196
198
|
[generation, @session]
|
|
@@ -272,6 +274,8 @@ module LittleGhost
|
|
|
272
274
|
@call_mutex.synchronize do
|
|
273
275
|
active = @call_tasks.count(&:alive?)
|
|
274
276
|
raise ProtocolError, "code-mode concurrent tool call limit exceeded" if active >= @limits.fetch(:concurrency)
|
|
277
|
+
@tool_calls += 1
|
|
278
|
+
raise ToolError, "tool call limit exceeded" if @tool_calls > @limits.fetch(:tool_calls)
|
|
275
279
|
|
|
276
280
|
call_errors = @call_errors
|
|
277
281
|
closing_marker = @closing_marker
|
|
@@ -58,6 +58,7 @@ module LittleGhost
|
|
|
58
58
|
Output and completion:
|
|
59
59
|
- The final Ruby expression becomes the completed program value.
|
|
60
60
|
- Use `text(value)` for user-visible output.
|
|
61
|
+
- Ordinary `puts`, `print`, `printf`, and `p` output is captured and combined into bounded chunks.
|
|
61
62
|
- Use `finish(value)` to complete early with a value.
|
|
62
63
|
|
|
63
64
|
The Sandbox controls filesystem, network, subprocess, and optional-library access. Do not assume host
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LittleGhost
|
|
4
|
+
# Carries provider-neutral inputs and results for text embeddings.
|
|
5
|
+
#
|
|
6
|
+
# Applications usually call LittleGhost.embed. Provider adapters receive a
|
|
7
|
+
# Request and return a Response without exposing provider response objects.
|
|
8
|
+
module Embeddings
|
|
9
|
+
DEFAULT_LIMITS = {
|
|
10
|
+
max_inputs: 128,
|
|
11
|
+
max_input_bytes: 64 * 1024,
|
|
12
|
+
max_total_bytes: 1024 * 1024
|
|
13
|
+
}.freeze # :nodoc:
|
|
14
|
+
|
|
15
|
+
Request = Data.define(:inputs, :settings, :limits, :cancellation_token, :deadline) do # :nodoc:
|
|
16
|
+
# Validates configured budgets before copying retained input strings.
|
|
17
|
+
def initialize(inputs:, settings: {}, limits: {}, cancellation_token: Support::CancellationToken.new, deadline: nil)
|
|
18
|
+
values = inputs.is_a?(String) ? [inputs] : inputs
|
|
19
|
+
unless values.is_a?(Array) && !values.empty? && values.all? { |value| value.is_a?(String) && !value.empty? }
|
|
20
|
+
raise ArgumentError, "inputs must be a nonempty String or Array of nonempty Strings"
|
|
21
|
+
end
|
|
22
|
+
raise ArgumentError, "settings must be a mapping" unless settings.respond_to?(:to_h)
|
|
23
|
+
raise ArgumentError, "limits must be a mapping" unless limits.respond_to?(:to_h)
|
|
24
|
+
|
|
25
|
+
configured_limits = DEFAULT_LIMITS.merge(limits.to_h.transform_keys(&:to_sym))
|
|
26
|
+
configured_limits.transform_values! { |value| Integer(value) }
|
|
27
|
+
unless configured_limits.values.all?(&:positive?)
|
|
28
|
+
raise ArgumentError, "embedding limits must be positive integers"
|
|
29
|
+
end
|
|
30
|
+
if values.length > configured_limits.fetch(:max_inputs)
|
|
31
|
+
raise UnsupportedInputError, "Embedding input count exceeds the configured limit"
|
|
32
|
+
end
|
|
33
|
+
total_bytes = 0
|
|
34
|
+
values.each do |value|
|
|
35
|
+
bytes = value.bytesize
|
|
36
|
+
if bytes > configured_limits.fetch(:max_input_bytes)
|
|
37
|
+
raise UnsupportedInputError, "An embedding input exceeds the configured byte limit"
|
|
38
|
+
end
|
|
39
|
+
total_bytes += bytes
|
|
40
|
+
if total_bytes > configured_limits.fetch(:max_total_bytes)
|
|
41
|
+
raise UnsupportedInputError, "Embedding inputs exceed the configured aggregate byte limit"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
super(
|
|
46
|
+
inputs: values.map { |value| value.dup.freeze }.freeze,
|
|
47
|
+
settings: settings.to_h.transform_keys(&:to_sym).freeze,
|
|
48
|
+
limits: configured_limits.freeze,
|
|
49
|
+
cancellation_token:,
|
|
50
|
+
deadline:
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Carries the validated inputs and controls for one embedding operation.
|
|
56
|
+
#
|
|
57
|
+
# LittleGhost builds this value for LittleGhost.embed and passes it to the
|
|
58
|
+
# selected provider. Provider implementations may receive calls
|
|
59
|
+
# concurrently. They must observe +cancellation_token+ and +deadline+ while
|
|
60
|
+
# performing external work.
|
|
61
|
+
class Request < Data # :doc:
|
|
62
|
+
##
|
|
63
|
+
# :singleton-method: new
|
|
64
|
+
# :call-seq:
|
|
65
|
+
# new(inputs:, settings: {}, limits: {}, cancellation_token: Support::CancellationToken.new, deadline: nil) -> Request
|
|
66
|
+
#
|
|
67
|
+
# Validates and freezes one String or a nonempty Array of Strings.
|
|
68
|
+
#
|
|
69
|
+
# +settings+ contains trusted model settings. +limits+ may override the
|
|
70
|
+
# input budgets:
|
|
71
|
+
#
|
|
72
|
+
# [+:max_inputs+]
|
|
73
|
+
# Number of input strings. Defaults to 128.
|
|
74
|
+
# [+:max_input_bytes+]
|
|
75
|
+
# Bytes allowed in one input. Defaults to 64 KiB.
|
|
76
|
+
# [+:max_total_bytes+]
|
|
77
|
+
# Bytes allowed across all inputs. Defaults to 1 MiB.
|
|
78
|
+
#
|
|
79
|
+
# Invalid shapes raise ArgumentError; inputs outside these budgets raise
|
|
80
|
+
# UnsupportedInputError.
|
|
81
|
+
|
|
82
|
+
##
|
|
83
|
+
# :attr_reader: inputs
|
|
84
|
+
# Frozen input strings in caller order.
|
|
85
|
+
|
|
86
|
+
##
|
|
87
|
+
# :attr_reader: settings
|
|
88
|
+
# Frozen model settings chosen by trusted application code.
|
|
89
|
+
|
|
90
|
+
##
|
|
91
|
+
# :attr_reader: limits
|
|
92
|
+
# Frozen input budgets applied before LittleGhost retains the strings.
|
|
93
|
+
|
|
94
|
+
##
|
|
95
|
+
# :attr_reader: cancellation_token
|
|
96
|
+
# Token that stops the provider operation when cancellation is requested.
|
|
97
|
+
|
|
98
|
+
##
|
|
99
|
+
# :attr_reader: deadline
|
|
100
|
+
# Monotonic deadline for the provider operation, or +nil+.
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LittleGhost
|
|
4
|
+
module Embeddings
|
|
5
|
+
Response = Data.define(:vectors, :usage, :metadata) do # :nodoc:
|
|
6
|
+
# Validates finite, consistently sized numeric vectors and freezes them.
|
|
7
|
+
def initialize(vectors:, usage: Usage.new, metadata: {})
|
|
8
|
+
values = Array(vectors)
|
|
9
|
+
unless !values.empty? && values.all? { |vector| vector.is_a?(Array) && !vector.empty? }
|
|
10
|
+
raise ProtocolError, "Embedding provider returned no vectors"
|
|
11
|
+
end
|
|
12
|
+
dimensions = values.first.length
|
|
13
|
+
unless values.all? { |vector| vector.length == dimensions && vector.all? { |number| number.is_a?(Numeric) && (!number.respond_to?(:finite?) || number.finite?) } }
|
|
14
|
+
raise ProtocolError, "Embedding provider returned invalid vectors"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
frozen_vectors = values.map { |vector| vector.map(&:to_f).freeze }.freeze
|
|
18
|
+
super(vectors: frozen_vectors, usage:, metadata: metadata.to_h.merge(dimensions:).freeze)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Number of numeric values in each vector.
|
|
22
|
+
def dimensions = metadata.fetch(:dimensions)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Carries validated embedding vectors without provider response objects.
|
|
26
|
+
#
|
|
27
|
+
# Vectors are finite, consistently sized, frozen, and ordered like the
|
|
28
|
+
# corresponding Request inputs. LittleGhost raises ProtocolError instead of
|
|
29
|
+
# returning an invalid or partial provider response.
|
|
30
|
+
class Response < Data # :doc:
|
|
31
|
+
##
|
|
32
|
+
# :singleton-method: new
|
|
33
|
+
# :call-seq:
|
|
34
|
+
# new(vectors:, usage: Usage.new, metadata: {}) -> Response
|
|
35
|
+
#
|
|
36
|
+
# Validates and freezes provider-neutral vectors, usage, and metadata.
|
|
37
|
+
|
|
38
|
+
##
|
|
39
|
+
# :attr_reader: vectors
|
|
40
|
+
# Frozen numeric vectors in request-input order.
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# :attr_reader: usage
|
|
44
|
+
# Normalized provider usage for the operation.
|
|
45
|
+
|
|
46
|
+
##
|
|
47
|
+
# :attr_reader: metadata
|
|
48
|
+
# Frozen operation metadata, including +dimensions+.
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# :method: dimensions
|
|
52
|
+
# Number of numeric values in each vector.
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/little_ghost/errors.rb
CHANGED
|
@@ -36,6 +36,8 @@ module LittleGhost
|
|
|
36
36
|
class AssemblyStepTimeoutError < AssemblyError; end
|
|
37
37
|
# Raised when an invocation contains an unsupported input form.
|
|
38
38
|
class UnsupportedInputError < InvocationError; end
|
|
39
|
+
# Raised when a provider does not implement the requested model operation.
|
|
40
|
+
class UnsupportedModelOperationError < ConfigurationError; end
|
|
39
41
|
# Base class for provider request, response, and protocol failures. Agent Runs
|
|
40
42
|
# normally record these as failed outcomes; provider retry policy may handle a
|
|
41
43
|
# retryable failure before it reaches the Run.
|
data/lib/little_ghost/model.rb
CHANGED
|
@@ -76,6 +76,26 @@ module LittleGhost
|
|
|
76
76
|
provider.stream(configured_request, &block)
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
+
# Applies profile settings and executes an Embeddings::Request.
|
|
80
|
+
#
|
|
81
|
+
# Returns an Embeddings::Response with one vector per input. A provider that
|
|
82
|
+
# returns another shape raises ProtocolError.
|
|
83
|
+
def embed(request)
|
|
84
|
+
configured = Embeddings::Request.new(
|
|
85
|
+
inputs: request.inputs,
|
|
86
|
+
settings: settings.merge(request.settings),
|
|
87
|
+
limits: request.limits,
|
|
88
|
+
cancellation_token: request.cancellation_token,
|
|
89
|
+
deadline: request.deadline
|
|
90
|
+
)
|
|
91
|
+
response = provider.embed(configured)
|
|
92
|
+
unless response.is_a?(Embeddings::Response) && response.vectors.length == configured.inputs.length
|
|
93
|
+
raise ProtocolError, "Embedding provider returned an unexpected vector count"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
response
|
|
97
|
+
end
|
|
98
|
+
|
|
79
99
|
# Uses advertised provider capabilities.
|
|
80
100
|
def capabilities
|
|
81
101
|
@capabilities ||= provider.capabilities(metadata: details.attributes)
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module LittleGhost
|
|
6
|
+
# Executes bounded model operations without creating an Agent or Run.
|
|
7
|
+
class ModelOperations # :nodoc:
|
|
8
|
+
MAX_STRUCTURED_RESULT_BYTES = 1_000_000
|
|
9
|
+
MAX_STRUCTURED_RESULT_DEPTH = 64
|
|
10
|
+
MAX_STRUCTURED_RESULT_NODES = 100_000
|
|
11
|
+
MAX_STRUCTURED_RESULT_REPAIR_ATTEMPTS = 3
|
|
12
|
+
|
|
13
|
+
def initialize(model_resolver:)
|
|
14
|
+
@model_resolver = model_resolver
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def generate(model:, messages:, result_schema: nil, settings: {}, structured_result_repair_attempts: 1, cancellation_token: Support::CancellationToken.new, deadline: nil)
|
|
18
|
+
resolved = @model_resolver.resolve(model)
|
|
19
|
+
schema = normalize_schema(result_schema)
|
|
20
|
+
repair_attempts = normalize_repair_attempts(structured_result_repair_attempts) if schema
|
|
21
|
+
strategy = StructuredOutput.resolve(schema, model: resolved, ordinary_tools: []) if schema
|
|
22
|
+
handle = Instrumentation.start(:generation, model_provider: resolved.target.provider, model_id: resolved.model_id, model_role: resolved.role, structured: !schema.nil?)
|
|
23
|
+
usage = Usage.new
|
|
24
|
+
conversation = messages.map { |message| Message.coerce(message) }
|
|
25
|
+
response = complete(resolved, messages: conversation, settings:, schema:, strategy:, repair: false, cancellation_token:, deadline:)
|
|
26
|
+
usage += response.usage
|
|
27
|
+
output, errors = schema ? parse_structured_response(response.message, schema, strategy) : [response.message.text, []]
|
|
28
|
+
conversation << (schema ? redact_structured_response(response.message, schema, strategy) : response.message)
|
|
29
|
+
repairs_remaining = repair_attempts
|
|
30
|
+
while schema && !errors.empty? && repairs_remaining.positive?
|
|
31
|
+
conversation << structured_repair_message(response.message, strategy, errors:, repairs_remaining:)
|
|
32
|
+
response = complete(resolved, messages: conversation, settings:, schema:, strategy:, repair: true, cancellation_token:, deadline:)
|
|
33
|
+
usage += response.usage
|
|
34
|
+
output, errors = parse_structured_response(response.message, schema, strategy)
|
|
35
|
+
conversation << redact_structured_response(response.message, schema, strategy)
|
|
36
|
+
repairs_remaining -= 1
|
|
37
|
+
end
|
|
38
|
+
unless errors.empty?
|
|
39
|
+
raise StructuredResultError.new(
|
|
40
|
+
"The model did not return a valid structured result after its repair attempts",
|
|
41
|
+
schema_name: schema.fetch(:name), validation_errors: errors
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
handle.finish(outcome: :success, **usage_attributes(usage))
|
|
45
|
+
structured_result = StructuredResult.new(schema_name: schema.fetch(:name), value: output) if schema
|
|
46
|
+
final_message = schema ? conversation.last : response.message
|
|
47
|
+
RunResult.new(
|
|
48
|
+
message: final_message,
|
|
49
|
+
stop_reason: schema ? :structured_result : response.stop_reason,
|
|
50
|
+
usage:,
|
|
51
|
+
messages: conversation.freeze,
|
|
52
|
+
state: DataMap.new,
|
|
53
|
+
structured_result:,
|
|
54
|
+
steps: []
|
|
55
|
+
)
|
|
56
|
+
rescue => error
|
|
57
|
+
handle&.finish(outcome: :error, error_type: error.class.name) if handle&.active?
|
|
58
|
+
raise
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def embed(model:, inputs:, settings: {}, limits: {}, cancellation_token: Support::CancellationToken.new, deadline: nil)
|
|
62
|
+
request = Embeddings::Request.new(inputs:, settings:, limits:, cancellation_token:, deadline:)
|
|
63
|
+
resolved = @model_resolver.resolve(model)
|
|
64
|
+
handle = Instrumentation.start(:embedding, model_provider: resolved.target.provider, model_id: resolved.model_id, model_role: resolved.role, input_count: request.inputs.length)
|
|
65
|
+
response = resolved.embed(request)
|
|
66
|
+
metadata = response.metadata.merge(provider: resolved.target.provider, model: resolved.model_id, model_role: resolved.role, input_count: request.inputs.length).compact
|
|
67
|
+
result = Embeddings::Response.new(vectors: response.vectors, usage: response.usage, metadata:)
|
|
68
|
+
handle.finish(outcome: :success, dimensions: result.dimensions, **usage_attributes(result.usage))
|
|
69
|
+
result
|
|
70
|
+
rescue => error
|
|
71
|
+
handle&.finish(outcome: :error, error_type: error.class.name) if handle&.active?
|
|
72
|
+
raise
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def complete(model, messages:, settings:, schema:, strategy:, repair:, cancellation_token:, deadline:)
|
|
78
|
+
request = ModelRequest.new(
|
|
79
|
+
messages:, settings:,
|
|
80
|
+
tools: strategy ? strategy.tools([]) : [],
|
|
81
|
+
output_schema: strategy&.output_schema,
|
|
82
|
+
tool_choice: direct_generation_tool_choice(strategy, repair:),
|
|
83
|
+
required_capabilities: strategy ? strategy.required_capabilities : [],
|
|
84
|
+
cancellation_token:, deadline:
|
|
85
|
+
)
|
|
86
|
+
response = nil
|
|
87
|
+
model.stream(request) do |event|
|
|
88
|
+
response = event.data[:response] if event.type == :message_stop
|
|
89
|
+
end
|
|
90
|
+
response || raise(ProtocolError, "Provider stream ended without a response")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def normalize_schema(value)
|
|
94
|
+
return unless value
|
|
95
|
+
raise ArgumentError, "result_schema must be a mapping" unless value.respond_to?(:to_h)
|
|
96
|
+
|
|
97
|
+
schema = value.to_h.transform_keys(&:to_sym)
|
|
98
|
+
name = schema.fetch(:name).to_s
|
|
99
|
+
json_schema = schema.fetch(:schema)
|
|
100
|
+
Class.new(Agent).result_schema(
|
|
101
|
+
json_schema,
|
|
102
|
+
name:,
|
|
103
|
+
description: schema[:description],
|
|
104
|
+
strategy: :auto
|
|
105
|
+
).except(:strategy).freeze
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def parse_structured_response(message, schema, strategy)
|
|
109
|
+
return parse_structured(message.text, schema) if strategy.provider?
|
|
110
|
+
|
|
111
|
+
tool_uses = message.content.grep(Content::ToolUse)
|
|
112
|
+
result_tool_uses = tool_uses.select { |tool_use| tool_use.name == strategy.schema_name }
|
|
113
|
+
return [nil, ["The structured result tool was not called"]] if result_tool_uses.empty?
|
|
114
|
+
return [nil, ["The model called the structured result tool more than once"]] if result_tool_uses.length > 1
|
|
115
|
+
return [nil, ["The structured result tool must be the only tool call in its response"]] if tool_uses.length > 1
|
|
116
|
+
|
|
117
|
+
validate_structured_value(result_tool_uses.first.input, schema)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def parse_structured(text, schema)
|
|
121
|
+
raise StructuredResultError.new("Structured result exceeds the maximum serialized size", schema_name: schema.fetch(:name)) if text.bytesize > MAX_STRUCTURED_RESULT_BYTES
|
|
122
|
+
value = JSON.parse(text)
|
|
123
|
+
validate_structured_value(value, schema)
|
|
124
|
+
rescue JSON::ParserError
|
|
125
|
+
[nil, ["Structured result is not valid JSON"]]
|
|
126
|
+
rescue StructuredResultError => error
|
|
127
|
+
[nil, [error.message]]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def validate_structured_value(value, schema)
|
|
131
|
+
validate_complexity!(value, schema.fetch(:name))
|
|
132
|
+
errors = Tool::SchemaValidator.new(schema.fetch(:schema)).validate(value)
|
|
133
|
+
[value, errors]
|
|
134
|
+
rescue StructuredResultError => error
|
|
135
|
+
[nil, [error.message]]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def structured_repair_message(message, strategy, errors:, repairs_remaining:)
|
|
139
|
+
tool_uses = message.content.grep(Content::ToolUse)
|
|
140
|
+
feedback = "The structured result did not match the required schema: #{errors.join("; ")}. Submit it again using the required schema."
|
|
141
|
+
if strategy.tool? && !tool_uses.empty?
|
|
142
|
+
return Message.new(
|
|
143
|
+
role: :tool,
|
|
144
|
+
content: tool_uses.map do |tool_use|
|
|
145
|
+
Content::ToolResult.new(
|
|
146
|
+
tool_use_id: tool_use.id,
|
|
147
|
+
content: feedback,
|
|
148
|
+
status: :error
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
requirement = strategy.tool? ? "Call #{strategy.schema_name} exactly once as your only tool call." : "Return only JSON matching the configured output schema."
|
|
155
|
+
attempts_description = (repairs_remaining == 1) ? "one repair attempt" : "#{repairs_remaining} repair attempts"
|
|
156
|
+
Message.new(role: :user, content: "#{requirement} You have #{attempts_description} remaining. #{feedback}")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def direct_generation_tool_choice(strategy, repair:)
|
|
160
|
+
return unless strategy
|
|
161
|
+
return {name: strategy.schema_name}.freeze if strategy.tool?
|
|
162
|
+
|
|
163
|
+
strategy.tool_choice(repair:)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def normalize_repair_attempts(value)
|
|
167
|
+
return value if value.is_a?(Integer) && value.between?(0, MAX_STRUCTURED_RESULT_REPAIR_ATTEMPTS)
|
|
168
|
+
|
|
169
|
+
raise ArgumentError, "structured_result_repair_attempts must be an integer from zero through #{MAX_STRUCTURED_RESULT_REPAIR_ATTEMPTS}"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def validate_complexity!(value, schema_name)
|
|
173
|
+
nodes = 0
|
|
174
|
+
stack = [[value, 1]]
|
|
175
|
+
until stack.empty?
|
|
176
|
+
child, depth = stack.pop
|
|
177
|
+
nodes += 1
|
|
178
|
+
raise StructuredResultError.new("Structured result exceeds the maximum nesting depth", schema_name:) if depth > MAX_STRUCTURED_RESULT_DEPTH
|
|
179
|
+
raise StructuredResultError.new("Structured result exceeds the maximum complexity", schema_name:) if nodes > MAX_STRUCTURED_RESULT_NODES
|
|
180
|
+
child.each { |key, nested| stack << [key, depth + 1] << [nested, depth + 1] } if child.is_a?(Hash)
|
|
181
|
+
child.each { |nested| stack << [nested, depth + 1] } if child.is_a?(Array)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def redact_structured_message(message, schema)
|
|
186
|
+
Message.new(
|
|
187
|
+
role: message.role,
|
|
188
|
+
content: "[Structured result #{schema.fetch(:name)} redacted]",
|
|
189
|
+
metadata: message.metadata
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def redact_structured_response(message, schema, strategy)
|
|
194
|
+
tool_uses = message.content.grep(Content::ToolUse)
|
|
195
|
+
return redact_structured_message(message, schema) unless strategy.tool? && !tool_uses.empty?
|
|
196
|
+
|
|
197
|
+
Message.new(
|
|
198
|
+
role: message.role,
|
|
199
|
+
content: tool_uses.map do |tool_use|
|
|
200
|
+
Content::ToolUse.new(id: tool_use.id, name: tool_use.name, input: {})
|
|
201
|
+
end,
|
|
202
|
+
metadata: message.metadata
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def usage_attributes(usage)
|
|
207
|
+
usage.to_h.except(:total_tokens)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
@@ -21,7 +21,12 @@ module LittleGhost
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
# Shared provider contract. Provider adapters implement #stream and may
|
|
24
|
-
# override capability-sensitive request preparation.
|
|
24
|
+
# implement #embed or override capability-sensitive request preparation.
|
|
25
|
+
#
|
|
26
|
+
# LittleGhost may call one provider instance concurrently. An embedding
|
|
27
|
+
# adapter accepts Embeddings::Request, observes its cancellation and
|
|
28
|
+
# deadline, and returns Embeddings::Response. Provider failures should use a
|
|
29
|
+
# content-safe ProviderError subclass.
|
|
25
30
|
class Base
|
|
26
31
|
# Returns the trusted per-profile request options this adapter accepts.
|
|
27
32
|
# Connection options remain authoritative and are configured separately.
|
|
@@ -32,6 +37,16 @@ module LittleGhost
|
|
|
32
37
|
raise AbstractMethodError, "#{self.class} must implement #stream"
|
|
33
38
|
end
|
|
34
39
|
|
|
40
|
+
# Executes +request+ when the adapter supports text embeddings.
|
|
41
|
+
#
|
|
42
|
+
# The default implementation raises UnsupportedModelOperationError.
|
|
43
|
+
#
|
|
44
|
+
# :call-seq:
|
|
45
|
+
# embed(request) -> Embeddings::Response
|
|
46
|
+
def embed(_request)
|
|
47
|
+
raise UnsupportedModelOperationError, "#{self.class} does not support embeddings"
|
|
48
|
+
end
|
|
49
|
+
|
|
35
50
|
# Applies provider-specific capability constraints before streaming.
|
|
36
51
|
def prepare_request(request, capabilities:)
|
|
37
52
|
request
|
|
@@ -45,7 +45,7 @@ module LittleGhost
|
|
|
45
45
|
|
|
46
46
|
def canonical_path(uri)
|
|
47
47
|
path = uri.path.empty? ? "/" : uri.path
|
|
48
|
-
path.split("/", -1).map { |part| URI.encode_www_form_component(
|
|
48
|
+
path.split("/", -1).map { |part| URI.encode_www_form_component(part).gsub("+", "%20") }.join("/")
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def canonical_query(uri)
|
|
@@ -10,6 +10,7 @@ module LittleGhost
|
|
|
10
10
|
# the adapter.
|
|
11
11
|
class HTTPClient # :nodoc:
|
|
12
12
|
Response = Data.define(:stream)
|
|
13
|
+
InvokeResponse = Data.define(:body)
|
|
13
14
|
DEFAULT_MAX_RESPONSE_BYTES = 256 * 1024 * 1024
|
|
14
15
|
|
|
15
16
|
def initialize(region:, credentials: nil, credential_resolver: nil, endpoint: nil,
|
|
@@ -44,33 +45,53 @@ module LittleGhost
|
|
|
44
45
|
Response.new(stream:)
|
|
45
46
|
end
|
|
46
47
|
|
|
48
|
+
def invoke_model(model_id:, body:, max_response_bytes:, cancellation_token: nil, deadline: nil)
|
|
49
|
+
payload = JSON.generate(camelize(body))
|
|
50
|
+
uri = URI.join(@endpoint.to_s, "/model/#{escape_path(model_id)}/invoke")
|
|
51
|
+
credentials = @credential_resolver.call
|
|
52
|
+
signer = AwsSigV4.new(service: "bedrock", region: @region, credentials:, clock: @clock)
|
|
53
|
+
headers = signer.headers(method: :post, uri:, headers: {"content-type" => "application/json", "accept" => "application/json"}, body: payload)
|
|
54
|
+
response = @http_client.request(
|
|
55
|
+
uri:, method: :post, headers:, body: payload,
|
|
56
|
+
cancellation_token:, deadline:, label: "Bedrock request", max_response_bytes:
|
|
57
|
+
)
|
|
58
|
+
InvokeResponse.new(body: response)
|
|
59
|
+
end
|
|
60
|
+
|
|
47
61
|
private
|
|
48
62
|
|
|
49
63
|
def event(headers, payload)
|
|
50
64
|
value = payload.empty? ? {} : JSON.parse(payload)
|
|
51
65
|
type = headers[":event-type"] || headers[":exception-type"]
|
|
52
|
-
{type
|
|
66
|
+
{underscore(type).to_sym => normalize_event_payload(value)}
|
|
53
67
|
rescue JSON::ParserError => error
|
|
54
68
|
raise ProtocolError, "Bedrock returned invalid event JSON: #{error.message}"
|
|
55
69
|
end
|
|
56
70
|
|
|
57
|
-
def camelize(value)
|
|
71
|
+
def camelize(value, json_schema: false, input_schema: false)
|
|
58
72
|
case value
|
|
59
73
|
when Hash
|
|
60
|
-
value.to_h
|
|
61
|
-
|
|
74
|
+
value.to_h do |key, child|
|
|
75
|
+
key = key.to_s
|
|
76
|
+
[json_schema ? key : camelize_key(key), camelize(child, json_schema: json_schema || (input_schema && key == "json"), input_schema: key == "input_schema")]
|
|
77
|
+
end
|
|
78
|
+
when Array then value.map { |child| camelize(child, json_schema:, input_schema: false) }
|
|
62
79
|
else value
|
|
63
80
|
end
|
|
64
81
|
end
|
|
65
82
|
|
|
66
|
-
def
|
|
83
|
+
def camelize_key(key) = key.gsub(/_([a-z])/) { Regexp.last_match(1).upcase }
|
|
84
|
+
|
|
85
|
+
def normalize_event_payload(value)
|
|
67
86
|
case value
|
|
68
|
-
when Hash then value.to_h { |key, child| [key.to_sym,
|
|
69
|
-
when Array then value.map { |child|
|
|
87
|
+
when Hash then value.to_h { |key, child| [underscore(key).to_sym, normalize_event_payload(child)] }
|
|
88
|
+
when Array then value.map { |child| normalize_event_payload(child) }
|
|
70
89
|
else value
|
|
71
90
|
end
|
|
72
91
|
end
|
|
73
92
|
|
|
93
|
+
def underscore(value) = value.to_s.gsub(/([a-z0-9])([A-Z])/, "\\1_\\2").downcase
|
|
94
|
+
|
|
74
95
|
def escape_path(value) = URI.encode_www_form_component(value.to_s).gsub("+", "%20")
|
|
75
96
|
end
|
|
76
97
|
end
|
|
@@ -11,9 +11,9 @@ module LittleGhost
|
|
|
11
11
|
# request and response types. Agents select them through model configuration rather
|
|
12
12
|
# than depending on a provider class directly.
|
|
13
13
|
module Providers
|
|
14
|
-
# Bedrock lets LittleGhost
|
|
15
|
-
#
|
|
16
|
-
# LittleGhost provider.
|
|
14
|
+
# Bedrock lets LittleGhost features use models available through Amazon
|
|
15
|
+
# Bedrock. Generation uses Converse and follows the same streaming events as
|
|
16
|
+
# every other LittleGhost provider.
|
|
17
17
|
#
|
|
18
18
|
# provider = LittleGhost::Providers::Bedrock.new(
|
|
19
19
|
# model: ENV.fetch("BEDROCK_MODEL_ID"),
|
|
@@ -34,6 +34,7 @@ module LittleGhost
|
|
|
34
34
|
|
|
35
35
|
INITIAL_RETRY_DELAY = 1 # :nodoc:
|
|
36
36
|
MAX_RETRY_DELAY = 16 # :nodoc:
|
|
37
|
+
DEFAULT_MAX_EMBEDDING_RESPONSE_BYTES = 8 * 1024 * 1024 # :nodoc:
|
|
37
38
|
TRANSIENT_STREAM_ERRORS = %w[
|
|
38
39
|
internal_server_exception model_stream_error_exception service_unavailable_exception throttling_exception
|
|
39
40
|
].freeze # :nodoc:
|
|
@@ -63,15 +64,17 @@ module LittleGhost
|
|
|
63
64
|
# Configures Bedrock for +model+.
|
|
64
65
|
#
|
|
65
66
|
# +region+ and remaining +client_options+ configure the built-in HTTP client.
|
|
66
|
-
# +max_retries+, +sleeper+, and +on_retry+ control retry behavior.
|
|
67
|
-
# +
|
|
67
|
+
# +max_retries+, +sleeper+, and +on_retry+ control retry behavior.
|
|
68
|
+
# +max_embedding_response_bytes+ bounds each embedding response retained in
|
|
69
|
+
# memory. Injecting +client+ bypasses creation of the built-in HTTP client.
|
|
68
70
|
def initialize(model:, region: nil, client: nil, max_retries: 2, sleeper: nil,
|
|
69
|
-
on_retry: ->(*) {}, **client_options)
|
|
71
|
+
on_retry: ->(*) {}, max_embedding_response_bytes: DEFAULT_MAX_EMBEDDING_RESPONSE_BYTES, **client_options)
|
|
70
72
|
@model = model
|
|
71
73
|
@client = client || build_client(region:, **client_options)
|
|
72
74
|
@max_retries = Integer(max_retries)
|
|
73
75
|
@sleeper = sleeper
|
|
74
76
|
@on_retry = on_retry
|
|
77
|
+
@max_embedding_response_bytes = positive_integer(max_embedding_response_bytes, :max_embedding_response_bytes)
|
|
75
78
|
end
|
|
76
79
|
|
|
77
80
|
# Streams LittleGhost StreamEvent objects for +request+.
|
|
@@ -132,6 +135,49 @@ module LittleGhost
|
|
|
132
135
|
end
|
|
133
136
|
end
|
|
134
137
|
|
|
138
|
+
# Embeds text with Amazon Titan Text Embeddings V2.
|
|
139
|
+
#
|
|
140
|
+
# The +:dimensions+ request setting accepts 256, 512, or 1024 and defaults
|
|
141
|
+
# to 1024. +:normalize+ controls vector normalization and defaults to true.
|
|
142
|
+
# Inputs are requested sequentially, and a failure raises without returning
|
|
143
|
+
# a partial batch.
|
|
144
|
+
def embed(request)
|
|
145
|
+
unless model == "amazon.titan-embed-text-v2:0"
|
|
146
|
+
raise UnsupportedModelOperationError, "Bedrock embeddings require amazon.titan-embed-text-v2:0"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
dimensions = Integer(request.settings.fetch(:dimensions, 1024))
|
|
150
|
+
raise ConfigurationError, "dimensions must be 256, 512, or 1024" unless [256, 512, 1024].include?(dimensions)
|
|
151
|
+
normalize = request.settings.fetch(:normalize, true)
|
|
152
|
+
unless normalize == true || normalize == false
|
|
153
|
+
raise ConfigurationError, "normalize must be true or false"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
vectors = []
|
|
157
|
+
usage = Usage.new
|
|
158
|
+
request.inputs.each do |input|
|
|
159
|
+
response = with_retries(request) do
|
|
160
|
+
@client.invoke_model(
|
|
161
|
+
model_id: model,
|
|
162
|
+
body: {input_text: input, dimensions:, normalize:},
|
|
163
|
+
cancellation_token: request.cancellation_token,
|
|
164
|
+
deadline: request.deadline,
|
|
165
|
+
max_response_bytes: @max_embedding_response_bytes
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
payload = JSON.parse(response.body)
|
|
169
|
+
vector = payload.fetch("embedding")
|
|
170
|
+
unless vector.is_a?(Array) && vector.length == dimensions
|
|
171
|
+
raise ProtocolError, "Bedrock returned an embedding with unexpected dimensions"
|
|
172
|
+
end
|
|
173
|
+
vectors << vector
|
|
174
|
+
usage += Usage.new(input_tokens: payload["inputTextTokenCount"])
|
|
175
|
+
rescue JSON::ParserError, KeyError
|
|
176
|
+
raise ProtocolError, "Bedrock returned an invalid embedding response"
|
|
177
|
+
end
|
|
178
|
+
Embeddings::Response.new(vectors:, usage:, metadata: {model:})
|
|
179
|
+
end
|
|
180
|
+
|
|
135
181
|
# Reads capabilities from Bedrock +supported_parameters+ metadata. Missing
|
|
136
182
|
# metadata produces ModelCapabilities.unknown.
|
|
137
183
|
def capabilities(metadata: {})
|
|
@@ -149,6 +195,29 @@ module LittleGhost
|
|
|
149
195
|
|
|
150
196
|
private
|
|
151
197
|
|
|
198
|
+
def positive_integer(value, name)
|
|
199
|
+
integer = Integer(value)
|
|
200
|
+
raise ArgumentError, "#{name} must be positive" unless integer.positive?
|
|
201
|
+
|
|
202
|
+
integer
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def with_retries(request)
|
|
206
|
+
attempts = 0
|
|
207
|
+
begin
|
|
208
|
+
request.cancellation_token.raise_if_cancelled!
|
|
209
|
+
yield
|
|
210
|
+
rescue HTTPError => error
|
|
211
|
+
raise unless error.retryable? && attempts < @max_retries
|
|
212
|
+
|
|
213
|
+
attempts += 1
|
|
214
|
+
delay = capped_retry_delay(request, retry_delay(attempts))
|
|
215
|
+
@on_retry.call(attempts, error, delay)
|
|
216
|
+
wait_before_retry(request, delay)
|
|
217
|
+
retry
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
152
221
|
def build_client(region:, **options)
|
|
153
222
|
resolver = options.delete(:credential_resolver)
|
|
154
223
|
resolver ||= CredentialResolver.new
|
|
@@ -261,9 +330,8 @@ module LittleGhost
|
|
|
261
330
|
tool_spec = {
|
|
262
331
|
name: definition.fetch(:name),
|
|
263
332
|
description: definition[:description],
|
|
264
|
-
input_schema: {json: definition[:input_schema] || {}}
|
|
333
|
+
input_schema: {json: bedrock_tool_schema(definition[:input_schema] || {})}
|
|
265
334
|
}
|
|
266
|
-
tool_spec[:strict] = definition[:strict] unless definition[:strict].nil?
|
|
267
335
|
{
|
|
268
336
|
tool_spec: {
|
|
269
337
|
**tool_spec
|
|
@@ -277,6 +345,18 @@ module LittleGhost
|
|
|
277
345
|
{tool: {name: choice.fetch(:name).to_s}}
|
|
278
346
|
end
|
|
279
347
|
|
|
348
|
+
def bedrock_tool_schema(schema)
|
|
349
|
+
return schema unless nova_model?
|
|
350
|
+
|
|
351
|
+
schema.to_h.each_with_object({}) do |(key, value), result|
|
|
352
|
+
result[key] = value if %w[type properties required].include?(key.to_s)
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def nova_model?
|
|
357
|
+
model.match?(/(?:^|\.)amazon\.nova(?:[-.])/)
|
|
358
|
+
end
|
|
359
|
+
|
|
280
360
|
def extract_settings(settings, keys)
|
|
281
361
|
keys.each_with_object({}) do |key, result|
|
|
282
362
|
value = settings[key] || settings[key.to_s]
|
|
@@ -409,6 +489,10 @@ module LittleGhost
|
|
|
409
489
|
raise ProtocolError, "Bedrock stream ended before message_stop" unless @terminal
|
|
410
490
|
|
|
411
491
|
@finished = true
|
|
492
|
+
if @stop_reason == :malformed_tool_use
|
|
493
|
+
raise MalformedToolCallError, "Bedrock returned malformed_tool_use"
|
|
494
|
+
end
|
|
495
|
+
|
|
412
496
|
blocks = []
|
|
413
497
|
@reasoning_blocks.sort.each do |_index, reasoning|
|
|
414
498
|
if !reasoning[:redacted_content].empty?
|
|
@@ -503,6 +587,7 @@ module LittleGhost
|
|
|
503
587
|
def normalize_stop(value)
|
|
504
588
|
case value
|
|
505
589
|
when "tool_use" then :tool_use
|
|
590
|
+
when "malformed_tool_use" then :malformed_tool_use
|
|
506
591
|
when "max_tokens" then :max_tokens
|
|
507
592
|
when "guardrail_intervened", "content_filtered" then :content_filter
|
|
508
593
|
else :end_turn
|
|
@@ -4,8 +4,9 @@ require_relative "openai_compatible"
|
|
|
4
4
|
|
|
5
5
|
module LittleGhost
|
|
6
6
|
module Providers
|
|
7
|
-
# OpenAI connects LittleGhost
|
|
8
|
-
#
|
|
7
|
+
# OpenAI connects LittleGhost features to OpenAI models for generation and
|
|
8
|
+
# embeddings. Generation uses the Responses API by default and supports
|
|
9
|
+
# streaming, Tools, and structured results.
|
|
9
10
|
#
|
|
10
11
|
# provider = LittleGhost::Providers::OpenAI.new(
|
|
11
12
|
# api_key: ENV.fetch("OPENAI_API_KEY"),
|
|
@@ -17,10 +18,85 @@ module LittleGhost
|
|
|
17
18
|
class OpenAI < OpenAICompatible
|
|
18
19
|
# The OpenAI API endpoint used when +base_url+ is omitted.
|
|
19
20
|
DEFAULT_BASE_URL = "https://api.openai.com/v1/"
|
|
21
|
+
DEFAULT_MAX_EMBEDDING_RESPONSE_BYTES = 8 * 1024 * 1024 # :nodoc:
|
|
20
22
|
|
|
21
23
|
# Uses the official OpenAI API base URL by default.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
#
|
|
25
|
+
# +max_embedding_response_bytes+ bounds the response retained for one
|
|
26
|
+
# embedding batch. Remaining +arguments+ configure the shared generation
|
|
27
|
+
# transport and retry behavior.
|
|
28
|
+
def initialize(base_url: DEFAULT_BASE_URL, max_embedding_response_bytes: DEFAULT_MAX_EMBEDDING_RESPONSE_BYTES, **arguments)
|
|
29
|
+
@max_embedding_response_bytes = Integer(max_embedding_response_bytes)
|
|
30
|
+
raise ArgumentError, "max_embedding_response_bytes must be positive" unless @max_embedding_response_bytes.positive?
|
|
31
|
+
|
|
32
|
+
super(base_url:, **arguments)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Embeds one or more strings with the configured OpenAI model.
|
|
36
|
+
#
|
|
37
|
+
# The optional +:dimensions+ request setting selects a supported output
|
|
38
|
+
# size for models that accept it. The response preserves input order and
|
|
39
|
+
# raises ProtocolError when OpenAI returns an incomplete or invalid batch.
|
|
40
|
+
def embed(request)
|
|
41
|
+
attempts = 0
|
|
42
|
+
begin
|
|
43
|
+
request.cancellation_token.raise_if_cancelled!
|
|
44
|
+
payload = {
|
|
45
|
+
model:,
|
|
46
|
+
input: request.inputs,
|
|
47
|
+
encoding_format: "float"
|
|
48
|
+
}
|
|
49
|
+
dimensions = request.settings[:dimensions]
|
|
50
|
+
payload[:dimensions] = Integer(dimensions) if dimensions
|
|
51
|
+
body = +""
|
|
52
|
+
@transport.stream(
|
|
53
|
+
path: "embeddings",
|
|
54
|
+
headers: {"Authorization" => "Bearer #{@api_key}", "Content-Type" => "application/json"}.merge(@headers),
|
|
55
|
+
body: JSON.generate(payload),
|
|
56
|
+
cancellation_token: request.cancellation_token,
|
|
57
|
+
deadline: request.deadline
|
|
58
|
+
) do |chunk|
|
|
59
|
+
if body.bytesize + chunk.bytesize > @max_embedding_response_bytes
|
|
60
|
+
raise ProtocolError, "OpenAI embedding response exceeded #{@max_embedding_response_bytes} bytes"
|
|
61
|
+
end
|
|
62
|
+
body << chunk
|
|
63
|
+
end
|
|
64
|
+
normalize_embedding_response(body, request.inputs.length, dimensions && Integer(dimensions))
|
|
65
|
+
rescue HTTPError => error
|
|
66
|
+
raise unless error.retryable? && attempts < @max_retries
|
|
67
|
+
|
|
68
|
+
attempts += 1
|
|
69
|
+
delay = capped_retry_delay(request, retry_delay(attempts))
|
|
70
|
+
@on_retry.call(attempts, error, delay)
|
|
71
|
+
wait_before_retry(request, delay)
|
|
72
|
+
retry
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def normalize_embedding_response(body, input_count, expected_dimensions)
|
|
79
|
+
payload = JSON.parse(body)
|
|
80
|
+
data = payload.fetch("data")
|
|
81
|
+
raise ProtocolError, "OpenAI returned an invalid embedding count" unless data.is_a?(Array) && data.length == input_count
|
|
82
|
+
|
|
83
|
+
ordered = data.sort_by { |item| Integer(item.fetch("index")) }
|
|
84
|
+
expected = (0...input_count).to_a
|
|
85
|
+
raise ProtocolError, "OpenAI returned invalid embedding indices" unless ordered.map { |item| Integer(item.fetch("index")) } == expected
|
|
86
|
+
|
|
87
|
+
vectors = ordered.map { |item| item.fetch("embedding") }
|
|
88
|
+
if expected_dimensions && vectors.any? { |vector| !vector.is_a?(Array) || vector.length != expected_dimensions }
|
|
89
|
+
raise ProtocolError, "OpenAI returned embeddings with unexpected dimensions"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
usage = payload.fetch("usage", {})
|
|
93
|
+
Embeddings::Response.new(
|
|
94
|
+
vectors:,
|
|
95
|
+
usage: Usage.new(input_tokens: usage["prompt_tokens"] || usage["input_tokens"]),
|
|
96
|
+
metadata: {model: payload["model"] || model}
|
|
97
|
+
)
|
|
98
|
+
rescue JSON::ParserError, KeyError, ArgumentError, TypeError
|
|
99
|
+
raise ProtocolError, "OpenAI returned an invalid embedding response"
|
|
24
100
|
end
|
|
25
101
|
end
|
|
26
102
|
end
|
|
@@ -91,6 +91,7 @@ module LittleGhost
|
|
|
91
91
|
automatic: automatic_descriptor,
|
|
92
92
|
oversized:
|
|
93
93
|
)
|
|
94
|
+
content = append_materialized_references(content, all_descriptors)
|
|
94
95
|
Tool::ExecutionResult.new(
|
|
95
96
|
value: result.value,
|
|
96
97
|
content:,
|
|
@@ -229,6 +230,13 @@ module LittleGhost
|
|
|
229
230
|
end.freeze
|
|
230
231
|
end
|
|
231
232
|
|
|
233
|
+
def append_materialized_references(content, artifacts)
|
|
234
|
+
references = artifacts.filter_map { |artifact| artifact.reference if artifact.bytes }
|
|
235
|
+
return content if references.empty?
|
|
236
|
+
|
|
237
|
+
"#{content}\n\nWorkspace artifacts:\n#{references.map { |reference| "- #{reference}" }.join("\n")}"
|
|
238
|
+
end
|
|
239
|
+
|
|
232
240
|
def oversized_artifact(result, tool_use:)
|
|
233
241
|
data, media_type, extension = serialized_value(result.value)
|
|
234
242
|
Artifact.new(
|
data/lib/little_ghost/runtime.rb
CHANGED
|
@@ -136,6 +136,7 @@ module LittleGhost
|
|
|
136
136
|
@invocation_class = @settings[:invocation] || Invocation
|
|
137
137
|
@model_resolver = @settings.fetch(:model_resolver)
|
|
138
138
|
@default_model = @settings.fetch(:default_model, "default").to_s
|
|
139
|
+
@model_operations = ModelOperations.new(model_resolver:)
|
|
139
140
|
|
|
140
141
|
@startup_phase = "session_store"
|
|
141
142
|
@session_store = build_session_store(@settings[:session_store])
|
|
@@ -183,6 +184,28 @@ module LittleGhost
|
|
|
183
184
|
payload.is_a?(@invocation_class) ? payload : @invocation_class.new(payload)
|
|
184
185
|
end
|
|
185
186
|
|
|
187
|
+
# :call-seq:
|
|
188
|
+
# generate(model:, messages:, result_schema: nil, settings: {}, structured_result_repair_attempts: 1, cancellation_token: Support::CancellationToken.new, deadline: nil) -> RunResult
|
|
189
|
+
#
|
|
190
|
+
# Generates one response through this Runtime's model resolver.
|
|
191
|
+
#
|
|
192
|
+
# Returns a RunResult without creating a Run or invoking runtime hooks. See
|
|
193
|
+
# LittleGhost.generate for the operation contract.
|
|
194
|
+
def generate(**arguments)
|
|
195
|
+
@model_operations.generate(**arguments)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# :call-seq:
|
|
199
|
+
# embed(model:, inputs:, settings: {}, limits: {}, cancellation_token: Support::CancellationToken.new, deadline: nil) -> Embeddings::Response
|
|
200
|
+
#
|
|
201
|
+
# Embeds text through this Runtime's model resolver.
|
|
202
|
+
#
|
|
203
|
+
# Returns an Embeddings::Response without creating a Run or invoking runtime
|
|
204
|
+
# hooks. See LittleGhost.embed for the operation contract.
|
|
205
|
+
def embed(**arguments)
|
|
206
|
+
@model_operations.embed(**arguments)
|
|
207
|
+
end
|
|
208
|
+
|
|
186
209
|
# Creates a Run that owns any workspace and sandbox built for the request.
|
|
187
210
|
#
|
|
188
211
|
# +include_agent_events_by_default+ is trusted stream policy for the Run
|
|
@@ -81,7 +81,8 @@ module LittleGhost
|
|
|
81
81
|
|
|
82
82
|
# Executes a bounded request and returns the complete response body.
|
|
83
83
|
def request(uri:, method: :get, headers: {}, body: nil, allow_insecure_http: false,
|
|
84
|
-
cancellation_token: nil, deadline: nil)
|
|
84
|
+
cancellation_token: nil, deadline: nil, label: "HTTP request", max_response_bytes: @max_response_bytes)
|
|
85
|
+
response_limit = positive_integer(max_response_bytes, :max_response_bytes)
|
|
85
86
|
response_body = +""
|
|
86
87
|
each_chunk(
|
|
87
88
|
uri:,
|
|
@@ -90,8 +91,14 @@ module LittleGhost
|
|
|
90
91
|
body:,
|
|
91
92
|
cancellation_token:,
|
|
92
93
|
deadline: deadline || Time.now + @read_timeout,
|
|
93
|
-
allow_insecure_http
|
|
94
|
-
|
|
94
|
+
allow_insecure_http:,
|
|
95
|
+
label:
|
|
96
|
+
) do |chunk|
|
|
97
|
+
if response_body.bytesize + chunk.bytesize > response_limit
|
|
98
|
+
raise ProtocolError, "#{label} response exceeded #{response_limit} bytes"
|
|
99
|
+
end
|
|
100
|
+
response_body << chunk
|
|
101
|
+
end
|
|
95
102
|
response_body
|
|
96
103
|
end
|
|
97
104
|
|
|
@@ -61,7 +61,9 @@ module LittleGhost
|
|
|
61
61
|
workflow: "invoke_workflow",
|
|
62
62
|
swarm: "invoke_swarm",
|
|
63
63
|
graph: "invoke_graph",
|
|
64
|
-
assembly: "invoke_assembly"
|
|
64
|
+
assembly: "invoke_assembly",
|
|
65
|
+
generation: "chat",
|
|
66
|
+
embedding: "embeddings"
|
|
65
67
|
}.freeze # :nodoc:
|
|
66
68
|
REQUEST_SETTING_ATTRIBUTES = {
|
|
67
69
|
frequency_penalty: "gen_ai.request.frequency_penalty",
|
|
@@ -244,7 +246,7 @@ module LittleGhost
|
|
|
244
246
|
end
|
|
245
247
|
description = [error_type, event_attributes["exception.message"]].compact.join(": ")
|
|
246
248
|
span.status = ::OpenTelemetry::Trace::Status.error(description)
|
|
247
|
-
event_name = (kind
|
|
249
|
+
event_name = %i[model generation embedding].include?(kind) ? "gen_ai.client.operation.exception" : "exception"
|
|
248
250
|
span.add_event(event_name, attributes: event_attributes.compact)
|
|
249
251
|
end
|
|
250
252
|
|
|
@@ -270,7 +272,7 @@ module LittleGhost
|
|
|
270
272
|
when :swarm, :graph, :assembly then attributes[:assembly_id]
|
|
271
273
|
when :assembly_step then attributes[:participant]
|
|
272
274
|
when :agent_turn then attributes[:turn]
|
|
273
|
-
when :model then attributes[:model_id]
|
|
275
|
+
when :model, :generation, :embedding then attributes[:model_id]
|
|
274
276
|
when :subagent then attributes[:subagent_id] || attributes[:kind]
|
|
275
277
|
when :tool then attributes[:tool_name]
|
|
276
278
|
end
|
|
@@ -448,7 +450,7 @@ module LittleGhost
|
|
|
448
450
|
end
|
|
449
451
|
|
|
450
452
|
def span_kind(kind)
|
|
451
|
-
%i[model session_store].include?(kind) ? :client : :internal
|
|
453
|
+
%i[model generation embedding session_store].include?(kind) ? :client : :internal
|
|
452
454
|
end
|
|
453
455
|
|
|
454
456
|
def gen_ai_usage_value(key, attributes, value)
|
data/lib/little_ghost/version.rb
CHANGED
data/lib/little_ghost.rb
CHANGED
|
@@ -20,6 +20,8 @@ require_relative "little_ghost/stream_event"
|
|
|
20
20
|
require_relative "little_ghost/agent_stream_source"
|
|
21
21
|
require_relative "little_ghost/model_request"
|
|
22
22
|
require_relative "little_ghost/model_response"
|
|
23
|
+
require_relative "little_ghost/embeddings/request"
|
|
24
|
+
require_relative "little_ghost/embeddings/response"
|
|
23
25
|
require_relative "little_ghost/run_result"
|
|
24
26
|
require_relative "little_ghost/model_capabilities"
|
|
25
27
|
require_relative "little_ghost/models/target"
|
|
@@ -82,6 +84,7 @@ require_relative "little_ghost/agent_factory"
|
|
|
82
84
|
require_relative "little_ghost/runtime/hook"
|
|
83
85
|
require_relative "little_ghost/runtime/hooks/artifacts"
|
|
84
86
|
require_relative "little_ghost/runtime"
|
|
87
|
+
require_relative "little_ghost/model_operations"
|
|
85
88
|
|
|
86
89
|
# Build AI features as ordinary Ruby classes. An Agent owns one model
|
|
87
90
|
# conversation. Larger units called Assemblies coordinate several Agents while
|
|
@@ -151,6 +154,28 @@ module LittleGhost
|
|
|
151
154
|
# Returns the model resolver owned by the active process configuration.
|
|
152
155
|
def model_resolver = configuration.model_resolver
|
|
153
156
|
|
|
157
|
+
# :call-seq:
|
|
158
|
+
# LittleGhost.generate(model:, messages:, result_schema: nil, settings: {}, structured_result_repair_attempts: 1, cancellation_token: Support::CancellationToken.new, deadline: nil) -> RunResult
|
|
159
|
+
#
|
|
160
|
+
# Generates one model response and returns a RunResult.
|
|
161
|
+
#
|
|
162
|
+
# Use this entrypoint when application code owns the workflow and does not
|
|
163
|
+
# need Tools, Sessions, delegation, or agent callbacks. +model+ and
|
|
164
|
+
# +settings+ are trusted application controls. A +result_schema+ checks one
|
|
165
|
+
# object result. When the result is invalid, the default permits one repair
|
|
166
|
+
# attempt. Set +structured_result_repair_attempts+ to an integer from zero
|
|
167
|
+
# through three when a checked result warrants additional attempts.
|
|
168
|
+
def generate(...) = runtime.generate(...)
|
|
169
|
+
|
|
170
|
+
# :call-seq:
|
|
171
|
+
# LittleGhost.embed(model:, inputs:, settings: {}, limits: {}, cancellation_token: Support::CancellationToken.new, deadline: nil) -> Embeddings::Response
|
|
172
|
+
#
|
|
173
|
+
# Embeds one or more strings and returns vectors in input order.
|
|
174
|
+
#
|
|
175
|
+
# +model+, +settings+, and any raised +limits+ are trusted application
|
|
176
|
+
# controls. The operation raises rather than returning a partial batch.
|
|
177
|
+
def embed(...) = runtime.embed(...)
|
|
178
|
+
|
|
154
179
|
# Makes +configuration+ and its independent shared Runtime current only
|
|
155
180
|
# while the block runs.
|
|
156
181
|
#
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: little_ghost
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matt Robinson
|
|
@@ -225,6 +225,8 @@ files:
|
|
|
225
225
|
- lib/little_ghost/content.rb
|
|
226
226
|
- lib/little_ghost/data/model_catalog.json
|
|
227
227
|
- lib/little_ghost/data_map.rb
|
|
228
|
+
- lib/little_ghost/embeddings/request.rb
|
|
229
|
+
- lib/little_ghost/embeddings/response.rb
|
|
228
230
|
- lib/little_ghost/errors.rb
|
|
229
231
|
- lib/little_ghost/events.rb
|
|
230
232
|
- lib/little_ghost/execution.rb
|
|
@@ -240,6 +242,7 @@ files:
|
|
|
240
242
|
- lib/little_ghost/message.rb
|
|
241
243
|
- lib/little_ghost/model.rb
|
|
242
244
|
- lib/little_ghost/model_capabilities.rb
|
|
245
|
+
- lib/little_ghost/model_operations.rb
|
|
243
246
|
- lib/little_ghost/model_request.rb
|
|
244
247
|
- lib/little_ghost/model_resolver.rb
|
|
245
248
|
- lib/little_ghost/model_response.rb
|
|
@@ -342,14 +345,14 @@ files:
|
|
|
342
345
|
- lib/little_ghost/version.rb
|
|
343
346
|
- lib/little_ghost/workflow.rb
|
|
344
347
|
- lib/little_ghost/workspace.rb
|
|
345
|
-
homepage: https://github.com/
|
|
348
|
+
homepage: https://github.com/littleghostai/little_ghost
|
|
346
349
|
licenses:
|
|
347
350
|
- MIT
|
|
348
351
|
metadata:
|
|
349
|
-
bug_tracker_uri: https://github.com/
|
|
350
|
-
changelog_uri: https://github.com/
|
|
351
|
-
documentation_uri: https://
|
|
352
|
-
source_code_uri: https://github.com/
|
|
352
|
+
bug_tracker_uri: https://github.com/littleghostai/little_ghost/issues
|
|
353
|
+
changelog_uri: https://github.com/littleghostai/little_ghost/releases
|
|
354
|
+
documentation_uri: https://littleghostai.org/docs/
|
|
355
|
+
source_code_uri: https://github.com/littleghostai/little_ghost
|
|
353
356
|
allowed_push_host: https://rubygems.org
|
|
354
357
|
rubygems_mfa_required: 'true'
|
|
355
358
|
rdoc_options: []
|