lex-llm 0.1.1 → 0.1.3
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/.gitignore +1 -0
- data/CHANGELOG.md +11 -1
- data/Gemfile +1 -19
- data/README.md +22 -25
- data/lex-llm.gemspec +2 -2
- data/lib/legion/extensions/llm/agent.rb +366 -0
- data/lib/legion/extensions/llm/aliases.rb +42 -0
- data/lib/legion/extensions/llm/attachment.rb +229 -0
- data/lib/legion/extensions/llm/chat.rb +355 -0
- data/lib/legion/extensions/llm/chunk.rb +10 -0
- data/lib/legion/extensions/llm/configuration.rb +82 -0
- data/lib/legion/extensions/llm/connection.rb +134 -0
- data/lib/legion/extensions/llm/content.rb +81 -0
- data/lib/legion/extensions/llm/context.rb +33 -0
- data/lib/legion/extensions/llm/embedding.rb +33 -0
- data/lib/legion/extensions/llm/error.rb +116 -0
- data/lib/legion/extensions/llm/image.rb +109 -0
- data/lib/legion/extensions/llm/message.rb +111 -0
- data/lib/legion/extensions/llm/mime_type.rb +75 -0
- data/lib/legion/extensions/llm/model/info.rb +117 -0
- data/lib/legion/extensions/llm/model/modalities.rb +26 -0
- data/lib/legion/extensions/llm/model/pricing.rb +52 -0
- data/lib/legion/extensions/llm/model/pricing_category.rb +50 -0
- data/lib/legion/extensions/llm/model/pricing_tier.rb +37 -0
- data/lib/legion/extensions/llm/model.rb +11 -0
- data/lib/legion/extensions/llm/models.rb +514 -0
- data/lib/{lex_llm → legion/extensions/llm}/models_schema.json +1 -1
- data/lib/legion/extensions/llm/moderation.rb +60 -0
- data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +240 -0
- data/lib/legion/extensions/llm/provider.rb +282 -0
- data/lib/legion/extensions/llm/routing/lane_key.rb +57 -0
- data/lib/legion/extensions/llm/routing/model_offering.rb +173 -0
- data/lib/legion/extensions/llm/routing.rb +11 -0
- data/lib/legion/extensions/llm/stream_accumulator.rb +209 -0
- data/lib/legion/extensions/llm/streaming.rb +181 -0
- data/lib/legion/extensions/llm/thinking.rb +53 -0
- data/lib/legion/extensions/llm/tokens.rb +51 -0
- data/lib/legion/extensions/llm/tool.rb +258 -0
- data/lib/legion/extensions/llm/tool_call.rb +29 -0
- data/lib/legion/extensions/llm/transcription.rb +39 -0
- data/lib/legion/extensions/llm/utils.rb +95 -0
- data/lib/legion/extensions/llm/version.rb +9 -0
- data/lib/legion/extensions/llm.rb +85 -6
- metadata +40 -121
- data/lib/generators/lex_llm/agent/agent_generator.rb +0 -36
- data/lib/generators/lex_llm/agent/templates/agent.rb.tt +0 -6
- data/lib/generators/lex_llm/agent/templates/instructions.txt.erb.tt +0 -0
- data/lib/generators/lex_llm/chat_ui/chat_ui_generator.rb +0 -256
- data/lib/generators/lex_llm/chat_ui/templates/controllers/chats_controller.rb.tt +0 -38
- data/lib/generators/lex_llm/chat_ui/templates/controllers/messages_controller.rb.tt +0 -21
- data/lib/generators/lex_llm/chat_ui/templates/controllers/models_controller.rb.tt +0 -14
- data/lib/generators/lex_llm/chat_ui/templates/helpers/messages_helper.rb.tt +0 -25
- data/lib/generators/lex_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +0 -12
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +0 -16
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +0 -31
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +0 -31
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/new.html.erb.tt +0 -9
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +0 -27
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +0 -14
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_content.html.erb.tt +0 -1
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +0 -13
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +0 -23
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +0 -10
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +0 -2
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +0 -4
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +0 -14
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +0 -13
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +0 -21
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +0 -17
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +0 -40
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +0 -27
- data/lib/generators/lex_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +0 -16
- data/lib/generators/lex_llm/chat_ui/templates/views/chats/_form.html.erb.tt +0 -29
- data/lib/generators/lex_llm/chat_ui/templates/views/chats/index.html.erb.tt +0 -28
- data/lib/generators/lex_llm/chat_ui/templates/views/chats/new.html.erb.tt +0 -11
- data/lib/generators/lex_llm/chat_ui/templates/views/chats/show.html.erb.tt +0 -25
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +0 -9
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_content.html.erb.tt +0 -1
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_error.html.erb.tt +0 -8
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_form.html.erb.tt +0 -21
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_system.html.erb.tt +0 -6
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +0 -2
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +0 -4
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_user.html.erb.tt +0 -9
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +0 -7
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +0 -8
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +0 -16
- data/lib/generators/lex_llm/chat_ui/templates/views/models/_model.html.erb.tt +0 -15
- data/lib/generators/lex_llm/chat_ui/templates/views/models/index.html.erb.tt +0 -38
- data/lib/generators/lex_llm/chat_ui/templates/views/models/show.html.erb.tt +0 -17
- data/lib/generators/lex_llm/generator_helpers.rb +0 -214
- data/lib/generators/lex_llm/install/install_generator.rb +0 -109
- data/lib/generators/lex_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
- data/lib/generators/lex_llm/install/templates/chat_model.rb.tt +0 -3
- data/lib/generators/lex_llm/install/templates/create_chats_migration.rb.tt +0 -7
- data/lib/generators/lex_llm/install/templates/create_messages_migration.rb.tt +0 -19
- data/lib/generators/lex_llm/install/templates/create_models_migration.rb.tt +0 -39
- data/lib/generators/lex_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
- data/lib/generators/lex_llm/install/templates/initializer.rb.tt +0 -20
- data/lib/generators/lex_llm/install/templates/message_model.rb.tt +0 -4
- data/lib/generators/lex_llm/install/templates/model_model.rb.tt +0 -3
- data/lib/generators/lex_llm/install/templates/tool_call_model.rb.tt +0 -3
- data/lib/generators/lex_llm/schema/schema_generator.rb +0 -26
- data/lib/generators/lex_llm/schema/templates/schema.rb.tt +0 -2
- data/lib/generators/lex_llm/tool/templates/tool.rb.tt +0 -9
- data/lib/generators/lex_llm/tool/templates/tool_call.html.erb.tt +0 -13
- data/lib/generators/lex_llm/tool/templates/tool_result.html.erb.tt +0 -13
- data/lib/generators/lex_llm/tool/tool_generator.rb +0 -96
- data/lib/generators/lex_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
- data/lib/generators/lex_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
- data/lib/generators/lex_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
- data/lib/generators/lex_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
- data/lib/generators/lex_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
- data/lib/generators/lex_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
- data/lib/generators/lex_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
- data/lib/generators/lex_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
- data/lib/lex_llm/active_record/acts_as.rb +0 -180
- data/lib/lex_llm/active_record/acts_as_legacy.rb +0 -503
- data/lib/lex_llm/active_record/chat_methods.rb +0 -468
- data/lib/lex_llm/active_record/message_methods.rb +0 -131
- data/lib/lex_llm/active_record/model_methods.rb +0 -76
- data/lib/lex_llm/active_record/payload_helpers.rb +0 -26
- data/lib/lex_llm/active_record/tool_call_methods.rb +0 -15
- data/lib/lex_llm/agent.rb +0 -365
- data/lib/lex_llm/aliases.rb +0 -38
- data/lib/lex_llm/attachment.rb +0 -223
- data/lib/lex_llm/chat.rb +0 -351
- data/lib/lex_llm/chunk.rb +0 -6
- data/lib/lex_llm/configuration.rb +0 -81
- data/lib/lex_llm/connection.rb +0 -130
- data/lib/lex_llm/content.rb +0 -77
- data/lib/lex_llm/context.rb +0 -29
- data/lib/lex_llm/embedding.rb +0 -29
- data/lib/lex_llm/error.rb +0 -112
- data/lib/lex_llm/image.rb +0 -105
- data/lib/lex_llm/message.rb +0 -107
- data/lib/lex_llm/mime_type.rb +0 -71
- data/lib/lex_llm/model/info.rb +0 -113
- data/lib/lex_llm/model/modalities.rb +0 -22
- data/lib/lex_llm/model/pricing.rb +0 -48
- data/lib/lex_llm/model/pricing_category.rb +0 -46
- data/lib/lex_llm/model/pricing_tier.rb +0 -33
- data/lib/lex_llm/model.rb +0 -7
- data/lib/lex_llm/models.rb +0 -506
- data/lib/lex_llm/moderation.rb +0 -56
- data/lib/lex_llm/provider.rb +0 -278
- data/lib/lex_llm/railtie.rb +0 -35
- data/lib/lex_llm/routing/lane_key.rb +0 -51
- data/lib/lex_llm/routing/model_offering.rb +0 -169
- data/lib/lex_llm/routing.rb +0 -7
- data/lib/lex_llm/stream_accumulator.rb +0 -203
- data/lib/lex_llm/streaming.rb +0 -175
- data/lib/lex_llm/thinking.rb +0 -49
- data/lib/lex_llm/tokens.rb +0 -47
- data/lib/lex_llm/tool.rb +0 -254
- data/lib/lex_llm/tool_call.rb +0 -25
- data/lib/lex_llm/transcription.rb +0 -35
- data/lib/lex_llm/utils.rb +0 -91
- data/lib/lex_llm/version.rb +0 -5
- data/lib/lex_llm.rb +0 -95
- data/lib/tasks/lex_llm.rake +0 -23
- /data/lib/{lex_llm → legion/extensions/llm}/aliases.json +0 -0
- /data/lib/{lex_llm → legion/extensions/llm}/models.json +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 375f971150ba508862d136d724dd61f99ffb49bf3076d6a5debe0e8e12dfe86b
|
|
4
|
+
data.tar.gz: '092859a51545b6408d0b9342065fcabf32d77184fd7ebd9b2e5739e415c7a43f'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fead7c175af6e409b349ac8c6654d2c8ddbc8ed66ac2a158483b2bdd4f78898881e4e5b6aa15728515dcfa46d8ce0c601a8c81d99eeabb87f681f93828e3ce31
|
|
7
|
+
data.tar.gz: 69df8e7c7b0b09917d23b0de90d518dc9132dc9221a1ff9eef5dd1b30dc585beb0c1682d930a57335b29a9424a0b21f17b8994745e39515c1331dc9ff9a198ce
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.3 - 2026-04-27
|
|
4
|
+
|
|
5
|
+
- Convert the gem to a standard Legion extension runtime under `Legion::Extensions::Llm`.
|
|
6
|
+
- Remove the fork-era compatibility namespace, Rails railtie, generators, rake tasks, dummy app, and ActiveRecord helpers.
|
|
7
|
+
- Move provider-neutral chat, schema, model, routing, streaming, and fleet primitives under `lib/legion/extensions/llm`.
|
|
8
|
+
|
|
9
|
+
## 0.1.2 - 2026-04-27
|
|
10
|
+
|
|
11
|
+
- Add a shared OpenAI-compatible provider adapter for `lex-llm-openai`, `lex-llm-vllm`, `lex-llm-mlx`, and other compatible servers.
|
|
12
|
+
|
|
3
13
|
## 0.1.1 - 2026-04-27
|
|
4
14
|
|
|
5
15
|
- Remove fork-carried concrete provider implementations and VCR-backed provider specs from the base gem.
|
|
@@ -9,7 +19,7 @@
|
|
|
9
19
|
|
|
10
20
|
## 0.1.0 - 2026-04-26
|
|
11
21
|
|
|
12
|
-
- Rename the forked base gem to `lex-llm` with
|
|
22
|
+
- Rename the forked base gem to `lex-llm` with Legion extension integration.
|
|
13
23
|
- Add provider-neutral routing metadata for concrete model offerings and shared fleet lane keys.
|
|
14
24
|
- Use Legion JSON/settings/logging runtime dependencies for shared extension behavior.
|
|
15
25
|
- Remove the upstream RubyLLM docs site and issue templates from the LegionIO fork.
|
data/Gemfile
CHANGED
|
@@ -4,21 +4,18 @@ source 'https://rubygems.org'
|
|
|
4
4
|
|
|
5
5
|
gemspec
|
|
6
6
|
|
|
7
|
-
group :development do
|
|
7
|
+
group :development do
|
|
8
8
|
gem 'appraisal'
|
|
9
9
|
gem 'async', platform: :mri
|
|
10
10
|
gem 'bundler', '>= 2.0'
|
|
11
11
|
gem 'colorize'
|
|
12
12
|
gem 'dotenv'
|
|
13
|
-
gem 'ferrum'
|
|
14
13
|
gem 'flay'
|
|
15
|
-
gem 'image_processing', '~> 1.2'
|
|
16
14
|
gem 'irb'
|
|
17
15
|
gem 'json-schema'
|
|
18
16
|
gem 'nokogiri'
|
|
19
17
|
gem 'overcommit', '>= 0.66'
|
|
20
18
|
gem 'pry', '>= 0.14'
|
|
21
|
-
gem 'rails'
|
|
22
19
|
gem 'rake', '>= 13.0'
|
|
23
20
|
gem 'reline'
|
|
24
21
|
gem 'rspec', '~> 3.12'
|
|
@@ -30,21 +27,6 @@ group :development do # rubocop:disable Metrics/BlockLength
|
|
|
30
27
|
gem 'simplecov-cobertura'
|
|
31
28
|
gem 'test-queue'
|
|
32
29
|
|
|
33
|
-
# database drivers for MRI and JRuby
|
|
34
|
-
gem 'activerecord-jdbcsqlite3-adapter', platform: 'jruby'
|
|
35
|
-
gem 'jdbc-sqlite3', platform: 'jruby'
|
|
36
|
-
gem 'sqlite3', platform: 'mri'
|
|
37
|
-
|
|
38
30
|
gem 'vcr'
|
|
39
31
|
gem 'webmock', '~> 3.18'
|
|
40
|
-
|
|
41
|
-
# Optional dependency for Vertex AI
|
|
42
|
-
gem 'googleauth'
|
|
43
|
-
|
|
44
|
-
# Optional dependency for Bedrock
|
|
45
|
-
gem 'aws-eventstream'
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
group :development, :test do
|
|
49
|
-
gem 'turbo-rails'
|
|
50
32
|
end
|
data/README.md
CHANGED
|
@@ -4,33 +4,28 @@
|
|
|
4
4
|
|
|
5
5
|
Shared LegionIO framework for LLM provider extensions.
|
|
6
6
|
|
|
7
|
-
`lex-llm` is
|
|
7
|
+
`lex-llm` is a standard Legion extension gem. It does not expose a standalone RubyLLM-compatible API, Rails integration, generators, rake tasks, or concrete providers. Its runtime contract is `Legion::Extensions::Llm`, which provider gems extend through nested namespaces such as `Legion::Extensions::Llm::Ollama`.
|
|
8
8
|
|
|
9
9
|
The routing principle is simple: provider is not the routing unit anymore. A concrete model offering is.
|
|
10
10
|
|
|
11
|
-
That
|
|
12
|
-
|
|
13
|
-
- one local Ollama instance with many models
|
|
14
|
-
- multiple remote Ollama or vLLM instances
|
|
15
|
-
- several Bedrock accounts or regions exposing overlapping Anthropic models
|
|
16
|
-
- direct frontier providers such as OpenAI or Anthropic
|
|
17
|
-
- fleet workers on MacBooks, GPU servers, or cloud-side proxy nodes
|
|
11
|
+
That lets Legion reason about one local Ollama instance with many models, multiple remote Ollama or vLLM instances, Bedrock accounts in different regions, direct frontier providers, and fleet workers on MacBooks, GPU servers, or cloud-side proxy nodes.
|
|
18
12
|
|
|
19
13
|
## What This Gem Owns
|
|
20
14
|
|
|
21
|
-
`lex-llm` provides
|
|
15
|
+
`lex-llm` provides provider-neutral primitives only. Provider-specific behavior belongs in provider gems.
|
|
22
16
|
|
|
23
17
|
This gem owns:
|
|
24
18
|
|
|
25
|
-
- `LexLLM`, the shared Ruby API and base provider framework
|
|
26
19
|
- `Legion::Extensions::Llm`, the Legion extension namespace used by autoloading and settings
|
|
27
|
-
- provider-neutral
|
|
28
|
-
-
|
|
20
|
+
- provider-neutral request, response, message, content, token, and tool objects
|
|
21
|
+
- schema bridging through `Legion::Extensions::Llm::Schema`
|
|
22
|
+
- model metadata and capability normalization
|
|
23
|
+
- routing structures such as `Legion::Extensions::Llm::Routing::ModelOffering`
|
|
29
24
|
- fleet lane key generation for shared RabbitMQ work lanes
|
|
30
|
-
-
|
|
25
|
+
- shared chat, embedding, moderation, image, transcription, streaming, and OpenAI-compatible adapter helpers
|
|
31
26
|
- shared runtime dependencies such as `legion-json`, `legion-settings`, and `legion-logging`
|
|
32
27
|
|
|
33
|
-
Concrete provider gems should depend on this gem and implement the provider-specific transport, authentication, model discovery, request translation, and
|
|
28
|
+
Concrete provider gems should depend on this gem and implement the provider-specific transport, authentication, model discovery, request translation, response translation, and health checks.
|
|
34
29
|
|
|
35
30
|
Expected provider gems include:
|
|
36
31
|
|
|
@@ -58,27 +53,29 @@ spec.add_dependency 'lex-llm', '>= 0.1.0'
|
|
|
58
53
|
|
|
59
54
|
For local development across LegionIO repos, prefer a local path override in the app or test `Gemfile`, not a permanent git dependency in the gemspec.
|
|
60
55
|
|
|
61
|
-
##
|
|
56
|
+
## Namespace
|
|
62
57
|
|
|
63
|
-
|
|
58
|
+
Load the extension through the Legion namespace:
|
|
64
59
|
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
```ruby
|
|
61
|
+
require 'legion/extensions/llm'
|
|
62
|
+
```
|
|
67
63
|
|
|
68
64
|
Provider gems must use nested Legion extension namespaces so LegionIO autoloading can find them consistently.
|
|
69
65
|
|
|
70
66
|
Example for `lex-llm-ollama`:
|
|
71
67
|
|
|
72
68
|
```ruby
|
|
73
|
-
require 'legion/extensions/llm
|
|
69
|
+
require 'legion/extensions/llm'
|
|
74
70
|
|
|
75
71
|
module Legion
|
|
76
72
|
module Extensions
|
|
77
73
|
module Llm
|
|
78
74
|
module Ollama
|
|
79
75
|
def self.default_settings
|
|
80
|
-
Legion::Extensions::Llm.
|
|
81
|
-
|
|
76
|
+
Legion::Extensions::Llm.provider_settings(
|
|
77
|
+
family: :ollama,
|
|
78
|
+
instance: { base_url: 'http://localhost:11434' }
|
|
82
79
|
)
|
|
83
80
|
end
|
|
84
81
|
end
|
|
@@ -92,7 +89,7 @@ end
|
|
|
92
89
|
A model offering describes one concrete model made available by one provider instance. It is the base unit for routing, filtering, fleet lane creation, health, policy, and cost decisions.
|
|
93
90
|
|
|
94
91
|
```ruby
|
|
95
|
-
offering =
|
|
92
|
+
offering = Legion::Extensions::Llm::Routing::ModelOffering.new(
|
|
96
93
|
provider_family: :ollama,
|
|
97
94
|
instance_id: :macbook_m4_max,
|
|
98
95
|
transport: :local,
|
|
@@ -152,7 +149,7 @@ offering.lane_key
|
|
|
152
149
|
Embedding lanes omit context size:
|
|
153
150
|
|
|
154
151
|
```ruby
|
|
155
|
-
|
|
152
|
+
Legion::Extensions::Llm::Routing::ModelOffering.new(
|
|
156
153
|
provider_family: :ollama,
|
|
157
154
|
instance_id: :gpu_embed_01,
|
|
158
155
|
transport: :rabbitmq,
|
|
@@ -214,7 +211,7 @@ Provider gems can build a complete provider settings hash without duplicating me
|
|
|
214
211
|
Legion::Extensions::Llm.provider_settings(
|
|
215
212
|
family: :ollama,
|
|
216
213
|
instance: {
|
|
217
|
-
base_url:
|
|
214
|
+
base_url: 'http://localhost:11434',
|
|
218
215
|
fleet: { enabled: true, consumer_priority: 10 }
|
|
219
216
|
}
|
|
220
217
|
)
|
|
@@ -241,7 +238,7 @@ Provider extensions should avoid duplicating shared classes, schema logic, fleet
|
|
|
241
238
|
`lex-llm` still depends on `ruby_llm-schema` because the current schema bridge exposes:
|
|
242
239
|
|
|
243
240
|
```ruby
|
|
244
|
-
|
|
241
|
+
Legion::Extensions::Llm::Schema
|
|
245
242
|
```
|
|
246
243
|
|
|
247
244
|
as:
|
data/lex-llm.gemspec
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'lib/
|
|
3
|
+
require_relative 'lib/legion/extensions/llm/version'
|
|
4
4
|
|
|
5
5
|
Gem::Specification.new do |spec|
|
|
6
6
|
spec.name = 'lex-llm'
|
|
7
|
-
spec.version =
|
|
7
|
+
spec.version = Legion::Extensions::Llm::VERSION
|
|
8
8
|
spec.authors = ['LegionIO', 'Carmine Paolino']
|
|
9
9
|
spec.email = ['matthewdiverson@gmail.com']
|
|
10
10
|
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
require 'forwardable'
|
|
5
|
+
require 'pathname'
|
|
6
|
+
require 'ruby_llm/schema'
|
|
7
|
+
|
|
8
|
+
module Legion
|
|
9
|
+
module Extensions
|
|
10
|
+
module Llm
|
|
11
|
+
# Base class for simple, class-configured agents.
|
|
12
|
+
class Agent
|
|
13
|
+
extend Forwardable
|
|
14
|
+
include Enumerable
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def inherited(subclass)
|
|
18
|
+
super
|
|
19
|
+
subclass.instance_variable_set(:@chat_kwargs, (@chat_kwargs || {}).dup)
|
|
20
|
+
subclass.instance_variable_set(:@tools, (@tools || []).dup)
|
|
21
|
+
subclass.instance_variable_set(:@instructions, @instructions)
|
|
22
|
+
subclass.instance_variable_set(:@temperature, @temperature)
|
|
23
|
+
subclass.instance_variable_set(:@thinking, @thinking)
|
|
24
|
+
subclass.instance_variable_set(:@params, (@params || {}).dup)
|
|
25
|
+
subclass.instance_variable_set(:@headers, (@headers || {}).dup)
|
|
26
|
+
subclass.instance_variable_set(:@schema, @schema)
|
|
27
|
+
subclass.instance_variable_set(:@context, @context)
|
|
28
|
+
subclass.instance_variable_set(:@chat_model, @chat_model)
|
|
29
|
+
subclass.instance_variable_set(:@input_names, (@input_names || []).dup)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def model(model_id = nil, **options)
|
|
33
|
+
options[:model] = model_id unless model_id.nil?
|
|
34
|
+
@chat_kwargs = options
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def tools(*tools, &block)
|
|
38
|
+
return @tools || [] if tools.empty? && !block_given?
|
|
39
|
+
|
|
40
|
+
@tools = block_given? ? block : tools.flatten
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def instructions(text = nil, **prompt_locals, &block)
|
|
44
|
+
if text.nil? && prompt_locals.empty? && !block_given?
|
|
45
|
+
@instructions ||= { prompt: 'instructions', locals: {} }
|
|
46
|
+
return @instructions
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
@instructions = block || text || { prompt: 'instructions', locals: prompt_locals }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def temperature(value = nil)
|
|
53
|
+
return @temperature if value.nil?
|
|
54
|
+
|
|
55
|
+
@temperature = value
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def thinking(effort: nil, budget: nil)
|
|
59
|
+
return @thinking if effort.nil? && budget.nil?
|
|
60
|
+
|
|
61
|
+
@thinking = { effort: effort, budget: budget }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def params(**params, &block)
|
|
65
|
+
return @params || {} if params.empty? && !block_given?
|
|
66
|
+
|
|
67
|
+
@params = block_given? ? block : params
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def headers(**headers, &block)
|
|
71
|
+
return @headers || {} if headers.empty? && !block_given?
|
|
72
|
+
|
|
73
|
+
@headers = block_given? ? block : headers
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def schema(value = nil, &block)
|
|
77
|
+
return @schema if value.nil? && !block_given?
|
|
78
|
+
|
|
79
|
+
@schema = block_given? ? block : value
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def context(value = nil)
|
|
83
|
+
return @context if value.nil?
|
|
84
|
+
|
|
85
|
+
@context = value
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def chat_model(value = nil)
|
|
89
|
+
return @chat_model if value.nil?
|
|
90
|
+
|
|
91
|
+
@chat_model = value
|
|
92
|
+
remove_instance_variable(:@resolved_chat_model) if instance_variable_defined?(:@resolved_chat_model)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def inputs(*names)
|
|
96
|
+
return @input_names || [] if names.empty?
|
|
97
|
+
|
|
98
|
+
@input_names = names.flatten.map(&:to_sym)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def chat_kwargs
|
|
102
|
+
@chat_kwargs || {}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def chat(**kwargs)
|
|
106
|
+
input_values, chat_options = partition_inputs(kwargs)
|
|
107
|
+
chat = Legion::Extensions::Llm.chat(**chat_kwargs, **chat_options)
|
|
108
|
+
apply_configuration(chat, input_values:, persist_instructions: true)
|
|
109
|
+
chat
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def create(**)
|
|
113
|
+
with_chat_record(:create, **)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def create!(**)
|
|
117
|
+
with_chat_record(:create!, **)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def find(id, **kwargs)
|
|
121
|
+
raise ArgumentError, 'chat_model must be configured to use find' unless resolved_chat_model
|
|
122
|
+
|
|
123
|
+
input_values, = partition_inputs(kwargs)
|
|
124
|
+
record = resolved_chat_model.find(id)
|
|
125
|
+
apply_configuration(record, input_values:, persist_instructions: false)
|
|
126
|
+
|
|
127
|
+
record
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def sync_instructions!(chat_or_id, **kwargs)
|
|
131
|
+
raise ArgumentError, 'chat_model must be configured to use sync_instructions!' unless resolved_chat_model
|
|
132
|
+
|
|
133
|
+
input_values, = partition_inputs(kwargs)
|
|
134
|
+
record = chat_or_id.is_a?(resolved_chat_model) ? chat_or_id : resolved_chat_model.find(chat_or_id)
|
|
135
|
+
apply_assume_model_exists(record)
|
|
136
|
+
runtime = runtime_context(chat: record, inputs: input_values)
|
|
137
|
+
instructions_value = resolved_instructions_value(record, runtime, inputs: input_values)
|
|
138
|
+
return record if instructions_value.nil?
|
|
139
|
+
|
|
140
|
+
record.with_instructions(instructions_value)
|
|
141
|
+
record
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def render_prompt(name, chat:, inputs:, locals:)
|
|
145
|
+
path = prompt_path_for(name)
|
|
146
|
+
unless File.exist?(path)
|
|
147
|
+
raise Legion::Extensions::Llm::PromptNotFoundError,
|
|
148
|
+
"Prompt file not found for #{self}: #{path}. Create the file or use inline instructions."
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
resolved_locals = resolve_prompt_locals(locals, runtime: runtime_context(chat:, inputs:), chat:, inputs:)
|
|
152
|
+
ERB.new(File.read(path)).result_with_hash(resolved_locals)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
private
|
|
156
|
+
|
|
157
|
+
def with_chat_record(method_name, **kwargs)
|
|
158
|
+
raise ArgumentError, 'chat_model must be configured to use create/create!' unless resolved_chat_model
|
|
159
|
+
|
|
160
|
+
input_values, chat_options = partition_inputs(kwargs)
|
|
161
|
+
record = resolved_chat_model.public_send(method_name, **chat_kwargs, **chat_options)
|
|
162
|
+
apply_configuration(record, input_values:, persist_instructions: true) if record
|
|
163
|
+
record
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def apply_configuration(chat_object, input_values:, persist_instructions:)
|
|
167
|
+
runtime = runtime_context(chat: chat_object, inputs: input_values)
|
|
168
|
+
llm_chat = llm_chat_for(chat_object)
|
|
169
|
+
|
|
170
|
+
apply_context(llm_chat)
|
|
171
|
+
apply_instructions(chat_object, runtime, inputs: input_values, persist: persist_instructions)
|
|
172
|
+
apply_tools(llm_chat, runtime)
|
|
173
|
+
apply_temperature(llm_chat)
|
|
174
|
+
apply_thinking(llm_chat)
|
|
175
|
+
apply_params(llm_chat, runtime)
|
|
176
|
+
apply_headers(llm_chat, runtime)
|
|
177
|
+
apply_schema(llm_chat, runtime)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def apply_context(llm_chat)
|
|
181
|
+
llm_chat.with_context(context) if context
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def apply_instructions(chat_object, runtime, inputs:, persist:)
|
|
185
|
+
value = resolved_instructions_value(chat_object, runtime, inputs:)
|
|
186
|
+
return if value.nil?
|
|
187
|
+
|
|
188
|
+
target = instruction_target(chat_object, persist:)
|
|
189
|
+
return target.with_runtime_instructions(value) if use_runtime_instructions?(target, persist:)
|
|
190
|
+
|
|
191
|
+
target.with_instructions(value)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def apply_tools(llm_chat, runtime)
|
|
195
|
+
tools_to_apply = Array(evaluate(tools, runtime))
|
|
196
|
+
llm_chat.with_tools(*tools_to_apply) unless tools_to_apply.empty?
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def apply_temperature(llm_chat)
|
|
200
|
+
llm_chat.with_temperature(temperature) unless temperature.nil?
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def apply_thinking(llm_chat)
|
|
204
|
+
llm_chat.with_thinking(**thinking) if thinking
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def apply_params(llm_chat, runtime)
|
|
208
|
+
value = evaluate(params, runtime)
|
|
209
|
+
llm_chat.with_params(**value) if value && !value.empty?
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def apply_headers(llm_chat, runtime)
|
|
213
|
+
value = evaluate(headers, runtime)
|
|
214
|
+
llm_chat.with_headers(**value) if value && !value.empty?
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def apply_schema(llm_chat, runtime)
|
|
218
|
+
value = resolved_schema_value(runtime)
|
|
219
|
+
llm_chat.with_schema(value) if value
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def resolved_schema_value(runtime)
|
|
223
|
+
value = schema
|
|
224
|
+
return value unless value.is_a?(Proc)
|
|
225
|
+
|
|
226
|
+
evaluate(value, runtime)
|
|
227
|
+
rescue NoMethodError => e
|
|
228
|
+
raise unless e.receiver.equal?(runtime)
|
|
229
|
+
|
|
230
|
+
Legion::Extensions::Llm::Schema.create(&value)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def llm_chat_for(chat_object)
|
|
234
|
+
apply_assume_model_exists(chat_object)
|
|
235
|
+
chat_object.respond_to?(:to_llm) ? chat_object.to_llm : chat_object
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def apply_assume_model_exists(chat_object)
|
|
239
|
+
return unless chat_kwargs.key?(:assume_model_exists) &&
|
|
240
|
+
resolved_chat_model &&
|
|
241
|
+
chat_object.is_a?(resolved_chat_model)
|
|
242
|
+
|
|
243
|
+
chat_object.assume_model_exists = chat_kwargs[:assume_model_exists]
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def evaluate(value, runtime)
|
|
247
|
+
value.is_a?(Proc) ? runtime.instance_exec(&value) : value
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def resolved_instructions_value(chat_object, runtime, inputs:)
|
|
251
|
+
value = evaluate(@instructions, runtime)
|
|
252
|
+
return value unless prompt_instruction?(value)
|
|
253
|
+
|
|
254
|
+
runtime.prompt(
|
|
255
|
+
value[:prompt],
|
|
256
|
+
**resolve_prompt_locals(value[:locals] || {}, runtime:, chat: chat_object, inputs:)
|
|
257
|
+
)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def prompt_instruction?(value)
|
|
261
|
+
value.is_a?(Hash) && value[:prompt]
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def instruction_target(chat_object, persist:)
|
|
265
|
+
if persist || !chat_object.respond_to?(:to_llm)
|
|
266
|
+
chat_object
|
|
267
|
+
else
|
|
268
|
+
runtime_instruction_target(chat_object)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def runtime_instruction_target(chat_object)
|
|
273
|
+
return chat_object if chat_object.respond_to?(:with_runtime_instructions)
|
|
274
|
+
|
|
275
|
+
chat_object.to_llm
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def use_runtime_instructions?(target, persist:)
|
|
279
|
+
!persist && target.respond_to?(:with_runtime_instructions)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def resolve_prompt_locals(locals, runtime:, chat:, inputs:)
|
|
283
|
+
base = { chat: chat }.merge(inputs)
|
|
284
|
+
evaluated = locals.each_with_object({}) do |(key, value), acc|
|
|
285
|
+
acc[key.to_sym] = value.is_a?(Proc) ? runtime.instance_exec(&value) : value
|
|
286
|
+
end
|
|
287
|
+
base.merge(evaluated)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def partition_inputs(kwargs)
|
|
291
|
+
input_values = {}
|
|
292
|
+
chat_options = {}
|
|
293
|
+
|
|
294
|
+
kwargs.each do |key, value|
|
|
295
|
+
symbolized_key = key.to_sym
|
|
296
|
+
if inputs.include?(symbolized_key)
|
|
297
|
+
input_values[symbolized_key] = value
|
|
298
|
+
else
|
|
299
|
+
chat_options[symbolized_key] = value
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
[input_values, chat_options]
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def runtime_context(chat:, inputs:)
|
|
307
|
+
agent_class = self
|
|
308
|
+
Object.new.tap do |runtime|
|
|
309
|
+
runtime.define_singleton_method(:chat) { chat }
|
|
310
|
+
runtime.define_singleton_method(:prompt) do |name, **locals|
|
|
311
|
+
agent_class.render_prompt(name, chat:, inputs:, locals:)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
inputs.each do |name, value|
|
|
315
|
+
runtime.define_singleton_method(name) { value }
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def prompt_path_for(name)
|
|
321
|
+
filename = name.to_s
|
|
322
|
+
filename += '.txt.erb' unless filename.end_with?('.txt.erb')
|
|
323
|
+
prompt_root.join(prompt_agent_path, filename)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def prompt_agent_path
|
|
327
|
+
class_name = name || 'agent'
|
|
328
|
+
class_name.gsub('::', '/')
|
|
329
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
330
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
331
|
+
.tr('-', '_')
|
|
332
|
+
.downcase
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def prompt_root
|
|
336
|
+
Pathname.new(Dir.pwd).join('app/prompts')
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def resolved_chat_model
|
|
340
|
+
return @resolved_chat_model if defined?(@resolved_chat_model)
|
|
341
|
+
|
|
342
|
+
@resolved_chat_model = case @chat_model
|
|
343
|
+
when String then Object.const_get(@chat_model)
|
|
344
|
+
else @chat_model
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def initialize(chat: nil, inputs: nil, persist_instructions: true, **kwargs)
|
|
350
|
+
input_values, chat_options = self.class.send(:partition_inputs, kwargs)
|
|
351
|
+
@chat = chat || Legion::Extensions::Llm.chat(**self.class.chat_kwargs, **chat_options)
|
|
352
|
+
self.class.send(:apply_configuration, @chat, input_values: input_values.merge(inputs || {}),
|
|
353
|
+
persist_instructions:)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
attr_reader :chat
|
|
357
|
+
|
|
358
|
+
def_delegators :chat, :model, :messages, :tools, :params, :headers, :schema, :ask, :say,
|
|
359
|
+
:with_tool, :with_tools,
|
|
360
|
+
:with_model, :with_temperature, :with_thinking, :with_context, :with_params, :with_headers,
|
|
361
|
+
:with_schema, :on_new_message, :on_end_message, :on_tool_call, :on_tool_result, :each, :complete,
|
|
362
|
+
:add_message, :reset_messages!
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Llm
|
|
6
|
+
# Manages model aliases for provider-specific versions
|
|
7
|
+
class Aliases
|
|
8
|
+
class << self
|
|
9
|
+
def resolve(model_id, provider = nil)
|
|
10
|
+
return model_id unless aliases[model_id]
|
|
11
|
+
|
|
12
|
+
if provider
|
|
13
|
+
aliases[model_id][provider.to_s] || model_id
|
|
14
|
+
else
|
|
15
|
+
aliases[model_id].values.first || model_id
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def aliases
|
|
20
|
+
@aliases ||= load_aliases
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def aliases_file
|
|
24
|
+
File.expand_path('aliases.json', __dir__)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def load_aliases
|
|
28
|
+
if File.exist?(aliases_file)
|
|
29
|
+
Legion::JSON.parse(File.read(aliases_file), symbolize_names: false)
|
|
30
|
+
else
|
|
31
|
+
{}
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def reload!
|
|
36
|
+
@aliases = load_aliases
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|