llm-fillin 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 +7 -0
- data/LICENSE +3 -0
- data/README.md +32 -0
- data/bin/console +6 -0
- data/examples/run_create_toy.rb +41 -0
- data/lib/llm/fillin/adapters/openai_adapter.rb +33 -0
- data/lib/llm/fillin/adapters/store_memory.rb +12 -0
- data/lib/llm/fillin/idempotency.rb +12 -0
- data/lib/llm/fillin/orchestrator.rb +52 -0
- data/lib/llm/fillin/registry.rb +37 -0
- data/lib/llm/fillin/toys.rb +38 -0
- data/lib/llm/fillin/validators.rb +14 -0
- data/lib/llm/fillin/version.rb +6 -0
- data/lib/llm/fillin.rb +8 -0
- metadata +86 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1167e6d823abb66aaa8e4cfd2f0a5ba2cb969dfe2ebc88f5a9931e5cf641b846
|
|
4
|
+
data.tar.gz: 1419c96c1c60df093cf8175cb1fa0e4098460f5528e3ed43097effd7426f4b1a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: cd888ae3ec1245f5201c5509b3d08ce234927c5b91e61a46b10fd52739d4bfb314657a289da336b4d01fedebdeb3166ec4c036e75fb67d48e7b40be21f336e92
|
|
7
|
+
data.tar.gz: 4cdb05933e2f97aae0bf296f44f11abf2ac77612a57d8784c959fe184e8038caae4348c009d4ca1979c2294c2bffba9c0909333a0e345518be955049ebc5e425
|
data/LICENSE
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# llm-fillin
|
|
2
|
+
|
|
3
|
+
**LLM-powered slot filling + tool orchestration for Ruby.**
|
|
4
|
+
Register JSON-schema tools, let an LLM ask for missing fields, then call your handlers safely.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
```bash
|
|
8
|
+
bundle install
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Configure
|
|
12
|
+
Set your OpenAI API key:
|
|
13
|
+
```bash
|
|
14
|
+
export OPENAI_API_KEY=sk-...
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Run the demo
|
|
18
|
+
```bash
|
|
19
|
+
ruby examples/run_create_toy.rb
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Try:
|
|
23
|
+
```
|
|
24
|
+
I want a red race car toy for $12
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The assistant will ask for any missing fields (like category) and then “create” the toy.
|
|
28
|
+
|
|
29
|
+
## Use in your app
|
|
30
|
+
- Register tools (schemas + handlers)
|
|
31
|
+
- Call the Orchestrator with your message list
|
|
32
|
+
- Validate server-side; enforce tenant/RBAC; generate idempotency for creates
|
data/bin/console
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
3
|
+
|
|
4
|
+
require "llm/fillin"
|
|
5
|
+
require "llm/fillin/toys"
|
|
6
|
+
|
|
7
|
+
registry = LLM::Fillin::Registry.new
|
|
8
|
+
LLM::Fillin.register_toy_tools!(registry)
|
|
9
|
+
|
|
10
|
+
adapter = LLM::Fillin::OpenAIAdapter.new(
|
|
11
|
+
api_key: ENV.fetch("OPENAI_API_KEY"),
|
|
12
|
+
model: "gpt-4.1-mini",
|
|
13
|
+
temperature: 0
|
|
14
|
+
)
|
|
15
|
+
store = LLM::Fillin::StoreMemory.new
|
|
16
|
+
orch = LLM::Fillin::Orchestrator.new(adapter: adapter, registry: registry, store: store)
|
|
17
|
+
|
|
18
|
+
thread_id = "demo-123"
|
|
19
|
+
tenant_id = "org_123"
|
|
20
|
+
actor_id = "user_42"
|
|
21
|
+
|
|
22
|
+
messages = [
|
|
23
|
+
{ role: "user", content: "I want a red race car toy for $12" }
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
loop do
|
|
27
|
+
outcome = orch.step(thread_id: thread_id, tenant_id: tenant_id, actor_id: actor_id, messages: messages)
|
|
28
|
+
|
|
29
|
+
case outcome[:type]
|
|
30
|
+
when :assistant
|
|
31
|
+
puts "AI: #{outcome[:text]}"
|
|
32
|
+
print "YOU: "
|
|
33
|
+
input = STDIN.gets&.strip
|
|
34
|
+
break unless input
|
|
35
|
+
messages << { role: "user", content: input }
|
|
36
|
+
when :tool_ran
|
|
37
|
+
toy = outcome[:result]
|
|
38
|
+
puts "✅ Toy created: #{toy[:name]} (#{toy[:category]}, #{toy[:color]}) - $#{toy[:price_minor].to_i/100.0} | ID #{toy[:id]}"
|
|
39
|
+
break
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "openai"
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module LLM
|
|
6
|
+
module Fillin
|
|
7
|
+
class OpenAIAdapter
|
|
8
|
+
def initialize(api_key:, model:, temperature: 0)
|
|
9
|
+
@client = OpenAI::Client.new(access_token: api_key)
|
|
10
|
+
@model = model
|
|
11
|
+
@temperature = temperature
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def step(system_prompt:, messages:, tools:, tool_results: [])
|
|
15
|
+
resp = @client.chat(parameters: {
|
|
16
|
+
model: @model,
|
|
17
|
+
temperature: @temperature,
|
|
18
|
+
tools: tools,
|
|
19
|
+
tool_choice: "auto",
|
|
20
|
+
messages: [{ role: "system", content: system_prompt }] +
|
|
21
|
+
messages +
|
|
22
|
+
tool_results
|
|
23
|
+
})
|
|
24
|
+
msg = resp.dig("choices", 0, "message")
|
|
25
|
+
{ tool_calls: msg["tool_calls"], content: msg["content"] }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def tool_result_message(tool_call_id:, name:, content:)
|
|
29
|
+
{ role: "tool", tool_call_id:, name:, content: content.to_json }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module LLM
|
|
3
|
+
module Fillin
|
|
4
|
+
class StoreMemory
|
|
5
|
+
def initialize
|
|
6
|
+
@tool_msgs_by_thread = Hash.new { |h,k| h[k] = [] }
|
|
7
|
+
end
|
|
8
|
+
def fetch_tool_messages(thread_id) = @tool_msgs_by_thread[thread_id]
|
|
9
|
+
def push_tool_message(thread_id, msg) = @tool_msgs_by_thread[thread_id] << msg
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module LLM
|
|
5
|
+
module Fillin
|
|
6
|
+
class Orchestrator
|
|
7
|
+
POLICY = <<~SYS
|
|
8
|
+
You are a task-oriented assistant. Identify intent, extract entities,
|
|
9
|
+
ask for missing required fields one at a time, and call exactly one function when ready.
|
|
10
|
+
Keep replies concise and friendly.
|
|
11
|
+
SYS
|
|
12
|
+
|
|
13
|
+
def initialize(adapter:, registry:, store:)
|
|
14
|
+
@adapter, @registry, @store = adapter, registry, store
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# messages: [{role:"user", content:"..."}]
|
|
18
|
+
def step(thread_id:, tenant_id:, actor_id:, messages:)
|
|
19
|
+
prior_tool_msgs = @store.fetch_tool_messages(thread_id)
|
|
20
|
+
res = @adapter.step(
|
|
21
|
+
system_prompt: POLICY,
|
|
22
|
+
messages: messages,
|
|
23
|
+
tools: @registry.tools_for_llm,
|
|
24
|
+
tool_results: prior_tool_msgs
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if (calls = res[:tool_calls]).is_a?(Array) && calls.any?
|
|
28
|
+
call = calls.first
|
|
29
|
+
name, version = call.dig("function", "name").split(/_v/i)
|
|
30
|
+
args = JSON.parse(call.dig("function", "arguments") || "{}")
|
|
31
|
+
|
|
32
|
+
tool = @registry.tool(name, version: "v1")
|
|
33
|
+
Validators.validate!(tool.schema, args)
|
|
34
|
+
|
|
35
|
+
ctx = { tenant_id:, actor_id:, thread_id: }
|
|
36
|
+
result = tool.handler.call(args, ctx)
|
|
37
|
+
|
|
38
|
+
tool_msg = @adapter.tool_result_message(
|
|
39
|
+
tool_call_id: call["id"],
|
|
40
|
+
name: "#{name}_v1",
|
|
41
|
+
content: result
|
|
42
|
+
)
|
|
43
|
+
@store.push_tool_message(thread_id, tool_msg)
|
|
44
|
+
|
|
45
|
+
{ type: :tool_ran, tool_name: name, result: result }
|
|
46
|
+
else
|
|
47
|
+
{ type: :assistant, text: res[:content].to_s }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module LLM
|
|
3
|
+
module Fillin
|
|
4
|
+
Tool = Struct.new(:name, :version, :schema, :description, :handler, keyword_init: true)
|
|
5
|
+
|
|
6
|
+
class Registry
|
|
7
|
+
def initialize
|
|
8
|
+
@tools = {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def register!(name:, version:, schema:, description:, handler:)
|
|
12
|
+
@tools[key_for(name, version)] = Tool.new(name:, version:, schema:, description:, handler:)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def tool(name, version: "v1")
|
|
16
|
+
@tools.fetch(key_for(name, version))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def tools_for_llm
|
|
20
|
+
@tools.values.map do |t|
|
|
21
|
+
{
|
|
22
|
+
type: "function",
|
|
23
|
+
function: {
|
|
24
|
+
name: "#{t.name}_#{t.version}",
|
|
25
|
+
description: t.description,
|
|
26
|
+
parameters: t.schema
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def key_for(name, version) = "#{name}:#{version}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "securerandom"
|
|
3
|
+
|
|
4
|
+
module LLM
|
|
5
|
+
module Fillin
|
|
6
|
+
CREATE_TOY_V1 = {
|
|
7
|
+
type: "object", additionalProperties: false,
|
|
8
|
+
properties: {
|
|
9
|
+
name: { type: "string", description: "Toy name" },
|
|
10
|
+
category: { type: "string", enum: %w[plush puzzle doll car lego other] },
|
|
11
|
+
price_minor: { type: "integer", minimum: 0, description: "Price in cents" },
|
|
12
|
+
color: { type: "string", description: "Primary color" }
|
|
13
|
+
},
|
|
14
|
+
required: %w[name category price_minor]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
def self.register_toy_tools!(registry)
|
|
18
|
+
registry.register!(
|
|
19
|
+
name: "create_toy", version: "v1",
|
|
20
|
+
schema: CREATE_TOY_V1,
|
|
21
|
+
description: "Create a toy with name, category, price (in cents), and optional color.",
|
|
22
|
+
handler: ->(args, ctx) {
|
|
23
|
+
key = Idempotency.generate(thread_id: ctx[:thread_id])
|
|
24
|
+
{
|
|
25
|
+
id: "TOY-#{SecureRandom.hex(3).upcase}",
|
|
26
|
+
name: args["name"],
|
|
27
|
+
category: args["category"],
|
|
28
|
+
price_minor: args["price_minor"],
|
|
29
|
+
color: args["color"] || "unspecified",
|
|
30
|
+
created_by: ctx[:actor_id],
|
|
31
|
+
tenant: ctx[:tenant_id],
|
|
32
|
+
idempotency_key: key
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "json_schemer"
|
|
3
|
+
|
|
4
|
+
module LLM
|
|
5
|
+
module Fillin
|
|
6
|
+
class Validators
|
|
7
|
+
def self.validate!(schema, args)
|
|
8
|
+
schemer = JSONSchemer.schema(schema)
|
|
9
|
+
errors = schemer.validate(args).to_a
|
|
10
|
+
raise ArgumentError, "Schema validation failed: #{errors}" if errors.any?
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/llm/fillin.rb
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative "fillin/version"
|
|
3
|
+
require_relative "fillin/registry"
|
|
4
|
+
require_relative "fillin/orchestrator"
|
|
5
|
+
require_relative "fillin/validators"
|
|
6
|
+
require_relative "fillin/idempotency"
|
|
7
|
+
require_relative "fillin/adapters/openai_adapter"
|
|
8
|
+
require_relative "fillin/adapters/store_memory"
|
metadata
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: llm-fillin
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Phia Vang
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-09-03 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: openai
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.21'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.21'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: json_schemer
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '2.3'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '2.3'
|
|
41
|
+
description: Register JSON-schema tools and let an LLM handle intent, slot-filling,
|
|
42
|
+
and tool calls safely.
|
|
43
|
+
email:
|
|
44
|
+
- pnvang@gmail.com
|
|
45
|
+
executables:
|
|
46
|
+
- console
|
|
47
|
+
extensions: []
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- LICENSE
|
|
51
|
+
- README.md
|
|
52
|
+
- bin/console
|
|
53
|
+
- examples/run_create_toy.rb
|
|
54
|
+
- lib/llm/fillin.rb
|
|
55
|
+
- lib/llm/fillin/adapters/openai_adapter.rb
|
|
56
|
+
- lib/llm/fillin/adapters/store_memory.rb
|
|
57
|
+
- lib/llm/fillin/idempotency.rb
|
|
58
|
+
- lib/llm/fillin/orchestrator.rb
|
|
59
|
+
- lib/llm/fillin/registry.rb
|
|
60
|
+
- lib/llm/fillin/toys.rb
|
|
61
|
+
- lib/llm/fillin/validators.rb
|
|
62
|
+
- lib/llm/fillin/version.rb
|
|
63
|
+
homepage: https://github.com/pnvang/llm-fillin
|
|
64
|
+
licenses:
|
|
65
|
+
- MIT
|
|
66
|
+
metadata: {}
|
|
67
|
+
post_install_message:
|
|
68
|
+
rdoc_options: []
|
|
69
|
+
require_paths:
|
|
70
|
+
- lib
|
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '3.1'
|
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '0'
|
|
81
|
+
requirements: []
|
|
82
|
+
rubygems_version: 3.5.3
|
|
83
|
+
signing_key:
|
|
84
|
+
specification_version: 4
|
|
85
|
+
summary: LLM-powered slot filling and tool orchestration for Ruby.
|
|
86
|
+
test_files: []
|