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 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
@@ -0,0 +1,3 @@
1
+ MIT License
2
+
3
+ Copyright (c) ...
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,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ require "bundler/setup"
4
+ require "llm/fillin"
5
+ require "pry"
6
+ Pry.start
@@ -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,12 @@
1
+ # frozen_string_literal: true
2
+ require "securerandom"
3
+
4
+ module LLM
5
+ module Fillin
6
+ module Idempotency
7
+ def self.generate(thread_id:)
8
+ "chat-#{thread_id}-#{SecureRandom.hex(6)}"
9
+ end
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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ module LLM
3
+ module Fillin
4
+ VERSION = "0.1.0"
5
+ end
6
+ 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: []