omakase-agents 0.0.2.alpha → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +160 -37
- data/lib/omakase/agent.rb +21 -1
- data/lib/omakase/doc.rb +17 -4
- data/lib/omakase/memory.rb +39 -0
- data/lib/omakase/request.rb +1 -1
- data/lib/omakase/schema.rb +2 -0
- data/lib/omakase/strategies/code_act.rb +5 -2
- data/lib/omakase/type.rb +10 -1
- data/lib/omakase/version.rb +1 -1
- data/lib/omakase.rb +6 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9c9bb739c426cb0b979359a772e7cdcbe84019c303d10c0d1c0b27fe8b8d00c0
|
|
4
|
+
data.tar.gz: 88c343d1a654d19be11b6071658e962be3f64dc477e8be1aa767520c3c7ac13f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8d02d96784acf426a52dc5eb2207d3a426e9beddb1106cd96195941781557019823be03502e9c580253ff249a8dfa7df0d4210e7eace17a6cfaca1d362321151
|
|
7
|
+
data.tar.gz: 6fb9221fb891f4a9b0898b8924ec2705e093acfeba76367c57bde8d56810227da3ea9667036638360515797ceda2bf1d5edd442766f1fb87f44aeeaa75265166
|
data/README.md
CHANGED
|
@@ -3,37 +3,70 @@
|
|
|
3
3
|
A light agent framework — about 700 lines of library. *Omakase* (お任せ): you name what you want,
|
|
4
4
|
the rest is left to the chef.
|
|
5
5
|
|
|
6
|
-
**[esshka.github.io/omakase](https://esshka.github.io/omakase/)** · [rubygems](https://rubygems.org/gems/omakase-agents) ** · [rubygems](https://rubygems.org/gems/omakase-agents) 
|
|
7
7
|
|
|
8
8
|
The whole philosophy: **an agent is an object**. Its fields are state, its methods are what the
|
|
9
9
|
model can call, and the methods it *declares without a body* are written by the model at runtime —
|
|
10
10
|
the method name and prompt are the specification, the schema is the contract.
|
|
11
11
|
|
|
12
12
|
```ruby
|
|
13
|
-
class
|
|
14
|
-
instructions "You
|
|
13
|
+
class RefundAgent < ApplicationAgent
|
|
14
|
+
instructions "You are the refund desk of an online shop. Decide from the customer’s own orders."
|
|
15
15
|
|
|
16
|
-
describe "
|
|
17
|
-
def
|
|
16
|
+
describe "Every order this customer placed, newest first. An Order has placed_on, items and total"
|
|
17
|
+
def orders_for(email) = Order.where(email:).order(placed_on: :desc)
|
|
18
18
|
|
|
19
|
-
describe "
|
|
20
|
-
def
|
|
19
|
+
describe "What the policy says about a topic, such as :damage or :late"
|
|
20
|
+
def policy_on(topic) = POLICY.fetch(topic, "Refunds are allowed within 30 days.")
|
|
21
21
|
|
|
22
|
-
generates :
|
|
23
|
-
boolean :can_fulfill
|
|
24
|
-
number :total_cost
|
|
25
|
-
array :unavailable, of: :string
|
|
26
|
-
end
|
|
22
|
+
generates :decide, "Decide this refund, and name the policy you applied.", returns: Refund
|
|
27
23
|
end
|
|
28
24
|
|
|
29
|
-
|
|
30
|
-
# =>
|
|
25
|
+
RefundAgent.decide(email: "ada@example.com", complaint: "the mug arrived cracked")
|
|
26
|
+
# => #<struct Refund order_id=1, amount=39.9,
|
|
27
|
+
# reason="Mug arrived cracked; damaged goods refunded in full including shipping per policy :damage">
|
|
31
28
|
```
|
|
32
29
|
|
|
30
|
+
`Order` is your ActiveRecord model and `Refund` is your Struct. Nothing was registered anywhere, and
|
|
31
|
+
nothing came back as JSON to parse — the model wrote Ruby against your objects and handed one back.
|
|
32
|
+
Here is the run above, abridged:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
ruby orders = orders_for("ada@example.com")
|
|
36
|
+
orders.each { |o| puts "total: #{o.total}", "items: #{o.items.inspect}" }
|
|
37
|
+
out total: 39.9
|
|
38
|
+
items: [#<Item id: 1, name: "Stoneware mug", price: 34.0>, #<Item id: 2, name: "Shipping", price: 5.9>]
|
|
39
|
+
ruby puts policy_on(:damage)
|
|
40
|
+
out Damaged goods are refunded in full, including shipping, within 90 days.
|
|
41
|
+
ruby finish(Refund.new(order_id: 1, amount: 39.9, reason: "Mug arrived cracked; damaged goods …"))
|
|
42
|
+
out Answer accepted.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
That transcript is real, and it is from `meta/muse-glimmer-30b` on OpenRouter — `:code_act` is developed
|
|
46
|
+
and tested against a 30B model, because a strategy that only works on a frontier model is a demo, not
|
|
47
|
+
a library. [`examples/refund_agent.rb`](examples/refund_agent.rb) is the whole thing, runnable, with
|
|
48
|
+
an in-memory SQLite database.
|
|
49
|
+
|
|
33
50
|
There is no tool abstraction to keep in sync: the model writes Ruby that runs on the agent object,
|
|
34
51
|
reads what it printed and returned, and answers in the declared schema. Adding a tool is adding a
|
|
35
52
|
method; deleting one is deleting a method.
|
|
36
53
|
|
|
54
|
+
## Why this
|
|
55
|
+
|
|
56
|
+
Against **RubyLLM alone**: the tool loop, the schema plumbing, and the correction turn after a bad
|
|
57
|
+
answer are what these 700 lines are. Everything else — providers, keys, models, streaming, tracing —
|
|
58
|
+
is still RubyLLM's, and stays reachable.
|
|
59
|
+
|
|
60
|
+
Against **a framework with a tool registry**: there is nothing to register and nothing to keep in
|
|
61
|
+
sync. The model gets one tool, `ruby`, and reaches the rest through the object. A tool's description
|
|
62
|
+
is `describe`, a line above the method, instead of a JSON schema that drifts from the code it
|
|
63
|
+
describes. The answer comes back as the object the code built, not as JSON you parse again.
|
|
64
|
+
|
|
65
|
+
Reach for something else when the input is untrusted and you want code execution (see
|
|
66
|
+
[Safety](#safety)), when a run is hundreds of steps and must resume mid-flight, when memory means a
|
|
67
|
+
large corpus rather than what one agent learned, or when you want a token stream rather than a
|
|
68
|
+
value.
|
|
69
|
+
|
|
37
70
|
## Installation
|
|
38
71
|
|
|
39
72
|
Ruby 3.2+.
|
|
@@ -42,15 +75,37 @@ Ruby 3.2+.
|
|
|
42
75
|
gem "omakase-agents" # the library is `Omakase`
|
|
43
76
|
```
|
|
44
77
|
|
|
45
|
-
From the command line the flag is needed — `0.0.2.alpha` is a prerelease, and RubyGems skips those
|
|
46
|
-
unless asked. Bundler resolves it without one, since no stable version exists yet.
|
|
47
|
-
|
|
48
78
|
```bash
|
|
49
|
-
gem install omakase-agents
|
|
79
|
+
gem install omakase-agents
|
|
50
80
|
```
|
|
51
81
|
|
|
52
82
|
## Usage
|
|
53
83
|
|
|
84
|
+
The whole API, in one class:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
class MyAgent < Omakase::Agent
|
|
88
|
+
model "claude-sonnet-4-5" # any RubyLLM model and chat option
|
|
89
|
+
instructions "Who the agent is." # the system prompt
|
|
90
|
+
strategy :code_act # or :predict, or anything answering call(request)
|
|
91
|
+
|
|
92
|
+
mcp :files, transport_type: :stdio, config: {} # an MCP server's tools, as methods
|
|
93
|
+
skill "skills/commit-style" # a SKILL.md directory, as one described method
|
|
94
|
+
memory # remember(text) and recall(query)
|
|
95
|
+
|
|
96
|
+
describe "Units of an item on hand" # the docstring Ruby does not have
|
|
97
|
+
def stock_of(item) = ... # any method of yours is a tool
|
|
98
|
+
|
|
99
|
+
generates :answer, "What to produce.", returns: :string # written by the model at runtime
|
|
100
|
+
|
|
101
|
+
def context = "..." # live state, folded into every prompt
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Inside generated code the agent also answers `finish(value)` to return, `doc(object)` to inspect an
|
|
106
|
+
unfamiliar type, and `puts` to say something the model will read back. Outside it, `Marshal.dump`
|
|
107
|
+
is the session.
|
|
108
|
+
|
|
54
109
|
### Declaring an agent
|
|
55
110
|
|
|
56
111
|
```ruby
|
|
@@ -150,7 +205,6 @@ end
|
|
|
150
205
|
The connection opens when the class is defined and the tools are read from the server then, so a
|
|
151
206
|
tool's arguments reach the model as documentation. A failed call raises, which the model sees and
|
|
152
207
|
can correct. Only text comes back: an image or audio result is dropped.
|
|
153
|
-
### Testing
|
|
154
208
|
|
|
155
209
|
### Skills
|
|
156
210
|
|
|
@@ -170,6 +224,74 @@ That is the whole of “loaded on demand”: the one-line description is in the
|
|
|
170
224
|
reaches the model if the generated code calls `commit_style`. Anything else the skill ships —
|
|
171
225
|
scripts, templates — sits in the same directory, and the body ends with its path, so generated Ruby
|
|
172
226
|
can read or run it.
|
|
227
|
+
|
|
228
|
+
### Remembering
|
|
229
|
+
|
|
230
|
+
The chat is fresh on every call — two threads calling one agent must not share a mutable
|
|
231
|
+
conversation. What carries between calls is the object itself: override `context`, and whatever it
|
|
232
|
+
returns is appended to the instructions of the next call.
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
class InterviewAgent < ApplicationAgent
|
|
236
|
+
instructions "You interview a Ruby candidate. One question at a time."
|
|
237
|
+
|
|
238
|
+
def context = @asked.empty? ? nil : "Questions you already asked:\n- #{@asked.join("\n- ")}"
|
|
239
|
+
|
|
240
|
+
generates :question_after, "Ask the next question, on a topic you have not covered yet."
|
|
241
|
+
|
|
242
|
+
def ask(answer) = @asked << question_after(answer:)
|
|
243
|
+
end
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
So “what to keep” is a decision you write in Ruby rather than a policy the library guesses: keep the
|
|
247
|
+
last ten, keep a summary, keep the rows you touched. State is the memory, and it is already typed,
|
|
248
|
+
testable, and yours.
|
|
249
|
+
|
|
250
|
+
### Resuming
|
|
251
|
+
|
|
252
|
+
Because the state is the object, persisting a run is persisting the object — nothing to configure:
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
Redis.current.set("interview:#{id}", Marshal.dump(agent))
|
|
256
|
+
|
|
257
|
+
agent = Marshal.load(Redis.current.get("interview:#{id}"))
|
|
258
|
+
agent.ask("...") # picks up with everything it kept
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
The live chat is left out of the dump and rebuilt on the next call, so a resumed agent holds no
|
|
262
|
+
stale connection. In Rails you usually need none of this: the state came from your models, and the
|
|
263
|
+
agent is rebuilt from those rows per request.
|
|
264
|
+
|
|
265
|
+
A generation in flight is not resumable — the tool loop is RubyLLM's, and a crashed one is retried
|
|
266
|
+
whole, which is what `ActiveJob` does anyway.
|
|
267
|
+
|
|
268
|
+
### Memory
|
|
269
|
+
|
|
270
|
+
`memory` adds two more methods to the agent — one to save something, one to search it by meaning:
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
class SupportAgent < ApplicationAgent
|
|
274
|
+
instructions "You help customers."
|
|
275
|
+
memory
|
|
276
|
+
|
|
277
|
+
generates :answer, "Answer the customer, using what you remember.", returns: :string
|
|
278
|
+
end
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
They are listed with everything else the agent can do, so generated code decides when to reach for
|
|
282
|
+
them — `remember("Shipping to Canada takes three weeks")` on the way out, `recall("delivery time")`
|
|
283
|
+
on the way in. The store is a field, so what the agent learned marshals with it and is there on the
|
|
284
|
+
next run.
|
|
285
|
+
|
|
286
|
+
Embeddings come from RubyLLM (`Omakase.embedder` if you want another source — a fake one keeps
|
|
287
|
+
tests offline), and the search is a dot product over unit vectors. That holds for the few hundred
|
|
288
|
+
things one agent learns about its work; past that it is your database's job — pgvector and the
|
|
289
|
+
[`neighbor`](https://github.com/ankane/neighbor) gem — and `Omakase::Memory` is the interface to
|
|
290
|
+
reimplement against it. And for a few dozen facts, `@notes.grep(/shipping/)` beats every word of
|
|
291
|
+
this.
|
|
292
|
+
|
|
293
|
+
### Testing
|
|
294
|
+
|
|
173
295
|
`Omakase::Agent.new(chat:)` takes any object that quacks like a `RubyLLM::Chat`, and one ships with
|
|
174
296
|
the library, so agents are tested without a network:
|
|
175
297
|
|
|
@@ -317,6 +439,7 @@ generates :plan, strategy: CriticStrategy
|
|
|
317
439
|
lib/omakase/tools/ruby.rb that executor, as a RubyLLM tool, with a call budget
|
|
318
440
|
lib/omakase/mcp.rb an MCP server’s tools, as methods on the agent
|
|
319
441
|
lib/omakase/skills.rb a SKILL.md directory, as one described method
|
|
442
|
+
lib/omakase/memory.rb remember and recall, by meaning
|
|
320
443
|
lib/omakase/fake_chat.rb the stand-in chat for tests
|
|
321
444
|
lib/omakase/strategies/ code_act, predict
|
|
322
445
|
|
|
@@ -328,12 +451,15 @@ Copy `.env.example` to `.env` and fill in a key; `MODEL` and `PROVIDER` there pi
|
|
|
328
451
|
| --- | --- |
|
|
329
452
|
| [`feedback_agent.rb`](examples/feedback_agent.rb) | structured output in one call |
|
|
330
453
|
| [`inventory_agent.rb`](examples/inventory_agent.rb) | the agent's methods as the model's tools |
|
|
454
|
+
| [`refund_agent.rb`](examples/refund_agent.rb) | ActiveRecord objects in, a Ruby object out |
|
|
331
455
|
| [`warehouse_agent.rb`](examples/warehouse_agent.rb) | inspecting objects whose types are unknown |
|
|
332
456
|
| [`support_agent.rb`](examples/support_agent.rb) | plain Ruby orchestrating generated methods |
|
|
333
457
|
| [`support_job.rb`](examples/support_job.rb) | generation off the request thread, via ActiveJob |
|
|
334
458
|
| [`rails_app.rb`](examples/rails_app.rb) | a whole Rails app in one file: initializer, agent, controller |
|
|
335
459
|
| [`mcp_agent.rb`](examples/mcp_agent.rb) | an MCP server's tools as methods on the agent |
|
|
336
460
|
| [`skill_agent.rb`](examples/skill_agent.rb) | a SKILL.md directory the model loads when it needs it |
|
|
461
|
+
| [`interview_agent.rb`](examples/interview_agent.rb) | remembering across calls, without a shared chat |
|
|
462
|
+
| [`memory_agent.rb`](examples/memory_agent.rb) | recall by meaning, kept across a marshalled run |
|
|
337
463
|
|
|
338
464
|
```bash
|
|
339
465
|
bundle exec rake # tests, no network
|
|
@@ -348,6 +474,8 @@ too. Two rules follow:
|
|
|
348
474
|
|
|
349
475
|
- **Untrusted input (anything a user typed) belongs to `:predict`.** No code runs there.
|
|
350
476
|
- **`:code_act` is for work you control** — internal tooling, workers, isolated environments.
|
|
477
|
+
- **A marshalled agent is your data, never user input.** `Marshal.load` on bytes someone else can
|
|
478
|
+
write is remote code execution, resumed run or not.
|
|
351
479
|
|
|
352
480
|
What is bounded: ten tool calls per generation, a 30-second timeout per execution, and 4KB of
|
|
353
481
|
observation. What is not: what the code can reach. For real isolation, swap the executor —
|
|
@@ -357,21 +485,16 @@ anything answering `call(agent, code, timeout:)` will do:
|
|
|
357
485
|
Omakase.executor = MySubprocessExecutor # returns an observation String or Executor::Answer
|
|
358
486
|
```
|
|
359
487
|
|
|
360
|
-
##
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
-
|
|
365
|
-
|
|
366
|
-
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
- [x] **Skills** — capabilities as markdown files with front matter, loaded on demand rather than
|
|
374
|
-
all sitting in the system prompt. Done: `skill "path/to/dir"`, one described method.
|
|
375
|
-
- [ ] **Memory** — recall that survives across sessions, backed by vector search.
|
|
376
|
-
- [x] **Concurrency** — parallel generation calls. Done: output is buffered per thread instead of
|
|
377
|
-
through `$stdout`, so threads no longer collide.
|
|
488
|
+
## Not here, on purpose
|
|
489
|
+
|
|
490
|
+
- **A checkpoint inside a generation.** A crashed run is retried whole. The tool loop belongs to
|
|
491
|
+
RubyLLM, and making it resumable would be a different library.
|
|
492
|
+
- **Reflection and forgetting in memory.** No decay, no consolidation pass: `Memory` grows until you
|
|
493
|
+
prune it, and past a few hundred entries the answer is pgvector, not more code here.
|
|
494
|
+
- **A sandbox.** `instance_eval` runs in your process. Real isolation is a swapped executor, above.
|
|
495
|
+
- **Multi-agent orchestration.** An agent is an object, so one agent calling another is a method
|
|
496
|
+
call. There is nothing to add.
|
|
497
|
+
- **Streaming.** A generation method returns a value, not tokens. RubyLLM streams if you need that.
|
|
498
|
+
|
|
499
|
+
`1.0` lands when the API stops moving. Until then a minor version may move it, and `0.1.0` means
|
|
500
|
+
the library is usable, not that it is finished.
|
data/lib/omakase/agent.rb
CHANGED
|
@@ -37,6 +37,15 @@ module Omakase
|
|
|
37
37
|
# A skill directory — a SKILL.md with YAML front matter. Its description
|
|
38
38
|
# joins the agent's capabilities; its body arrives when the model asks.
|
|
39
39
|
def skill(path) = Skills.attach(self, path)
|
|
40
|
+
|
|
41
|
+
# Two more methods: one to save something, one to search it by meaning.
|
|
42
|
+
# The store is a field, so it marshals with the agent and outlives the run.
|
|
43
|
+
def memory
|
|
44
|
+
describe "Save something worth remembering after this run"
|
|
45
|
+
define_method(:remember) { |text| (@memory ||= Memory.new).remember(text) }
|
|
46
|
+
describe "Search what you remember, by meaning; the closest few come back"
|
|
47
|
+
define_method(:recall) { |query, limit: 5| (@memory ||= Memory.new).recall(query, limit:) }
|
|
48
|
+
end
|
|
40
49
|
# Documents the method defined next — the docstring Ruby does not have.
|
|
41
50
|
def describe(text)
|
|
42
51
|
@pending_description = text
|
|
@@ -88,9 +97,20 @@ module Omakase
|
|
|
88
97
|
@chat = chat
|
|
89
98
|
end
|
|
90
99
|
|
|
91
|
-
# A fresh conversation per call
|
|
100
|
+
# A fresh conversation per call — two threads calling one agent must not
|
|
101
|
+
# share a mutable chat. What carries between calls is the object's own state.
|
|
92
102
|
def chat = @chat || RubyLLM.chat(**self.class.chat_options)
|
|
93
103
|
|
|
104
|
+
# That state, as the model should read it: rebuilt on every call, and added
|
|
105
|
+
# to the class's instructions. Override it to remember anything.
|
|
106
|
+
def context = nil
|
|
107
|
+
|
|
108
|
+
# Resuming a run is loading the object back, so an agent marshals like any
|
|
109
|
+
# other Ruby object — minus the live chat, which is rebuilt on demand.
|
|
110
|
+
def marshal_dump = (instance_variables - [:@chat]).to_h { |name| [name, instance_variable_get(name)] }
|
|
111
|
+
|
|
112
|
+
def marshal_load(state) = state.each { |name, value| instance_variable_set(name, value) }
|
|
113
|
+
|
|
94
114
|
# For generated code meeting an object whose type it does not know.
|
|
95
115
|
def doc(object) = puts(Doc.of(object))
|
|
96
116
|
|
data/lib/omakase/doc.rb
CHANGED
|
@@ -8,16 +8,29 @@ module Omakase
|
|
|
8
8
|
|
|
9
9
|
module_function
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
# A framework's base class defines hundreds of methods the model has no use
|
|
12
|
+
# for. What it wants is what this class adds — its columns, its associations,
|
|
13
|
+
# and the methods you wrote.
|
|
14
|
+
def boundary = defined?(ActiveRecord::Base) ? [*CORE, ActiveRecord::Base] : CORE
|
|
15
|
+
|
|
16
|
+
def of(object) = [object.class.to_s, *signatures(object), *state(object)].join("\n")
|
|
17
|
+
|
|
18
|
+
# An object that answers `attributes` says what it holds better than its
|
|
19
|
+
# instance variables do — and a record's columns are state, not API.
|
|
20
|
+
def state(object)
|
|
21
|
+
values = object.respond_to?(:attributes) ? object.attributes : ivars(object)
|
|
22
|
+
values.map { |name, value| " #{name} = #{value.inspect}" }
|
|
14
23
|
end
|
|
15
24
|
|
|
25
|
+
def ivars(object) = object.instance_variables.to_h { |name| [name, object.instance_variable_get(name)] }
|
|
26
|
+
|
|
16
27
|
def signatures(object)
|
|
17
28
|
object.class.ancestors
|
|
18
|
-
.take_while { |mod| !
|
|
29
|
+
.take_while { |mod| !boundary.include?(mod) }
|
|
30
|
+
.reject { |mod| mod.to_s.end_with?("GeneratedAttributeMethods") }
|
|
19
31
|
.flat_map { |mod| mod.public_instance_methods(false) }
|
|
20
32
|
.uniq.sort
|
|
33
|
+
.reject { |name| name.match?(/\A_|_associated_records_for_/) }
|
|
21
34
|
.map { |name| " #{name}(#{Capabilities.parameters(object.method(name))})" }
|
|
22
35
|
end
|
|
23
36
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Omakase
|
|
4
|
+
# Recall by meaning rather than by key: text goes in, the closest of it comes
|
|
5
|
+
# back out. The embeddings are RubyLLM's and the search is a dot product over
|
|
6
|
+
# an array — enough for the few hundred things one agent learns about its
|
|
7
|
+
# work. Past that it is your database's job (pgvector, the `neighbor` gem),
|
|
8
|
+
# and this is the interface to reimplement against it.
|
|
9
|
+
class Memory
|
|
10
|
+
def initialize = @entries = {}
|
|
11
|
+
|
|
12
|
+
# Keyed by the text, so remembering the same thing twice costs one entry.
|
|
13
|
+
def remember(text)
|
|
14
|
+
@entries[text] ||= unit(Omakase.embedder.call(text))
|
|
15
|
+
text
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def recall(query, limit: 5)
|
|
19
|
+
return [] if empty?
|
|
20
|
+
|
|
21
|
+
vector = unit(Omakase.embedder.call(query))
|
|
22
|
+
@entries.max_by(limit) { |_, remembered| dot(vector, remembered) }.map(&:first)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def size = @entries.size
|
|
26
|
+
|
|
27
|
+
def empty? = @entries.empty?
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def dot(one, other) = one.zip(other).sum { |a, b| a * b }
|
|
32
|
+
|
|
33
|
+
# Unit vectors, so the dot product is the cosine and lengths cannot skew it.
|
|
34
|
+
def unit(vector)
|
|
35
|
+
norm = Math.sqrt(vector.sum { |value| value * value })
|
|
36
|
+
norm.zero? ? vector : vector.map { |value| value / norm }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/omakase/request.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Omakase
|
|
|
7
7
|
|
|
8
8
|
def schema = generation.schema
|
|
9
9
|
|
|
10
|
-
def instructions = agent.class.instructions
|
|
10
|
+
def instructions = [agent.class.instructions, agent.context].reject { |text| text.to_s.empty? }.join("\n\n")
|
|
11
11
|
|
|
12
12
|
def task
|
|
13
13
|
return generation.prompt if inputs.empty?
|
data/lib/omakase/schema.rb
CHANGED
|
@@ -17,8 +17,11 @@ module Omakase
|
|
|
17
17
|
|
|
18
18
|
return tool.answer.value if tool.answer
|
|
19
19
|
|
|
20
|
-
# It never called finish
|
|
21
|
-
Predict.call(request, task: "#{request.task}\n\nWork done:\n#{notes}")
|
|
20
|
+
# It never called finish. A JSON answer can still be given in a tool-free turn.
|
|
21
|
+
return Predict.call(request, task: "#{request.task}\n\nWork done:\n#{notes}") unless request.schema.code_only?
|
|
22
|
+
|
|
23
|
+
# An object cannot come back as JSON, so there is nowhere to fall back to.
|
|
24
|
+
raise ContractError, "#{request.generation.name}: the model never called finish(#{request.schema.describe})"
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
def instructions(request)
|
data/lib/omakase/type.rb
CHANGED
|
@@ -8,7 +8,16 @@ module Omakase
|
|
|
8
8
|
@klass = klass
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
# A Struct or Data says what it holds, and the model needs that to build one:
|
|
12
|
+
# `Refund.new(order_id:, amount:, reason:)` beats `a Refund`.
|
|
13
|
+
def describe
|
|
14
|
+
return "a #{@klass}" unless @klass.respond_to?(:members)
|
|
15
|
+
|
|
16
|
+
"#{@klass}.new(#{@klass.members.map { |name| "#{name}:" }.join(", ")})"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Only code can build it, so :predict is not a fallback.
|
|
20
|
+
def code_only? = true
|
|
12
21
|
|
|
13
22
|
def take(value)
|
|
14
23
|
return value if value.is_a?(@klass)
|
data/lib/omakase/version.rb
CHANGED
data/lib/omakase.rb
CHANGED
|
@@ -49,5 +49,11 @@ module Omakase
|
|
|
49
49
|
attr_writer :executor
|
|
50
50
|
|
|
51
51
|
def executor = @executor ||= Executor
|
|
52
|
+
|
|
53
|
+
# How text becomes a vector, for Memory. Anything answering `call(text)`
|
|
54
|
+
# will do; the model and its provider are RubyLLM's to configure.
|
|
55
|
+
attr_writer :embedder
|
|
56
|
+
|
|
57
|
+
def embedder = @embedder ||= ->(text) { RubyLLM.embed(text).vectors }
|
|
52
58
|
end
|
|
53
59
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: omakase-agents
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0
|
|
4
|
+
version: 0.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- eugeny
|
|
@@ -74,6 +74,7 @@ files:
|
|
|
74
74
|
- lib/omakase/fake_chat.rb
|
|
75
75
|
- lib/omakase/generation.rb
|
|
76
76
|
- lib/omakase/mcp.rb
|
|
77
|
+
- lib/omakase/memory.rb
|
|
77
78
|
- lib/omakase/request.rb
|
|
78
79
|
- lib/omakase/schema.rb
|
|
79
80
|
- lib/omakase/skills.rb
|
|
@@ -102,9 +103,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
102
103
|
version: '3.2'
|
|
103
104
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
104
105
|
requirements:
|
|
105
|
-
- - "
|
|
106
|
+
- - ">="
|
|
106
107
|
- !ruby/object:Gem::Version
|
|
107
|
-
version:
|
|
108
|
+
version: '0'
|
|
108
109
|
requirements: []
|
|
109
110
|
rubygems_version: 3.4.10
|
|
110
111
|
signing_key:
|