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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 96b04f8669c6e6bf4f176d19b779ca495fb1b2fde034952c48175eb2999647b3
4
- data.tar.gz: 32d2d58018dd35cf63590c889f6c9e5a8bca214e1ccfcd9989c0f90c157bec70
3
+ metadata.gz: 9c9bb739c426cb0b979359a772e7cdcbe84019c303d10c0d1c0b27fe8b8d00c0
4
+ data.tar.gz: 88c343d1a654d19be11b6071658e962be3f64dc477e8be1aa767520c3c7ac13f
5
5
  SHA512:
6
- metadata.gz: e596c18452218540f3a92c7f2542cbf1ea7e5b4fe0314c359e784d14fe331defdaf40f642cd4679667ed0e6ca5c8e42bd171cb940b2b2bd6b3fd47faba41bf97
7
- data.tar.gz: 18e626683d93029d7d29da409b9f456800cf4b0579e9e478636d4b8ff2902f628ef34460cf3dd935b164e76d057bc3b01bb22c3a4b58fe9c15c354befbdc9cf9
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) ![gem](https://img.shields.io/gem/v/omakase-agents?include_prereleases&color=c8452e&label=)
6
+ **[esshka.github.io/omakase](https://esshka.github.io/omakase/)** · [rubygems](https://rubygems.org/gems/omakase-agents) ![gem](https://img.shields.io/gem/v/omakase-agents?color=c8452e&label=)
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 InventoryAgent < ApplicationAgent
14
- instructions "You check inventory."
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 "Units of an item on hand"
17
- def stock_of(item) = STOCK.dig(item, :stock) || 0
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 "Unit price of an item"
20
- def price_of(item) = STOCK.dig(item, :price) || 0.0
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 :can_fulfill_order, "Decide whether the order fits the budget and is in stock." do
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
- InventoryAgent.can_fulfill_order(items: %w[apple banana orange], budget: 5.0)
30
- # => {can_fulfill: false, total_cost: 2.05, unavailable: ["orange"]}
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 --pre
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
- ## Roadmap
361
-
362
- What is not here yet, roughly in the order it would earn its place:
363
-
364
- - [x] **Live objects** the answer is computed in code and handed back as the object, not retyped
365
- as JSON. Done: `finish(value)` plus `returns: SomeClass`.
366
- - [x] **Tracing** RubyLLM emits `chat.ruby_llm` and `tool_call.ruby_llm`; point `config.instrumenter`
367
- at `ActiveSupport::Notifications` and subscribe. Done, by not writing it.
368
- - [ ] **Conversation history** — the chat is fresh per call. Keeping one per agent would let a
369
- method continue where the last one left off, at the cost of deciding what to keep.
370
- - [ ] **Session storage** — persist that history and the agent state so a run can be resumed.
371
- - [x] **MCP tools** external tools over the Model Context Protocol, via `ruby_llm-mcp`. Done:
372
- `mcp :files, …` puts the server's tools on the agent, and generated code calls them.
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
- def of(object)
12
- state = object.instance_variables.map { |name| " #{name} = #{object.instance_variable_get(name).inspect}" }
13
- [object.class.to_s, *signatures(object), *state].join("\n")
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| !CORE.include?(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
@@ -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?
@@ -29,6 +29,8 @@ module Omakase
29
29
 
30
30
  def json = @json ||= definition.new.to_json_schema
31
31
 
32
+ def code_only? = false
33
+
32
34
  # The shape, in the shorthand the model writes back: `{city: <string>}`.
33
35
  def describe
34
36
  return "<#{properties.fetch("result")["type"]}>" if wrapped?
@@ -17,8 +17,11 @@ module Omakase
17
17
 
18
18
  return tool.answer.value if tool.answer
19
19
 
20
- # It never called finish: fall back to a tool-free turn under the schema.
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
- def describe = "a #{@klass}"
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Omakase
4
- VERSION = "0.0.2.alpha"
4
+ VERSION = "0.1.0"
5
5
  end
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.2.alpha
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: 1.3.1
108
+ version: '0'
108
109
  requirements: []
109
110
  rubygems_version: 3.4.10
110
111
  signing_key: