llm.rb 5.4.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -1
- data/README.md +104 -22
- data/lib/llm/active_record/acts_as_agent.rb +5 -11
- data/lib/llm/active_record/acts_as_llm.rb +17 -37
- data/lib/llm/error.rb +4 -0
- data/lib/llm/function/ractor/task.rb +8 -2
- data/lib/llm/function.rb +6 -2
- data/lib/llm/sequel/agent.rb +2 -7
- data/lib/llm/sequel/plugin.rb +31 -38
- data/lib/llm/skill.rb +4 -0
- data/lib/llm/version.rb +1 -1
- data/llm.gemspec +1 -0
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8cc16b548be77e0a2bb78c0e130b14f0ac8fdabb2c2f082dd2824282e5a5733b
|
|
4
|
+
data.tar.gz: 668655ba6a7d65d44b53cc1b8b33afddf3563dc216df83181fb2b7394a2847ff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2257aeec49a43c56bfc974e3fc190a850ea27c98c437c7c242efe6c645c000eebdbe2e731fcc0b287303690c1055768f2de32be6ac900391c5156260da9c2ce5
|
|
7
|
+
data.tar.gz: 8f4f8f3475ac1bd2d9ffbd8c0b413b46224936459eb0ed6bb395876b994c1ab67ec02cf670176f90b8d9b892b59ab4b67271f4b69c80fa7c094aa89310ab5c58
|
data/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## v6.0.0
|
|
4
4
|
|
|
5
5
|
Changes since `v5.4.0`.
|
|
6
6
|
|
|
7
|
+
This release simplifies the ORM persistence contract around serialized
|
|
8
|
+
`data` state, removing the assumption of reserved `provider`, `model`, and
|
|
9
|
+
usage columns. Provider selection must now come from `provider:` hooks,
|
|
10
|
+
model defaults come from `context:` or agent DSL, and usage is read from the
|
|
11
|
+
serialized runtime state. Alongside this breaking change, Sequel JSON and
|
|
12
|
+
JSONB persistence is fixed, ractor-backed tools now fire tracer callbacks,
|
|
13
|
+
and `LLM::RactorError` is raised for unsupported ractor tool work.
|
|
14
|
+
|
|
15
|
+
### Change
|
|
16
|
+
|
|
17
|
+
* **Simplify ORM persistence to serialized `data` state** <br>
|
|
18
|
+
Change the built-in ActiveRecord and Sequel wrappers to treat serialized
|
|
19
|
+
`data` as the persistence contract, instead of assuming reserved
|
|
20
|
+
`provider`, `model`, and usage columns. Provider selection must now come
|
|
21
|
+
from `provider:` hooks that resolve a real `LLM::Provider` instance, model
|
|
22
|
+
defaults come from `context:` or agent DSL, and `usage` is read from the
|
|
23
|
+
serialized runtime state.
|
|
24
|
+
|
|
25
|
+
### Fix
|
|
26
|
+
|
|
27
|
+
* **Fix Sequel JSON and JSONB persistence** <br>
|
|
28
|
+
Load Sequel PostgreSQL JSON support when `plugin :llm` is configured with
|
|
29
|
+
`format: :json` or `:jsonb`, and wrap structured payloads correctly so
|
|
30
|
+
persisted context state can be stored in PostgreSQL JSON columns.
|
|
31
|
+
|
|
32
|
+
* **Trace ractor-backed tool callbacks** <br>
|
|
33
|
+
Make tool tracers fire `on_tool_start` and `on_tool_finish` for
|
|
34
|
+
class-based `:ractor` execution too, so ractor-backed tool calls show up
|
|
35
|
+
in tracer callbacks like the other concurrent tool paths.
|
|
36
|
+
|
|
37
|
+
* **Raise `LLM::RactorError` for unsupported ractor tool work** <br>
|
|
38
|
+
Add `LLM::RactorError` and fail fast when `:ractor` execution is requested
|
|
39
|
+
for unsupported tool types such as skill-backed tools, instead of letting
|
|
40
|
+
deeper Ruby isolation errors leak out later in execution.
|
|
41
|
+
|
|
7
42
|
## v5.4.0
|
|
8
43
|
|
|
9
44
|
Changes since `v5.3.0`.
|
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<p align="center">
|
|
5
5
|
<a href="https://0x1eef.github.io/x/llm.rb?rebuild=1"><img src="https://img.shields.io/badge/docs-0x1eef.github.io-blue.svg" alt="RubyDoc"></a>
|
|
6
6
|
<a href="https://opensource.org/license/0bsd"><img src="https://img.shields.io/badge/License-0BSD-orange.svg?" alt="License"></a>
|
|
7
|
-
<a href="https://github.com/llmrb/llm.rb/tags"><img src="https://img.shields.io/badge/version-
|
|
7
|
+
<a href="https://github.com/llmrb/llm.rb/tags"><img src="https://img.shields.io/badge/version-6.0.0-green.svg?" alt="Version"></a>
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
10
|
## About
|
|
@@ -25,7 +25,6 @@ schemas, files, and persisted state, so real systems can be built out of one coh
|
|
|
25
25
|
execution model instead of a pile of adapters.
|
|
26
26
|
|
|
27
27
|
Want to see some code? Jump to [the examples](#examples) section. <br>
|
|
28
|
-
Want to see an agentic framework built on top of llm.rb? Check out [general-intelligence-systems/brute](https://github.com/general-intelligence-systems/brute). <br>
|
|
29
28
|
Want to see a self-hosted LLM environment built on llm.rb? Check out [Relay](https://github.com/llmrb/relay).
|
|
30
29
|
|
|
31
30
|
## Architecture
|
|
@@ -102,20 +101,26 @@ separate agent table or a second persistence layer.
|
|
|
102
101
|
|
|
103
102
|
`acts_as_agent` extends a model with agent capabilities: the same runtime
|
|
104
103
|
surface as [`LLM::Agent`](https://0x1eef.github.io/x/llm.rb/LLM/Agent.html),
|
|
105
|
-
because it actually wraps an `LLM::Agent`, plus persistence through
|
|
106
|
-
JSON, or JSONB-backed column on the same table.
|
|
104
|
+
because it actually wraps an `LLM::Agent`, plus persistence through one text,
|
|
105
|
+
JSON, or JSONB-backed `data` column on the same table. If your app also has
|
|
106
|
+
provider or model columns, provide them to llm.rb through `set_provider` and
|
|
107
|
+
`set_context`.
|
|
107
108
|
|
|
108
109
|
|
|
109
110
|
```ruby
|
|
110
111
|
class Ticket < ApplicationRecord
|
|
111
|
-
acts_as_agent provider: :set_provider
|
|
112
|
+
acts_as_agent provider: :set_provider, context: :set_context
|
|
112
113
|
model "gpt-5.4-mini"
|
|
113
114
|
instructions "You are a support assistant."
|
|
114
115
|
|
|
115
116
|
private
|
|
116
117
|
|
|
117
118
|
def set_provider
|
|
118
|
-
|
|
119
|
+
LLM.openai(key: ENV["OPENAI_SECRET"])
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def set_context
|
|
123
|
+
{ mode: :responses, store: false }
|
|
119
124
|
end
|
|
120
125
|
end
|
|
121
126
|
```
|
|
@@ -303,7 +308,7 @@ finer sequential control across several steps before shutting the client down.
|
|
|
303
308
|
```ruby
|
|
304
309
|
mcp = LLM::MCP.http(
|
|
305
310
|
url: "https://api.githubcopilot.com/mcp/",
|
|
306
|
-
headers: {"Authorization" => "Bearer #{ENV
|
|
311
|
+
headers: {"Authorization" => "Bearer #{ENV["GITHUB_PAT"]}"}
|
|
307
312
|
).persistent
|
|
308
313
|
mcp.run do
|
|
309
314
|
ctx = LLM::Context.new(llm, tools: mcp.tools)
|
|
@@ -376,7 +381,8 @@ worker.join
|
|
|
376
381
|
Use threads, fibers, async tasks, or experimental ractors without
|
|
377
382
|
rewriting your tool layer. The current `:ractor` mode is for class-based
|
|
378
383
|
tools and does not support MCP tools, but mixed workloads can branch on
|
|
379
|
-
`tool.mcp?` and choose a supported strategy per tool.
|
|
384
|
+
`tool.mcp?` and choose a supported strategy per tool. Class-based
|
|
385
|
+
`:ractor` tools still emit normal tool tracer callbacks. `:ractor` is
|
|
380
386
|
especially useful for CPU-bound tools, while `:task`, `:fiber`, or
|
|
381
387
|
`:thread` may be a better fit for I/O-bound work.
|
|
382
388
|
- **Advanced workloads are built in, not bolted on** <br>
|
|
@@ -696,7 +702,7 @@ worker.join
|
|
|
696
702
|
|
|
697
703
|
#### Sequel (ORM)
|
|
698
704
|
|
|
699
|
-
The `plugin :llm` integration wraps [`LLM::Context`](https://0x1eef.github.io/x/llm.rb/LLM/Context.html) on a `Sequel::Model` and keeps tool execution explicit. <br> See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or [deepdive (markdown)](resources/deepdive.md) for more examples.
|
|
705
|
+
The `plugin :llm` integration wraps [`LLM::Context`](https://0x1eef.github.io/x/llm.rb/LLM/Context.html) on a `Sequel::Model` and keeps tool execution explicit. Like the ActiveRecord wrappers, its built-in persistence contract is the serialized `data` column, while `provider:` resolves a real `LLM::Provider` instance and `context:` injects defaults such as `model:`. <br> See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or [deepdive (markdown)](resources/deepdive.md) for more examples.
|
|
700
706
|
|
|
701
707
|
```ruby
|
|
702
708
|
require "llm"
|
|
@@ -705,10 +711,20 @@ require "sequel"
|
|
|
705
711
|
require "sequel/plugins/llm"
|
|
706
712
|
|
|
707
713
|
class Context < Sequel::Model
|
|
708
|
-
plugin :llm, provider:
|
|
714
|
+
plugin :llm, provider: :set_provider, context: :set_context
|
|
715
|
+
|
|
716
|
+
private
|
|
717
|
+
|
|
718
|
+
def set_provider
|
|
719
|
+
LLM.openai(key: ENV["OPENAI_SECRET"])
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
def set_context
|
|
723
|
+
{model: "gpt-5.4-mini", mode: :responses, store: false}
|
|
724
|
+
end
|
|
709
725
|
end
|
|
710
726
|
|
|
711
|
-
ctx = Context.create
|
|
727
|
+
ctx = Context.create
|
|
712
728
|
ctx.talk("Remember that my favorite language is Ruby")
|
|
713
729
|
puts ctx.talk("What is my favorite language?").content
|
|
714
730
|
```
|
|
@@ -716,36 +732,76 @@ puts ctx.talk("What is my favorite language?").content
|
|
|
716
732
|
#### ActiveRecord (ORM): acts_as_llm
|
|
717
733
|
|
|
718
734
|
The `acts_as_llm` method wraps [`LLM::Context`](https://0x1eef.github.io/x/llm.rb/LLM/Context.html) and
|
|
719
|
-
provides full control over tool execution.
|
|
735
|
+
provides full control over tool execution. Its built-in persistence contract is
|
|
736
|
+
one serialized `data` column. If your app has provider, model, or usage
|
|
737
|
+
columns, provide them to llm.rb through `provider:` and `context:` instead of
|
|
738
|
+
relying on reserved wrapper columns.
|
|
739
|
+
|
|
740
|
+
See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or [deepdive (markdown)](resources/deepdive.md) for more examples.
|
|
720
741
|
|
|
721
742
|
```ruby
|
|
722
743
|
require "llm"
|
|
723
|
-
require "net/http/persistent"
|
|
724
744
|
require "active_record"
|
|
725
745
|
require "llm/active_record"
|
|
726
746
|
|
|
727
747
|
class Context < ApplicationRecord
|
|
728
|
-
acts_as_llm provider:
|
|
748
|
+
acts_as_llm provider: :set_provider, context: :set_context
|
|
749
|
+
|
|
750
|
+
private
|
|
751
|
+
|
|
752
|
+
def set_provider
|
|
753
|
+
LLM.openai(key: ENV["OPENAI_SECRET"])
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
def set_context
|
|
757
|
+
{model: "gpt-5.4-mini", mode: :responses, store: false}
|
|
758
|
+
end
|
|
729
759
|
end
|
|
730
760
|
|
|
731
|
-
ctx = Context.create!
|
|
761
|
+
ctx = Context.create!
|
|
732
762
|
ctx.talk("Remember that my favorite language is Ruby")
|
|
733
763
|
puts ctx.talk("What is my favorite language?").content
|
|
734
764
|
```
|
|
735
765
|
|
|
766
|
+
```ruby
|
|
767
|
+
require "llm"
|
|
768
|
+
require "active_record"
|
|
769
|
+
require "llm/active_record"
|
|
770
|
+
|
|
771
|
+
class Context < ApplicationRecord
|
|
772
|
+
acts_as_llm provider: :set_provider, context: :set_context
|
|
773
|
+
|
|
774
|
+
# Optional application columns can still provide the provider and context.
|
|
775
|
+
# For example, `provider_name` and `model_name` can be normal columns.
|
|
776
|
+
|
|
777
|
+
private
|
|
778
|
+
|
|
779
|
+
def set_provider
|
|
780
|
+
LLM.public_send(provider_name, key: provider_key)
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
def set_context
|
|
784
|
+
{model: model_name, mode: :responses, store: false}
|
|
785
|
+
end
|
|
786
|
+
end
|
|
787
|
+
```
|
|
788
|
+
|
|
736
789
|
#### ActiveRecord (ORM): acts_as_agent
|
|
737
790
|
|
|
738
791
|
The `acts_as_agent` method wraps [`LLM::Agent`](https://0x1eef.github.io/x/llm.rb/LLM/Agent.html) and
|
|
739
|
-
manages tool execution for you.
|
|
792
|
+
manages tool execution for you. Like `acts_as_llm`, its built-in persistence
|
|
793
|
+
contract is one serialized `data` column. If your app has provider or model
|
|
794
|
+
columns, provide them to llm.rb through your hooks and agent DSL.
|
|
795
|
+
|
|
796
|
+
See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or [deepdive (markdown)](resources/deepdive.md) for more examples.
|
|
740
797
|
|
|
741
798
|
```ruby
|
|
742
799
|
require "llm"
|
|
743
|
-
require "net/http/persistent"
|
|
744
800
|
require "active_record"
|
|
745
801
|
require "llm/active_record"
|
|
746
802
|
|
|
747
803
|
class Ticket < ApplicationRecord
|
|
748
|
-
acts_as_agent provider: :set_provider
|
|
804
|
+
acts_as_agent provider: :set_provider, context: :set_context
|
|
749
805
|
model "gpt-5.4-mini"
|
|
750
806
|
instructions "You are a concise support assistant."
|
|
751
807
|
tools SearchDocs, Escalate
|
|
@@ -754,14 +810,40 @@ class Ticket < ApplicationRecord
|
|
|
754
810
|
private
|
|
755
811
|
|
|
756
812
|
def set_provider
|
|
757
|
-
|
|
813
|
+
LLM.openai(key: ENV["OPENAI_SECRET"])
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
def set_context
|
|
817
|
+
{mode: :responses, store: false}
|
|
758
818
|
end
|
|
759
819
|
end
|
|
760
820
|
|
|
761
|
-
ticket = Ticket.create!
|
|
821
|
+
ticket = Ticket.create!
|
|
762
822
|
puts ticket.talk("How do I rotate my API key?").content
|
|
763
823
|
```
|
|
764
824
|
|
|
825
|
+
```ruby
|
|
826
|
+
require "llm"
|
|
827
|
+
require "active_record"
|
|
828
|
+
require "llm/active_record"
|
|
829
|
+
|
|
830
|
+
class Ticket < ApplicationRecord
|
|
831
|
+
acts_as_agent provider: :set_provider, context: :set_context
|
|
832
|
+
model "gpt-5.4-mini"
|
|
833
|
+
instructions "You are a concise support assistant."
|
|
834
|
+
|
|
835
|
+
private
|
|
836
|
+
|
|
837
|
+
def set_provider
|
|
838
|
+
LLM.public_send(provider_name, key: provider_key)
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
def set_context
|
|
842
|
+
{mode: :responses, store: false}
|
|
843
|
+
end
|
|
844
|
+
end
|
|
845
|
+
```
|
|
846
|
+
|
|
765
847
|
#### MCP
|
|
766
848
|
|
|
767
849
|
This example uses [`LLM::MCP`](https://0x1eef.github.io/x/llm.rb/LLM/MCP.html) over HTTP so remote GitHub MCP tools run through the same `LLM::Context` tool path as local tools. It expects a GitHub token in `ENV["GITHUB_PAT"]`. See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or [deepdive (markdown)](resources/deepdive.md) for more examples.
|
|
@@ -773,7 +855,7 @@ require "net/http/persistent"
|
|
|
773
855
|
llm = LLM.openai(key: ENV["KEY"])
|
|
774
856
|
mcp = LLM::MCP.http(
|
|
775
857
|
url: "https://api.githubcopilot.com/mcp/",
|
|
776
|
-
headers: {"Authorization" => "Bearer #{ENV
|
|
858
|
+
headers: {"Authorization" => "Bearer #{ENV["GITHUB_PAT"]}"}
|
|
777
859
|
).persistent
|
|
778
860
|
|
|
779
861
|
mcp.start
|
|
@@ -788,7 +870,7 @@ For scoped work, `mcp.run do ... end` is shorter and handles cleanup for you:
|
|
|
788
870
|
```ruby
|
|
789
871
|
mcp = LLM::MCP.http(
|
|
790
872
|
url: "https://api.githubcopilot.com/mcp/",
|
|
791
|
-
headers: {"Authorization" => "Bearer #{ENV
|
|
873
|
+
headers: {"Authorization" => "Bearer #{ENV["GITHUB_PAT"]}"}
|
|
792
874
|
).persistent
|
|
793
875
|
mcp.run do
|
|
794
876
|
ctx = LLM::Context.new(llm, stream: $stdout, tools: mcp.tools)
|
|
@@ -11,7 +11,6 @@ module LLM::ActiveRecord
|
|
|
11
11
|
# class and forwarded to an internal agent subclass.
|
|
12
12
|
module ActsAsAgent
|
|
13
13
|
EMPTY_HASH = LLM::ActiveRecord::ActsAsLLM::EMPTY_HASH
|
|
14
|
-
DEFAULT_USAGE_COLUMNS = LLM::ActiveRecord::ActsAsLLM::DEFAULT_USAGE_COLUMNS
|
|
15
14
|
DEFAULTS = LLM::ActiveRecord::ActsAsLLM::DEFAULTS
|
|
16
15
|
Utils = LLM::ActiveRecord::ActsAsLLM::Utils
|
|
17
16
|
|
|
@@ -58,8 +57,6 @@ module LLM::ActiveRecord
|
|
|
58
57
|
# @param [Class] model
|
|
59
58
|
# @return [void]
|
|
60
59
|
def self.extended(model)
|
|
61
|
-
options = model.llm_plugin_options
|
|
62
|
-
model.validates options[:provider_column], options[:model_column], presence: true
|
|
63
60
|
model.include LLM::ActiveRecord::ActsAsLLM::InstanceMethods unless model.ancestors.include?(LLM::ActiveRecord::ActsAsLLM::InstanceMethods)
|
|
64
61
|
model.include InstanceMethods unless model.ancestors.include?(InstanceMethods)
|
|
65
62
|
model.extend ClassMethods unless model.singleton_class.ancestors.include?(ClassMethods)
|
|
@@ -77,6 +74,8 @@ module LLM::ActiveRecord
|
|
|
77
74
|
# @option options [Proc, Symbol, LLM::Tracer, nil] :tracer
|
|
78
75
|
# Optional tracer, method name, or proc that resolves to one and is
|
|
79
76
|
# assigned through `llm.tracer = ...` on the resolved provider.
|
|
77
|
+
# @option options [Proc, Symbol, LLM::Provider] :provider
|
|
78
|
+
# Must resolve to an `LLM::Provider` instance for the current record.
|
|
80
79
|
# @yield
|
|
81
80
|
# Evaluated in the model class after the wrapper is installed, so agent
|
|
82
81
|
# DSL methods such as `model`, `tools`, `schema`, `instructions`, and
|
|
@@ -84,9 +83,8 @@ module LLM::ActiveRecord
|
|
|
84
83
|
# @return [void]
|
|
85
84
|
def acts_as_agent(options = EMPTY_HASH, &block)
|
|
86
85
|
options = DEFAULTS.merge(options)
|
|
87
|
-
usage_columns = DEFAULT_USAGE_COLUMNS.merge(options[:usage_columns] || EMPTY_HASH)
|
|
88
86
|
class_attribute :llm_plugin_options, instance_accessor: false, default: DEFAULTS unless respond_to?(:llm_plugin_options)
|
|
89
|
-
self.llm_plugin_options = options.
|
|
87
|
+
self.llm_plugin_options = options.freeze
|
|
90
88
|
extend Hooks
|
|
91
89
|
class_exec(&block) if block
|
|
92
90
|
end
|
|
@@ -97,11 +95,8 @@ module LLM::ActiveRecord
|
|
|
97
95
|
# @return [LLM::Provider]
|
|
98
96
|
def llm
|
|
99
97
|
options = self.class.llm_plugin_options
|
|
100
|
-
columns = Utils.columns(options)
|
|
101
|
-
provider = self[columns[:provider_column]]
|
|
102
|
-
kwargs = Utils.resolve_options(self, options[:provider], ActsAsAgent::EMPTY_HASH)
|
|
103
98
|
return @llm if @llm
|
|
104
|
-
@llm =
|
|
99
|
+
@llm = Utils.resolve_provider(self, options, ActsAsAgent::EMPTY_HASH)
|
|
105
100
|
@llm.tracer = Utils.resolve_option(self, options[:tracer]) if options[:tracer]
|
|
106
101
|
@llm
|
|
107
102
|
end
|
|
@@ -113,10 +108,9 @@ module LLM::ActiveRecord
|
|
|
113
108
|
def ctx
|
|
114
109
|
@ctx ||= begin
|
|
115
110
|
options = self.class.llm_plugin_options
|
|
116
|
-
columns = Utils.columns(options)
|
|
117
111
|
params = Utils.resolve_options(self, options[:context], ActsAsAgent::EMPTY_HASH).dup
|
|
118
|
-
params[:model] ||= self[columns[:model_column]]
|
|
119
112
|
ctx = self.class.agent.new(llm, params.compact)
|
|
113
|
+
columns = Utils.columns(options)
|
|
120
114
|
data = self[columns[:data_column]]
|
|
121
115
|
if data.nil? || data == ""
|
|
122
116
|
ctx
|
|
@@ -17,19 +17,11 @@ module LLM::ActiveRecord
|
|
|
17
17
|
# `tracer:` can also be configured as symbols that are called on the model.
|
|
18
18
|
module ActsAsLLM
|
|
19
19
|
EMPTY_HASH = {}.freeze
|
|
20
|
-
DEFAULT_USAGE_COLUMNS = {
|
|
21
|
-
input_tokens: :input_tokens,
|
|
22
|
-
output_tokens: :output_tokens,
|
|
23
|
-
total_tokens: :total_tokens
|
|
24
|
-
}.freeze
|
|
25
20
|
DEFAULTS = {
|
|
26
|
-
provider_column: :provider,
|
|
27
|
-
model_column: :model,
|
|
28
21
|
data_column: :data,
|
|
29
22
|
format: :string,
|
|
30
|
-
usage_columns: DEFAULT_USAGE_COLUMNS,
|
|
31
23
|
tracer: nil,
|
|
32
|
-
provider:
|
|
24
|
+
provider: nil,
|
|
33
25
|
context: EMPTY_HASH
|
|
34
26
|
}.freeze
|
|
35
27
|
|
|
@@ -78,28 +70,26 @@ module LLM::ActiveRecord
|
|
|
78
70
|
# Maps wrapper options onto the record's storage columns.
|
|
79
71
|
# @return [Hash]
|
|
80
72
|
def self.columns(options)
|
|
81
|
-
usage_columns = options[:usage_columns]
|
|
82
73
|
{
|
|
83
|
-
|
|
84
|
-
model_column: options[:model_column],
|
|
85
|
-
data_column: options[:data_column],
|
|
86
|
-
input_tokens: usage_columns[:input_tokens],
|
|
87
|
-
output_tokens: usage_columns[:output_tokens],
|
|
88
|
-
total_tokens: usage_columns[:total_tokens]
|
|
74
|
+
data_column: options[:data_column]
|
|
89
75
|
}.freeze
|
|
90
76
|
end
|
|
91
77
|
|
|
78
|
+
##
|
|
79
|
+
# Resolves the provider runtime for a record.
|
|
80
|
+
# @return [LLM::Provider]
|
|
81
|
+
def self.resolve_provider(obj, options, empty_hash)
|
|
82
|
+
provider = resolve_option(obj, options[:provider])
|
|
83
|
+
return provider if LLM::Provider === provider
|
|
84
|
+
raise ArgumentError, "provider: must resolve to an LLM::Provider instance"
|
|
85
|
+
end
|
|
86
|
+
|
|
92
87
|
##
|
|
93
88
|
# Persists the runtime state and usage columns back onto the record.
|
|
94
89
|
# @return [void]
|
|
95
90
|
def self.save(obj, ctx, options)
|
|
96
91
|
columns = self.columns(options)
|
|
97
|
-
obj.assign_attributes(
|
|
98
|
-
columns[:data_column] => serialize_context(ctx, options[:format]),
|
|
99
|
-
columns[:input_tokens] => ctx.usage.input_tokens,
|
|
100
|
-
columns[:output_tokens] => ctx.usage.output_tokens,
|
|
101
|
-
columns[:total_tokens] => ctx.usage.total_tokens
|
|
102
|
-
)
|
|
92
|
+
obj.assign_attributes(columns[:data_column] => serialize_context(ctx, options[:format]))
|
|
103
93
|
obj.save!
|
|
104
94
|
end
|
|
105
95
|
end
|
|
@@ -111,8 +101,6 @@ module LLM::ActiveRecord
|
|
|
111
101
|
# @param [Class] model
|
|
112
102
|
# @return [void]
|
|
113
103
|
def self.extended(model)
|
|
114
|
-
options = model.llm_plugin_options
|
|
115
|
-
model.validates options[:provider_column], options[:model_column], presence: true
|
|
116
104
|
model.include InstanceMethods unless model.ancestors.include?(InstanceMethods)
|
|
117
105
|
end
|
|
118
106
|
end
|
|
@@ -128,12 +116,13 @@ module LLM::ActiveRecord
|
|
|
128
116
|
# @option options [Proc, Symbol, LLM::Tracer, nil] :tracer
|
|
129
117
|
# Optional tracer, method name, or proc that resolves to one and is
|
|
130
118
|
# assigned through `llm.tracer = ...` on the resolved provider.
|
|
119
|
+
# @option options [Proc, Symbol, LLM::Provider] :provider
|
|
120
|
+
# Must resolve to an `LLM::Provider` instance for the current record.
|
|
131
121
|
# @return [void]
|
|
132
122
|
def acts_as_llm(options = EMPTY_HASH)
|
|
133
123
|
options = DEFAULTS.merge(options)
|
|
134
|
-
usage_columns = DEFAULT_USAGE_COLUMNS.merge(options[:usage_columns] || EMPTY_HASH)
|
|
135
124
|
class_attribute :llm_plugin_options, instance_accessor: false, default: DEFAULTS unless respond_to?(:llm_plugin_options)
|
|
136
|
-
self.llm_plugin_options = options.
|
|
125
|
+
self.llm_plugin_options = options.freeze
|
|
137
126
|
extend Hooks
|
|
138
127
|
end
|
|
139
128
|
|
|
@@ -228,12 +217,7 @@ module LLM::ActiveRecord
|
|
|
228
217
|
# Returns usage from the mapped usage columns.
|
|
229
218
|
# @return [LLM::Object]
|
|
230
219
|
def usage
|
|
231
|
-
|
|
232
|
-
LLM::Object.from(
|
|
233
|
-
input_tokens: self[columns[:input_tokens]] || 0,
|
|
234
|
-
output_tokens: self[columns[:output_tokens]] || 0,
|
|
235
|
-
total_tokens: self[columns[:total_tokens]] || 0
|
|
236
|
-
)
|
|
220
|
+
ctx.usage || LLM::Object.from(input_tokens: 0, output_tokens: 0, total_tokens: 0)
|
|
237
221
|
end
|
|
238
222
|
|
|
239
223
|
##
|
|
@@ -285,11 +269,8 @@ module LLM::ActiveRecord
|
|
|
285
269
|
# @return [LLM::Provider]
|
|
286
270
|
def llm
|
|
287
271
|
options = self.class.llm_plugin_options
|
|
288
|
-
columns = Utils.columns(options)
|
|
289
|
-
provider = self[columns[:provider_column]]
|
|
290
|
-
kwargs = Utils.resolve_options(self, options[:provider], ActsAsLLM::EMPTY_HASH)
|
|
291
272
|
return @llm if @llm
|
|
292
|
-
@llm =
|
|
273
|
+
@llm = Utils.resolve_provider(self, options, ActsAsLLM::EMPTY_HASH)
|
|
293
274
|
@llm.tracer = Utils.resolve_option(self, options[:tracer]) if options[:tracer]
|
|
294
275
|
@llm
|
|
295
276
|
end
|
|
@@ -303,7 +284,6 @@ module LLM::ActiveRecord
|
|
|
303
284
|
options = self.class.llm_plugin_options
|
|
304
285
|
columns = Utils.columns(options)
|
|
305
286
|
params = Utils.resolve_options(self, options[:context], ActsAsLLM::EMPTY_HASH).dup
|
|
306
|
-
params[:model] ||= self[columns[:model_column]]
|
|
307
287
|
ctx = LLM::Context.new(llm, params.compact)
|
|
308
288
|
data = self[columns[:data_column]]
|
|
309
289
|
if data.nil? || data == ""
|
data/lib/llm/error.rb
CHANGED
|
@@ -63,6 +63,10 @@ module LLM
|
|
|
63
63
|
# When a request is interrupted
|
|
64
64
|
Interrupt = Class.new(Error)
|
|
65
65
|
|
|
66
|
+
##
|
|
67
|
+
# When a concurrency strategy cannot execute a given tool
|
|
68
|
+
RactorError = Class.new(Error)
|
|
69
|
+
|
|
66
70
|
##
|
|
67
71
|
# When a tool call cannot be mapped to a local tool
|
|
68
72
|
NoSuchToolError = Class.new(Error)
|
|
@@ -15,8 +15,12 @@ class LLM::Function
|
|
|
15
15
|
# @param [String, nil] id
|
|
16
16
|
# @param [String] name
|
|
17
17
|
# @param [Hash, Array, nil] arguments
|
|
18
|
+
# @param [LLM::Tracer, nil] tracer
|
|
19
|
+
# @param [Object, nil] span
|
|
18
20
|
# @return [LLM::Function::Ractor::Task]
|
|
19
|
-
def initialize(runner_class, id, name, arguments)
|
|
21
|
+
def initialize(runner_class, id, name, arguments, tracer: nil, span: nil)
|
|
22
|
+
@tracer = tracer
|
|
23
|
+
@span = span
|
|
20
24
|
@mailbox = Ractor::Mailbox.new(build_task(runner_class, id, name, arguments))
|
|
21
25
|
end
|
|
22
26
|
|
|
@@ -37,7 +41,9 @@ class LLM::Function
|
|
|
37
41
|
# @return [LLM::Function::Return]
|
|
38
42
|
def wait
|
|
39
43
|
id, name, value = mailbox.wait
|
|
40
|
-
Return.new(id, name, value)
|
|
44
|
+
result = Return.new(id, name, value)
|
|
45
|
+
@tracer&.on_tool_finish(result:, span: @span)
|
|
46
|
+
result
|
|
41
47
|
end
|
|
42
48
|
alias_method :value, :wait
|
|
43
49
|
|
data/lib/llm/function.rb
CHANGED
|
@@ -228,8 +228,12 @@ class LLM::Function
|
|
|
228
228
|
Fiber.yield
|
|
229
229
|
end.tap(&:resume)
|
|
230
230
|
when :ractor
|
|
231
|
-
raise
|
|
232
|
-
|
|
231
|
+
raise LLM::RactorError, "Ractor concurrency only supports class-based tools" unless Class === @runner
|
|
232
|
+
if @runner.respond_to?(:skill?) && @runner.skill?
|
|
233
|
+
raise LLM::RactorError, "Ractor concurrency does not support skill-backed tools"
|
|
234
|
+
end
|
|
235
|
+
span = @tracer&.on_tool_start(id:, name:, arguments:, model:)
|
|
236
|
+
Ractor::Task.new(@runner, id, name, arguments, tracer: @tracer, span:)
|
|
233
237
|
else
|
|
234
238
|
raise ArgumentError, "Unknown strategy: #{strategy.inspect}. Expected :thread, :task, :fiber, or :ractor"
|
|
235
239
|
end
|
data/lib/llm/sequel/agent.rb
CHANGED
|
@@ -12,7 +12,6 @@ module LLM::Sequel
|
|
|
12
12
|
module Agent
|
|
13
13
|
require_relative "plugin"
|
|
14
14
|
EMPTY_HASH = LLM::Sequel::Plugin::EMPTY_HASH
|
|
15
|
-
DEFAULT_USAGE_COLUMNS = LLM::Sequel::Plugin::DEFAULT_USAGE_COLUMNS
|
|
16
15
|
DEFAULTS = LLM::Sequel::Plugin::DEFAULTS
|
|
17
16
|
Utils = LLM::Sequel::Plugin::Utils
|
|
18
17
|
|
|
@@ -24,11 +23,8 @@ module LLM::Sequel
|
|
|
24
23
|
|
|
25
24
|
def self.configure(model, options = EMPTY_HASH, &block)
|
|
26
25
|
options = DEFAULTS.merge(options)
|
|
27
|
-
|
|
28
|
-
model.instance_variable_set(
|
|
29
|
-
:@llm_agent_options,
|
|
30
|
-
options.merge(usage_columns: usage_columns.freeze).freeze
|
|
31
|
-
)
|
|
26
|
+
model.db.extension :pg_json if %i[json jsonb].include?(options[:format])
|
|
27
|
+
model.instance_variable_set(:@llm_agent_options, options.freeze)
|
|
32
28
|
model.instance_exec(&block) if block
|
|
33
29
|
end
|
|
34
30
|
|
|
@@ -80,7 +76,6 @@ module LLM::Sequel
|
|
|
80
76
|
options = self.class.llm_plugin_options
|
|
81
77
|
columns = Agent::Utils.columns(options)
|
|
82
78
|
params = Agent::Utils.resolve_options(self, options[:context], Agent::EMPTY_HASH).dup
|
|
83
|
-
params[:model] ||= self[columns[:model_column]]
|
|
84
79
|
ctx = self.class.agent.new(llm, params.compact)
|
|
85
80
|
data = self[columns[:data_column]]
|
|
86
81
|
if data.nil? || data == ""
|
data/lib/llm/sequel/plugin.rb
CHANGED
|
@@ -17,11 +17,6 @@ module LLM::Sequel
|
|
|
17
17
|
# can also be configured as symbols that are called on the model.
|
|
18
18
|
module Plugin
|
|
19
19
|
EMPTY_HASH = {}.freeze
|
|
20
|
-
DEFAULT_USAGE_COLUMNS = {
|
|
21
|
-
input_tokens: :input_tokens,
|
|
22
|
-
output_tokens: :output_tokens,
|
|
23
|
-
total_tokens: :total_tokens
|
|
24
|
-
}.freeze
|
|
25
20
|
|
|
26
21
|
##
|
|
27
22
|
# Shared helper methods for the ORM wrapper.
|
|
@@ -68,38 +63,46 @@ module LLM::Sequel
|
|
|
68
63
|
# Maps wrapper options onto the record's storage columns.
|
|
69
64
|
# @return [Hash]
|
|
70
65
|
def self.columns(options)
|
|
71
|
-
usage_columns = options[:usage_columns]
|
|
72
66
|
{
|
|
73
|
-
|
|
74
|
-
model_column: options[:model_column],
|
|
75
|
-
data_column: options[:data_column],
|
|
76
|
-
input_tokens: usage_columns[:input_tokens],
|
|
77
|
-
output_tokens: usage_columns[:output_tokens],
|
|
78
|
-
total_tokens: usage_columns[:total_tokens]
|
|
67
|
+
data_column: options[:data_column]
|
|
79
68
|
}.freeze
|
|
80
69
|
end
|
|
81
70
|
|
|
71
|
+
##
|
|
72
|
+
# Resolves the provider runtime for a record.
|
|
73
|
+
# @return [LLM::Provider]
|
|
74
|
+
def self.resolve_provider(obj, options, empty_hash)
|
|
75
|
+
provider = resolve_option(obj, options[:provider])
|
|
76
|
+
return provider if LLM::Provider === provider
|
|
77
|
+
raise ArgumentError, "provider: must resolve to an LLM::Provider instance"
|
|
78
|
+
end
|
|
79
|
+
|
|
82
80
|
##
|
|
83
81
|
# Persists the runtime state and usage columns back onto the record.
|
|
84
82
|
# @return [void]
|
|
85
83
|
def self.save(obj, ctx, options)
|
|
86
84
|
columns = self.columns(options)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
85
|
+
payload = serialize_context(ctx, options[:format])
|
|
86
|
+
payload = wrap_json_payload(payload, options[:format])
|
|
87
|
+
obj.update(columns[:data_column] => payload)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
##
|
|
91
|
+
# Wraps JSON payloads for Sequel PostgreSQL adapters when needed.
|
|
92
|
+
# @return [Object]
|
|
93
|
+
def self.wrap_json_payload(payload, format)
|
|
94
|
+
case format
|
|
95
|
+
when :json then Sequel.pg_json_wrap(payload)
|
|
96
|
+
when :jsonb then Sequel.pg_jsonb_wrap(payload)
|
|
97
|
+
else payload
|
|
98
|
+
end
|
|
93
99
|
end
|
|
94
100
|
end
|
|
95
101
|
DEFAULTS = {
|
|
96
|
-
provider_column: :provider,
|
|
97
|
-
model_column: :model,
|
|
98
102
|
data_column: :data,
|
|
99
103
|
format: :string,
|
|
100
|
-
usage_columns: DEFAULT_USAGE_COLUMNS,
|
|
101
104
|
tracer: nil,
|
|
102
|
-
provider:
|
|
105
|
+
provider: nil,
|
|
103
106
|
context: EMPTY_HASH
|
|
104
107
|
}.freeze
|
|
105
108
|
|
|
@@ -134,14 +137,13 @@ module LLM::Sequel
|
|
|
134
137
|
# @option options [Proc, Symbol, LLM::Tracer, nil] :tracer
|
|
135
138
|
# Optional tracer, method name, or proc that resolves to one and is
|
|
136
139
|
# assigned through `llm.tracer = ...` on the resolved provider.
|
|
140
|
+
# @option options [Proc, Symbol, LLM::Provider] :provider
|
|
141
|
+
# Must resolve to an `LLM::Provider` instance for the current record.
|
|
137
142
|
# @return [void]
|
|
138
143
|
def self.configure(model, options = EMPTY_HASH)
|
|
139
144
|
options = DEFAULTS.merge(options)
|
|
140
|
-
|
|
141
|
-
model.instance_variable_set(
|
|
142
|
-
:@llm_plugin_options,
|
|
143
|
-
options.merge(usage_columns: usage_columns.freeze).freeze
|
|
144
|
-
)
|
|
145
|
+
model.db.extension :pg_json if %i[json jsonb].include?(options[:format])
|
|
146
|
+
model.instance_variable_set(:@llm_plugin_options, options.freeze)
|
|
145
147
|
end
|
|
146
148
|
end
|
|
147
149
|
|
|
@@ -247,12 +249,7 @@ module LLM::Sequel
|
|
|
247
249
|
# Returns usage from the mapped usage columns.
|
|
248
250
|
# @return [LLM::Object]
|
|
249
251
|
def usage
|
|
250
|
-
|
|
251
|
-
LLM::Object.from(
|
|
252
|
-
input_tokens: self[columns[:input_tokens]] || 0,
|
|
253
|
-
output_tokens: self[columns[:output_tokens]] || 0,
|
|
254
|
-
total_tokens: self[columns[:total_tokens]] || 0
|
|
255
|
-
)
|
|
252
|
+
ctx.usage || LLM::Object.from(input_tokens: 0, output_tokens: 0, total_tokens: 0)
|
|
256
253
|
end
|
|
257
254
|
|
|
258
255
|
##
|
|
@@ -304,11 +301,8 @@ module LLM::Sequel
|
|
|
304
301
|
# @return [LLM::Provider]
|
|
305
302
|
def llm
|
|
306
303
|
options = self.class.llm_plugin_options
|
|
307
|
-
columns = Utils.columns(options)
|
|
308
|
-
provider = self[columns[:provider_column]]
|
|
309
|
-
kwargs = Utils.resolve_options(self, options[:provider], Plugin::EMPTY_HASH)
|
|
310
304
|
return @llm if @llm
|
|
311
|
-
@llm =
|
|
305
|
+
@llm = Utils.resolve_provider(self, options, Plugin::EMPTY_HASH)
|
|
312
306
|
@llm.tracer = Utils.resolve_option(self, options[:tracer]) if options[:tracer]
|
|
313
307
|
@llm
|
|
314
308
|
end
|
|
@@ -322,7 +316,6 @@ module LLM::Sequel
|
|
|
322
316
|
options = self.class.llm_plugin_options
|
|
323
317
|
columns = Utils.columns(options)
|
|
324
318
|
params = Utils.resolve_options(self, options[:context], Plugin::EMPTY_HASH).dup
|
|
325
|
-
params[:model] ||= self[columns[:model_column]]
|
|
326
319
|
ctx = LLM::Context.new(llm, params.compact)
|
|
327
320
|
data = self[columns[:data_column]]
|
|
328
321
|
if data.nil? || data == ""
|
data/lib/llm/skill.rb
CHANGED
data/lib/llm/version.rb
CHANGED
data/llm.gemspec
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: llm.rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 6.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Antar Azri
|
|
@@ -236,6 +236,20 @@ dependencies:
|
|
|
236
236
|
- - "~>"
|
|
237
237
|
- !ruby/object:Gem::Version
|
|
238
238
|
version: '2.0'
|
|
239
|
+
- !ruby/object:Gem::Dependency
|
|
240
|
+
name: pg
|
|
241
|
+
requirement: !ruby/object:Gem::Requirement
|
|
242
|
+
requirements:
|
|
243
|
+
- - "~>"
|
|
244
|
+
- !ruby/object:Gem::Version
|
|
245
|
+
version: '1.5'
|
|
246
|
+
type: :development
|
|
247
|
+
prerelease: false
|
|
248
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
249
|
+
requirements:
|
|
250
|
+
- - "~>"
|
|
251
|
+
- !ruby/object:Gem::Version
|
|
252
|
+
version: '1.5'
|
|
239
253
|
description: |
|
|
240
254
|
llm.rb is a lightweight runtime for building capable AI systems in Ruby.
|
|
241
255
|
It is not just an API wrapper. llm.rb gives you one runtime for providers,
|