lex-llm 0.4.18 → 0.5.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 +4 -4
- data/.rubocop.yml +13 -2
- data/B1b-conformance-kit.md +79 -0
- data/CHANGELOG.md +27 -0
- data/lex-llm.gemspec +2 -3
- data/lib/legion/extensions/llm/attachment.rb +1 -1
- data/lib/legion/extensions/llm/canonical/chunk.rb +184 -0
- data/lib/legion/extensions/llm/canonical/content_block.rb +126 -0
- data/lib/legion/extensions/llm/canonical/message.rb +138 -0
- data/lib/legion/extensions/llm/canonical/params.rb +61 -0
- data/lib/legion/extensions/llm/canonical/request.rb +117 -0
- data/lib/legion/extensions/llm/canonical/response.rb +124 -0
- data/lib/legion/extensions/llm/canonical/thinking.rb +81 -0
- data/lib/legion/extensions/llm/canonical/tool_call.rb +134 -0
- data/lib/legion/extensions/llm/canonical/tool_definition.rb +98 -0
- data/lib/legion/extensions/llm/canonical/tool_schema.rb +46 -0
- data/lib/legion/extensions/llm/canonical/usage.rb +74 -0
- data/lib/legion/extensions/llm/canonical.rb +50 -0
- data/lib/legion/extensions/llm/chat.rb +3 -5
- data/lib/legion/extensions/llm/connection.rb +5 -1
- data/lib/legion/extensions/llm/error.rb +5 -7
- data/lib/legion/extensions/llm/fleet/envelope_validation.rb +1 -3
- data/lib/legion/extensions/llm/fleet/provider_responder.rb +1 -3
- data/lib/legion/extensions/llm/fleet/token_validator.rb +1 -3
- data/lib/legion/extensions/llm/model/info.rb +4 -6
- data/lib/legion/extensions/llm/models.rb +3 -3
- data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +9 -4
- data/lib/legion/extensions/llm/provider.rb +21 -4
- data/lib/legion/extensions/llm/provider_contract.rb +10 -1
- data/lib/legion/extensions/llm/routing/lane_key.rb +1 -3
- data/lib/legion/extensions/llm/stream_accumulator.rb +40 -1
- data/lib/legion/extensions/llm/streaming.rb +13 -5
- data/lib/legion/extensions/llm/tool.rb +1 -3
- data/lib/legion/extensions/llm/version.rb +1 -1
- data/lib/legion/extensions/llm.rb +118 -35
- data/spec/fixtures/ruby.mp3 +0 -0
- data/spec/fixtures/ruby.mp4 +0 -0
- data/spec/fixtures/ruby.png +0 -0
- data/spec/fixtures/ruby.txt +1 -0
- data/spec/fixtures/ruby.wav +0 -0
- data/spec/fixtures/ruby.xml +1 -0
- data/spec/fixtures/sample.pdf +0 -0
- data/spec/legion/extensions/llm/agent_spec.rb +179 -0
- data/spec/legion/extensions/llm/attachment_spec.rb +25 -0
- data/spec/legion/extensions/llm/auto_registration_spec.rb +38 -0
- data/spec/legion/extensions/llm/canonical/chunk_spec.rb +285 -0
- data/spec/legion/extensions/llm/canonical/content_block_spec.rb +179 -0
- data/spec/legion/extensions/llm/canonical/message_spec.rb +203 -0
- data/spec/legion/extensions/llm/canonical/params_spec.rb +159 -0
- data/spec/legion/extensions/llm/canonical/request_spec.rb +174 -0
- data/spec/legion/extensions/llm/canonical/response_spec.rb +234 -0
- data/spec/legion/extensions/llm/canonical/thinking_spec.rb +151 -0
- data/spec/legion/extensions/llm/canonical/tool_call_spec.rb +191 -0
- data/spec/legion/extensions/llm/canonical/tool_definition_spec.rb +221 -0
- data/spec/legion/extensions/llm/canonical/tool_schema_spec.rb +83 -0
- data/spec/legion/extensions/llm/canonical/usage_spec.rb +178 -0
- data/spec/legion/extensions/llm/configuration_spec.rb +38 -0
- data/spec/legion/extensions/llm/conformance/client_translator_examples.rb +432 -0
- data/spec/legion/extensions/llm/conformance/conformance.rb +51 -0
- data/spec/legion/extensions/llm/conformance/echo_translator.rb +56 -0
- data/spec/legion/extensions/llm/conformance/echo_translator_spec.rb +13 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_empty_response.json +13 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_error_response.json +19 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_fleet_round_trip.json +81 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_metering_audit_events.json +101 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_params_mapping_request.json +21 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_server_tool_continuation_request.json +43 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_server_tool_use_response.json +29 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_simple_text_request.json +13 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_simple_text_response.json +13 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_stop_reason_matrix.json +36 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_accumulated_response.json +20 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_error_chunks.json +26 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_server_tool_chunks.json +52 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_text_chunks.json +33 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_thinking_chunks.json +42 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_tool_call_chunks.json +41 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_system_prompt_request.json +14 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_thinking_request.json +18 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_thinking_response.json +17 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_tool_results_continuation_request.json +75 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_tool_use_response.json +25 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_tools_request.json +34 -0
- data/spec/legion/extensions/llm/conformance/provider_tool_rendering_examples.rb +77 -0
- data/spec/legion/extensions/llm/conformance/provider_translator_examples.rb +390 -0
- data/spec/legion/extensions/llm/connection_logging_spec.rb +53 -0
- data/spec/legion/extensions/llm/connection_retry_spec.rb +36 -0
- data/spec/legion/extensions/llm/context_spec.rb +127 -0
- data/spec/legion/extensions/llm/credential_sources_spec.rb +468 -0
- data/spec/legion/extensions/llm/error_middleware_spec.rb +102 -0
- data/spec/legion/extensions/llm/error_spec.rb +87 -0
- data/spec/legion/extensions/llm/fleet/provider_responder_spec.rb +120 -0
- data/spec/legion/extensions/llm/fleet/token_validator_spec.rb +163 -0
- data/spec/legion/extensions/llm/fleet/worker_execution_spec.rb +128 -0
- data/spec/legion/extensions/llm/fleet_messages_spec.rb +402 -0
- data/spec/legion/extensions/llm/gemspec_spec.rb +25 -0
- data/spec/legion/extensions/llm/message_spec.rb +64 -0
- data/spec/legion/extensions/llm/model/info_spec.rb +222 -0
- data/spec/legion/extensions/llm/models_spec.rb +104 -0
- data/spec/legion/extensions/llm/provider/open_ai_compatible_spec.rb +203 -0
- data/spec/legion/extensions/llm/provider/open_ai_compatible_tool_calls_array_spec.rb +68 -0
- data/spec/legion/extensions/llm/provider_contract_spec.rb +60 -0
- data/spec/legion/extensions/llm/provider_settings_spec.rb +76 -0
- data/spec/legion/extensions/llm/provider_spec.rb +613 -0
- data/spec/legion/extensions/llm/registry_event_builder_spec.rb +68 -0
- data/spec/legion/extensions/llm/registry_publisher_spec.rb +22 -0
- data/spec/legion/extensions/llm/responses/response_objects_spec.rb +75 -0
- data/spec/legion/extensions/llm/responses/thinking_extractor_spec.rb +75 -0
- data/spec/legion/extensions/llm/routing/model_offering_spec.rb +222 -0
- data/spec/legion/extensions/llm/routing/offering_registry_spec.rb +50 -0
- data/spec/legion/extensions/llm/routing/registry_event_spec.rb +120 -0
- data/spec/legion/extensions/llm/stream_accumulator_spec.rb +155 -0
- data/spec/legion/extensions/llm/streaming_spec.rb +108 -0
- data/spec/legion/extensions/llm/tool_spec.rb +94 -0
- data/spec/legion/extensions/llm/transport/fleet_lane_spec.rb +60 -0
- data/spec/legion/extensions/llm/utils_spec.rb +113 -0
- data/spec/legion/extensions/llm_base_contract_spec.rb +110 -0
- data/spec/legion/extensions/llm_extension_spec.rb +78 -0
- data/spec/legion/extensions/llm_root_spec.rb +51 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/support/fake_llm_provider.rb +148 -0
- data/spec/support/llm_configuration.rb +21 -0
- data/spec/support/rspec_configuration.rb +19 -0
- data/spec/support/simplecov_configuration.rb +20 -0
- metadata +103 -15
|
@@ -23,29 +23,125 @@ require 'marcel'
|
|
|
23
23
|
require 'ruby_llm/schema'
|
|
24
24
|
require 'securerandom'
|
|
25
25
|
require 'time'
|
|
26
|
-
require 'zeitwerk'
|
|
27
26
|
require_relative 'llm/version'
|
|
28
27
|
|
|
29
28
|
module Legion
|
|
30
29
|
module Extensions
|
|
31
30
|
# Legion-native namespace for the shared LLM provider framework.
|
|
32
31
|
module Llm
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
32
|
+
# ------------------------------------------------------------------ #
|
|
33
|
+
# Explicit requires (replaces Zeitwerk autoloading). #
|
|
34
|
+
# Load order: base classes & canonical types first, then anything #
|
|
35
|
+
# that references them. All live under Legion::Extensions::Llm so #
|
|
36
|
+
# unqualified constant lookups resolve via Ruby scope. #
|
|
37
|
+
# ------------------------------------------------------------------ #
|
|
38
|
+
|
|
39
|
+
# --- Base value objects (no internal deps) ---
|
|
40
|
+
require_relative 'llm/mime_type'
|
|
41
|
+
require_relative 'llm/model/info'
|
|
42
|
+
require_relative 'llm/model/modalities'
|
|
43
|
+
require_relative 'llm/model/pricing_category'
|
|
44
|
+
require_relative 'llm/model/pricing_tier'
|
|
45
|
+
require_relative 'llm/model/pricing'
|
|
46
|
+
require_relative 'llm/configuration'
|
|
47
|
+
require_relative 'llm/thinking'
|
|
48
|
+
require_relative 'llm/tokens'
|
|
49
|
+
require_relative 'llm/message'
|
|
50
|
+
require_relative 'llm/tool_call'
|
|
51
|
+
require_relative 'llm/content'
|
|
52
|
+
require_relative 'llm/errors/unsupported_capability'
|
|
53
|
+
require_relative 'llm/error'
|
|
54
|
+
|
|
55
|
+
# --- Build on message/base types ---
|
|
56
|
+
require_relative 'llm/chunk'
|
|
57
|
+
require_relative 'llm/model'
|
|
58
|
+
require_relative 'llm/attachment'
|
|
59
|
+
|
|
60
|
+
# --- Streaming fundamentals (must load before streaming/provider) ---
|
|
61
|
+
require_relative 'llm/stream_accumulator'
|
|
62
|
+
require_relative 'llm/responses/stream_chunk'
|
|
63
|
+
require_relative 'llm/streaming'
|
|
64
|
+
|
|
65
|
+
# --- Context, Connection ---
|
|
66
|
+
require_relative 'llm/context'
|
|
67
|
+
require_relative 'llm/connection'
|
|
68
|
+
|
|
69
|
+
# --- Response normalizers ---
|
|
70
|
+
require_relative 'llm/responses/chat_response'
|
|
71
|
+
require_relative 'llm/responses/embedding_response'
|
|
72
|
+
require_relative 'llm/responses/thinking_extractor'
|
|
73
|
+
|
|
74
|
+
# --- Provider base & allied modules ---
|
|
75
|
+
require_relative 'llm/provider_contract'
|
|
76
|
+
require_relative 'llm/provider_settings'
|
|
77
|
+
require_relative 'llm/provider'
|
|
78
|
+
|
|
79
|
+
# --- Provider subtypes ---
|
|
80
|
+
require_relative 'llm/provider/open_ai_compatible'
|
|
81
|
+
|
|
82
|
+
# --- Routing ---
|
|
83
|
+
require_relative 'llm/routing'
|
|
84
|
+
require_relative 'llm/routing/lane_key'
|
|
85
|
+
require_relative 'llm/routing/offering_registry'
|
|
86
|
+
require_relative 'llm/routing/registry_event'
|
|
87
|
+
require_relative 'llm/routing/model_offering'
|
|
88
|
+
|
|
89
|
+
# --- Models (scans for Provider subclasses) ---
|
|
90
|
+
require_relative 'llm/models'
|
|
91
|
+
|
|
92
|
+
# --- Agent & Chat (reference Provider, Context, Chat at method-time) ---
|
|
93
|
+
require_relative 'llm/agent'
|
|
94
|
+
require_relative 'llm/chat'
|
|
95
|
+
|
|
96
|
+
# --- Domain services ---
|
|
97
|
+
require_relative 'llm/embedding'
|
|
98
|
+
require_relative 'llm/moderation'
|
|
99
|
+
require_relative 'llm/image'
|
|
100
|
+
require_relative 'llm/transcription'
|
|
101
|
+
|
|
102
|
+
# --- Registry & misc support ---
|
|
103
|
+
require_relative 'llm/registry_event_builder'
|
|
104
|
+
require_relative 'llm/registry_publisher'
|
|
105
|
+
require_relative 'llm/auto_registration'
|
|
106
|
+
require_relative 'llm/credential_sources'
|
|
107
|
+
require_relative 'llm/tool'
|
|
108
|
+
require_relative 'llm/utils'
|
|
109
|
+
require_relative 'llm/aliases'
|
|
110
|
+
|
|
111
|
+
# --- Fleet protocol (depends on Provider, Models) ---
|
|
112
|
+
require_relative 'llm/fleet/protocol'
|
|
113
|
+
require_relative 'llm/fleet/settings'
|
|
114
|
+
require_relative 'llm/fleet/token_error'
|
|
115
|
+
require_relative 'llm/fleet/envelope_validation'
|
|
116
|
+
require_relative 'llm/fleet/publish_safety'
|
|
117
|
+
require_relative 'llm/fleet/default_exchange_reply'
|
|
118
|
+
require_relative 'llm/fleet/token_validator'
|
|
119
|
+
require_relative 'llm/fleet/worker_execution'
|
|
120
|
+
require_relative 'llm/fleet/provider_responder'
|
|
121
|
+
|
|
122
|
+
# --- Transport lane (references Fleet exchange/message autoloads) ---
|
|
123
|
+
require_relative 'llm/transport/fleet_lane'
|
|
124
|
+
|
|
125
|
+
# --- Canonical types — explicit self-contained loader ---
|
|
126
|
+
require_relative 'llm/canonical'
|
|
127
|
+
|
|
128
|
+
# --- Transport modules (lazy — depend on optional legion-transport) ---
|
|
129
|
+
# These remain as autoload so boot-time does not force legion-transport.
|
|
130
|
+
module Transport
|
|
131
|
+
# Shared AMQP exchange definitions for fleet routing.
|
|
132
|
+
# Lazy-loaded; only instantiated when legion-transport is available.
|
|
133
|
+
module Exchanges
|
|
134
|
+
autoload :Fleet, File.expand_path('llm/transport/exchanges/fleet', __dir__)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Shared AMQP message envelopes for fleet request/response cycles.
|
|
138
|
+
# Lazy-loaded; only instantiated when legion-transport is available.
|
|
139
|
+
module Messages
|
|
140
|
+
autoload :FleetRequest, File.expand_path('llm/transport/messages/fleet_request', __dir__)
|
|
141
|
+
autoload :FleetResponse, File.expand_path('llm/transport/messages/fleet_response', __dir__)
|
|
142
|
+
autoload :FleetError, File.expand_path('llm/transport/messages/fleet_error', __dir__)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
49
145
|
|
|
50
146
|
Schema = ::RubyLLM::Schema unless const_defined?(:Schema, false)
|
|
51
147
|
|
|
@@ -148,6 +244,11 @@ module Legion
|
|
|
148
244
|
require_policy: false,
|
|
149
245
|
require_idempotency: true,
|
|
150
246
|
idempotency_ttl_seconds: 600
|
|
247
|
+
},
|
|
248
|
+
request: {
|
|
249
|
+
logger: {
|
|
250
|
+
request_payload: false
|
|
251
|
+
}
|
|
151
252
|
}
|
|
152
253
|
}
|
|
153
254
|
}
|
|
@@ -156,24 +257,6 @@ module Legion
|
|
|
156
257
|
def self.provider_settings(...)
|
|
157
258
|
ProviderSettings.build(...)
|
|
158
259
|
end
|
|
159
|
-
|
|
160
|
-
require_relative 'llm/auto_registration'
|
|
161
|
-
require_relative 'llm/credential_sources'
|
|
162
|
-
loader.eager_load
|
|
163
|
-
|
|
164
|
-
module Transport
|
|
165
|
-
# Local autoloads for fleet exchange classes that depend on legion-transport.
|
|
166
|
-
module Exchanges
|
|
167
|
-
autoload :Fleet, File.expand_path('llm/transport/exchanges/fleet', __dir__)
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
# Local autoloads for fleet message classes that depend on legion-transport.
|
|
171
|
-
module Messages
|
|
172
|
-
autoload :FleetRequest, File.expand_path('llm/transport/messages/fleet_request', __dir__)
|
|
173
|
-
autoload :FleetResponse, File.expand_path('llm/transport/messages/fleet_response', __dir__)
|
|
174
|
-
autoload :FleetError, File.expand_path('llm/transport/messages/fleet_error', __dir__)
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
260
|
end
|
|
178
261
|
end
|
|
179
262
|
end
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Ruby is the best.
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<truism>Ruby is the best</truism>
|
|
Binary file
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Llm::Agent do
|
|
6
|
+
include_context 'with configured Legion::Extensions::Llm'
|
|
7
|
+
include_context 'with fake llm provider'
|
|
8
|
+
|
|
9
|
+
it 'builds a configured plain chat via .chat with runtime inputs' do
|
|
10
|
+
tool_class = Class.new(Legion::Extensions::Llm::Tool) do
|
|
11
|
+
def name = 'echo_tool'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
agent_class = Class.new(Legion::Extensions::Llm::Agent) do
|
|
15
|
+
model 'fake-chat-model', provider: :fake_llm, assume_model_exists: true
|
|
16
|
+
inputs :display_name
|
|
17
|
+
instructions { "Hello #{display_name}" }
|
|
18
|
+
tools { [tool_class.new] }
|
|
19
|
+
params { { max_tokens: 12 } }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
chat = agent_class.chat(display_name: 'Ava')
|
|
23
|
+
|
|
24
|
+
expect(chat.messages.first.role).to eq(:system)
|
|
25
|
+
expect(chat.messages.first.content).to eq('Hello Ava')
|
|
26
|
+
expect(chat.tools.keys).to include(:echo_tool)
|
|
27
|
+
expect(chat.params).to eq(max_tokens: 12)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'exposes Legion::Extensions::Llm::Chat as chat in execution context for .chat' do
|
|
31
|
+
agent_class = Class.new(Legion::Extensions::Llm::Agent) do
|
|
32
|
+
model 'fake-chat-model', provider: :fake_llm, assume_model_exists: true
|
|
33
|
+
instructions { chat.class.name }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
chat = agent_class.chat
|
|
37
|
+
expect(chat.messages.first.content).to eq('Legion::Extensions::Llm::Chat')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'raises when instructions default prompt is missing' do
|
|
41
|
+
agent_class = Class.new(Legion::Extensions::Llm::Agent) do
|
|
42
|
+
model 'fake-chat-model', provider: :fake_llm, assume_model_exists: true
|
|
43
|
+
instructions
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
expect { agent_class.chat }.to raise_error(Legion::Extensions::Llm::PromptNotFoundError, /Prompt file not found/)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'supports inline schema DSL via schema do ... end' do
|
|
50
|
+
agent_class = Class.new(Legion::Extensions::Llm::Agent) do
|
|
51
|
+
model 'fake-chat-model', provider: :fake_llm, assume_model_exists: true
|
|
52
|
+
schema do
|
|
53
|
+
string :verdict, enum: %w[pass revise]
|
|
54
|
+
string :feedback
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
chat = agent_class.chat
|
|
59
|
+
|
|
60
|
+
expect(chat.schema).to include(name: 'Schema', strict: true, schema: include(type: 'object'))
|
|
61
|
+
expect(chat.schema.dig(:schema, :properties)).to include(
|
|
62
|
+
verdict: include(type: 'string'),
|
|
63
|
+
feedback: include(type: 'string')
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'supports runtime-evaluated schema blocks that return a schema value' do
|
|
68
|
+
agent_class = Class.new(Legion::Extensions::Llm::Agent) do
|
|
69
|
+
model 'fake-chat-model', provider: :fake_llm, assume_model_exists: true
|
|
70
|
+
inputs :strict
|
|
71
|
+
|
|
72
|
+
schema do
|
|
73
|
+
if strict
|
|
74
|
+
{
|
|
75
|
+
type: 'object',
|
|
76
|
+
properties: { answer: { type: 'string' } },
|
|
77
|
+
required: ['answer'],
|
|
78
|
+
additionalProperties: false
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
strict_chat = agent_class.chat(strict: true)
|
|
85
|
+
loose_chat = agent_class.chat(strict: false)
|
|
86
|
+
|
|
87
|
+
expect(strict_chat.schema).to include(name: 'response', strict: true, schema: include(type: 'object'))
|
|
88
|
+
expect(loose_chat.schema).to be_nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'can ask using a registered provider' do
|
|
92
|
+
agent_class = Class.new(Legion::Extensions::Llm::Agent) do
|
|
93
|
+
model 'fake-chat-model', provider: :fake_llm, assume_model_exists: true
|
|
94
|
+
instructions 'Answer questions clearly.'
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
stub_const('SpecChatAgent', agent_class)
|
|
98
|
+
|
|
99
|
+
response = SpecChatAgent.new.ask('hello')
|
|
100
|
+
expect(response.content).to include('fake response to hello')
|
|
101
|
+
expect(response.role).to eq(:assistant)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'delegates add_message to the underlying chat interface' do
|
|
105
|
+
agent_class = Class.new(Legion::Extensions::Llm::Agent) do
|
|
106
|
+
model 'fake-chat-model', provider: :fake_llm, assume_model_exists: true
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
agent = agent_class.new
|
|
110
|
+
message = agent.add_message(role: :user, content: 'Hello')
|
|
111
|
+
|
|
112
|
+
expect(message.role).to eq(:user)
|
|
113
|
+
expect(message.content).to eq('Hello')
|
|
114
|
+
expect(agent.chat.messages.last).to eq(message)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'exposes messages like Legion::Extensions::Llm::Chat' do
|
|
118
|
+
agent_class = Class.new(Legion::Extensions::Llm::Agent) do
|
|
119
|
+
model 'fake-chat-model', provider: :fake_llm, assume_model_exists: true
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
agent = agent_class.new
|
|
123
|
+
agent.add_message(role: :user, content: 'First')
|
|
124
|
+
|
|
125
|
+
expect(agent.messages).to eq(agent.chat.messages)
|
|
126
|
+
expect(agent.messages.last.content).to eq('First')
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'delegates callback hooks to the underlying chat' do
|
|
130
|
+
fake_chat = Class.new do
|
|
131
|
+
attr_reader :events
|
|
132
|
+
|
|
133
|
+
def initialize
|
|
134
|
+
@events = []
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def on_new_message(&)
|
|
138
|
+
@events << :new_message
|
|
139
|
+
self
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def on_end_message(&)
|
|
143
|
+
@events << :end_message
|
|
144
|
+
self
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def on_tool_call(&)
|
|
148
|
+
@events << :tool_call
|
|
149
|
+
self
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def on_tool_result(&)
|
|
153
|
+
@events << :tool_result
|
|
154
|
+
self
|
|
155
|
+
end
|
|
156
|
+
end.new
|
|
157
|
+
|
|
158
|
+
agent = Class.new(described_class).new(chat: fake_chat)
|
|
159
|
+
|
|
160
|
+
expect(agent.on_new_message { :ok }).to eq(fake_chat)
|
|
161
|
+
expect(agent.on_end_message { :ok }).to eq(fake_chat)
|
|
162
|
+
expect(agent.on_tool_call { :ok }).to eq(fake_chat)
|
|
163
|
+
expect(agent.on_tool_result { :ok }).to eq(fake_chat)
|
|
164
|
+
expect(fake_chat.events).to eq(%i[new_message end_message tool_call tool_result])
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it 'supports Enumerable by delegating each to chat' do
|
|
168
|
+
fake_chat = Class.new do
|
|
169
|
+
def each(&)
|
|
170
|
+
return enum_for(:each) unless block_given?
|
|
171
|
+
|
|
172
|
+
%w[first second].each(&)
|
|
173
|
+
end
|
|
174
|
+
end.new
|
|
175
|
+
|
|
176
|
+
agent = Class.new(described_class).new(chat: fake_chat)
|
|
177
|
+
expect(agent.map(&:upcase)).to eq(%w[FIRST SECOND])
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'open3'
|
|
5
|
+
require 'rbconfig'
|
|
6
|
+
|
|
7
|
+
RSpec.describe Legion::Extensions::Llm::Attachment do
|
|
8
|
+
it 'supports path attachments from the public API' do
|
|
9
|
+
script = <<~'RUBY'
|
|
10
|
+
require 'legion/extensions/llm'
|
|
11
|
+
|
|
12
|
+
content = Legion::Extensions::Llm::Content.new('What is in this file?', 'spec/fixtures/ruby.txt')
|
|
13
|
+
attachment = content.attachments.first
|
|
14
|
+
puts "#{attachment.filename},#{attachment.mime_type}"
|
|
15
|
+
RUBY
|
|
16
|
+
|
|
17
|
+
stdout, stderr, status = Open3.capture3(
|
|
18
|
+
RbConfig.ruby, '-Ilib', '-e', script,
|
|
19
|
+
chdir: File.expand_path('../../../..', __dir__)
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
expect(status.success?).to be(true), stderr
|
|
23
|
+
expect(stdout.strip).to eq('ruby.txt,text/plain')
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Llm::AutoRegistration do
|
|
6
|
+
# Build a fake provider module that extends AutoRegistration,
|
|
7
|
+
# mimicking what a real lex-llm-* provider would look like.
|
|
8
|
+
let(:fake_provider_class) { Class.new }
|
|
9
|
+
|
|
10
|
+
let(:provider_module) do
|
|
11
|
+
klass = fake_provider_class
|
|
12
|
+
mod = Module.new do
|
|
13
|
+
extend Legion::Extensions::Llm::AutoRegistration
|
|
14
|
+
|
|
15
|
+
const_set(:PROVIDER_FAMILY, :fake_provider)
|
|
16
|
+
|
|
17
|
+
define_singleton_method(:provider_class) { klass }
|
|
18
|
+
end
|
|
19
|
+
mod
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe '#discover_instances' do
|
|
23
|
+
it 'returns an empty hash by default' do
|
|
24
|
+
expect(provider_module.discover_instances).to eq({})
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe '#provider_aliases' do
|
|
29
|
+
it 'returns an empty alias list by default' do
|
|
30
|
+
expect(provider_module.provider_aliases).to eq([])
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'does not expose legion-llm registry mutation hooks' do
|
|
35
|
+
expect(provider_module).not_to respond_to(:register_discovered_instances)
|
|
36
|
+
expect(provider_module).not_to respond_to(:rediscover!)
|
|
37
|
+
end
|
|
38
|
+
end
|