ruby-pi 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.
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/ruby_pi/llm/response.rb
4
+ #
5
+ # Represents a unified response from any LLM provider. Normalizes the varied
6
+ # response formats of Gemini, Anthropic, and OpenAI into a single, consistent
7
+ # structure that calling code can depend on regardless of provider.
8
+
9
+ module RubyPi
10
+ module LLM
11
+ # A normalized response object returned by all LLM providers after a
12
+ # completion request. Encapsulates the generated text content, any tool
13
+ # calls the model wants to invoke, token usage statistics, and the reason
14
+ # the model stopped generating.
15
+ #
16
+ # @example Accessing response data
17
+ # response = provider.complete(messages: messages)
18
+ # puts response.content
19
+ # response.tool_calls.each { |tc| handle_tool(tc) }
20
+ # puts "Tokens used: #{response.usage[:total_tokens]}"
21
+ class Response
22
+ # @return [String, nil] the generated text content from the model
23
+ attr_reader :content
24
+
25
+ # @return [Array<RubyPi::LLM::ToolCall>] tool calls the model wants to invoke
26
+ attr_reader :tool_calls
27
+
28
+ # @return [Hash] token usage statistics with keys like :prompt_tokens,
29
+ # :completion_tokens, :total_tokens
30
+ attr_reader :usage
31
+
32
+ # @return [String, nil] the reason the model stopped generating
33
+ # (e.g., "stop", "tool_calls", "max_tokens")
34
+ attr_reader :finish_reason
35
+
36
+ # Creates a new Response instance.
37
+ #
38
+ # @param content [String, nil] the generated text content
39
+ # @param tool_calls [Array<RubyPi::LLM::ToolCall>] list of tool invocations
40
+ # @param usage [Hash] token usage statistics
41
+ # @param finish_reason [String, nil] why the model stopped generating
42
+ def initialize(content: nil, tool_calls: [], usage: {}, finish_reason: nil)
43
+ @content = content
44
+ @tool_calls = Array(tool_calls)
45
+ @usage = usage
46
+ @finish_reason = finish_reason
47
+ end
48
+
49
+ # Returns true if the response includes one or more tool calls.
50
+ #
51
+ # @return [Boolean]
52
+ def tool_calls?
53
+ !@tool_calls.empty?
54
+ end
55
+
56
+ # Returns a hash representation of the response for serialization.
57
+ #
58
+ # @return [Hash] the response as a plain hash
59
+ def to_h
60
+ {
61
+ content: @content,
62
+ tool_calls: @tool_calls.map(&:to_h),
63
+ usage: @usage,
64
+ finish_reason: @finish_reason
65
+ }
66
+ end
67
+
68
+ # Returns a human-readable string representation of the response.
69
+ #
70
+ # @return [String]
71
+ def to_s
72
+ parts = []
73
+ parts << "content=#{@content.inspect}" if @content
74
+ parts << "tool_calls=#{@tool_calls.length}" if tool_calls?
75
+ parts << "finish_reason=#{@finish_reason}" if @finish_reason
76
+ "#<RubyPi::LLM::Response #{parts.join(', ')}>"
77
+ end
78
+
79
+ alias_method :inspect, :to_s
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/ruby_pi/llm/stream_event.rb
4
+ #
5
+ # Represents a single event in a streaming LLM response. When streaming is
6
+ # enabled, the provider yields a sequence of StreamEvent objects to the caller's
7
+ # block, allowing incremental processing of text deltas and tool call fragments.
8
+
9
+ module RubyPi
10
+ module LLM
11
+ # An event yielded during a streaming completion. Events carry a type
12
+ # indicating what kind of data they contain and a data payload with
13
+ # the actual content.
14
+ #
15
+ # @example Processing streaming events
16
+ # provider.complete(messages: msgs, stream: true) do |event|
17
+ # case event.type
18
+ # when :text_delta
19
+ # print event.data
20
+ # when :tool_call_delta
21
+ # accumulate_tool_call(event.data)
22
+ # when :done
23
+ # puts "\nStream complete"
24
+ # end
25
+ # end
26
+ class StreamEvent
27
+ # Valid event types for stream events.
28
+ VALID_TYPES = %i[text_delta tool_call_delta done].freeze
29
+
30
+ # @return [Symbol] the type of stream event — one of :text_delta,
31
+ # :tool_call_delta, or :done
32
+ attr_reader :type
33
+
34
+ # @return [Object] the event payload. For :text_delta this is a String
35
+ # fragment; for :tool_call_delta it is a Hash with partial tool call
36
+ # data; for :done it is nil or a final summary hash.
37
+ attr_reader :data
38
+
39
+ # Creates a new StreamEvent instance.
40
+ #
41
+ # @param type [Symbol] event type (:text_delta, :tool_call_delta, :done)
42
+ # @param data [Object] event payload
43
+ # @raise [ArgumentError] if the type is not recognized
44
+ def initialize(type:, data: nil)
45
+ unless VALID_TYPES.include?(type)
46
+ raise ArgumentError, "Invalid stream event type: #{type.inspect}. Must be one of: #{VALID_TYPES.join(', ')}"
47
+ end
48
+
49
+ @type = type
50
+ @data = data
51
+ end
52
+
53
+ # Returns true if this is a text delta event.
54
+ #
55
+ # @return [Boolean]
56
+ def text_delta?
57
+ @type == :text_delta
58
+ end
59
+
60
+ # Returns true if this is a tool call delta event.
61
+ #
62
+ # @return [Boolean]
63
+ def tool_call_delta?
64
+ @type == :tool_call_delta
65
+ end
66
+
67
+ # Returns true if this is a done/completion event.
68
+ #
69
+ # @return [Boolean]
70
+ def done?
71
+ @type == :done
72
+ end
73
+
74
+ # Returns a hash representation of the stream event.
75
+ #
76
+ # @return [Hash]
77
+ def to_h
78
+ { type: @type, data: @data }
79
+ end
80
+
81
+ # Returns a human-readable string representation.
82
+ #
83
+ # @return [String]
84
+ def to_s
85
+ "#<RubyPi::LLM::StreamEvent type=#{@type.inspect} data=#{@data.inspect}>"
86
+ end
87
+
88
+ alias_method :inspect, :to_s
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/ruby_pi/llm/tool_call.rb
4
+ #
5
+ # Represents a tool (function) call requested by the LLM. When the model
6
+ # decides to invoke a tool, it returns one or more ToolCall objects describing
7
+ # which function to call and with what arguments.
8
+
9
+ module RubyPi
10
+ module LLM
11
+ # A tool call extracted from an LLM response. Contains the unique call ID,
12
+ # the function name, and the parsed arguments hash. Provider-specific
13
+ # formats are normalized into this common structure.
14
+ #
15
+ # @example Handling a tool call
16
+ # response.tool_calls.each do |tool_call|
17
+ # result = dispatch(tool_call.name, tool_call.arguments)
18
+ # # Feed result back into conversation
19
+ # end
20
+ class ToolCall
21
+ # @return [String] unique identifier for this tool call, used to match
22
+ # results back to the calling context
23
+ attr_reader :id
24
+
25
+ # @return [String] the name of the tool/function to invoke
26
+ attr_reader :name
27
+
28
+ # @return [Hash] the parsed arguments to pass to the tool
29
+ attr_reader :arguments
30
+
31
+ # Creates a new ToolCall instance.
32
+ #
33
+ # @param id [String] unique call identifier
34
+ # @param name [String] tool/function name
35
+ # @param arguments [Hash] parsed arguments hash
36
+ def initialize(id:, name:, arguments: {})
37
+ @id = id
38
+ @name = name
39
+ @arguments = arguments.is_a?(Hash) ? arguments : parse_arguments(arguments)
40
+ end
41
+
42
+ # Returns a hash representation of the tool call for serialization.
43
+ #
44
+ # @return [Hash]
45
+ def to_h
46
+ {
47
+ id: @id,
48
+ name: @name,
49
+ arguments: @arguments
50
+ }
51
+ end
52
+
53
+ # Returns a human-readable string representation of the tool call.
54
+ #
55
+ # @return [String]
56
+ def to_s
57
+ "#<RubyPi::LLM::ToolCall id=#{@id.inspect} name=#{@name.inspect} arguments=#{@arguments.inspect}>"
58
+ end
59
+
60
+ alias_method :inspect, :to_s
61
+
62
+ private
63
+
64
+ # Attempts to parse a JSON string into a Hash. Falls back to wrapping
65
+ # the raw value in a hash if parsing fails.
66
+ #
67
+ # @param raw [String, Object] raw arguments data
68
+ # @return [Hash] parsed arguments
69
+ def parse_arguments(raw)
70
+ return {} if raw.nil? || raw.empty?
71
+
72
+ JSON.parse(raw.to_s)
73
+ rescue JSON::ParserError
74
+ { "_raw" => raw.to_s }
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/ruby_pi/tools/definition.rb
4
+ #
5
+ # RubyPi::Tools::Definition — Describes a callable tool with its metadata.
6
+ #
7
+ # A Definition encapsulates everything needed to declare and invoke a tool:
8
+ # its name, description, category, parameter schema, and implementation block.
9
+ # It also provides format converters for major LLM provider APIs (Gemini,
10
+ # Anthropic, OpenAI) so the same tool definition can be used across providers.
11
+ #
12
+ # Usage:
13
+ # tool = RubyPi::Tools::Definition.new(
14
+ # name: "create_post",
15
+ # description: "Creates a social media post",
16
+ # category: :content,
17
+ # parameters: RubyPi::Schema.object(
18
+ # content: RubyPi::Schema.string("Post content", required: true)
19
+ # )
20
+ # ) { |args| { post_id: "123", status: "created" } }
21
+ #
22
+ # tool.call(content: "Hello world")
23
+ # # => { post_id: "123", status: "created" }
24
+
25
+ module RubyPi
26
+ module Tools
27
+ class Definition
28
+ # @return [Symbol] The unique name identifying this tool.
29
+ attr_reader :name
30
+
31
+ # @return [String] A human-readable description of what this tool does.
32
+ attr_reader :description
33
+
34
+ # @return [Symbol, nil] An optional category for grouping related tools.
35
+ attr_reader :category
36
+
37
+ # @return [Hash] A JSON Schema hash describing the tool's parameters.
38
+ attr_reader :parameters
39
+
40
+ # Creates a new tool definition.
41
+ #
42
+ # @param name [String, Symbol] Unique identifier for the tool.
43
+ # @param description [String] What the tool does (shown to the LLM).
44
+ # @param category [Symbol, nil] Optional grouping category.
45
+ # @param parameters [Hash] JSON Schema hash for the tool's input parameters.
46
+ # @yield [Hash] Block that implements the tool logic. Receives a hash of arguments.
47
+ # @raise [ArgumentError] If name or description is missing, or no block given.
48
+ def initialize(name:, description:, category: nil, parameters: {}, &block)
49
+ raise ArgumentError, "Tool name is required" if name.nil? || name.to_s.strip.empty?
50
+ raise ArgumentError, "Tool description is required" if description.nil? || description.strip.empty?
51
+ raise ArgumentError, "Tool implementation block is required" unless block_given?
52
+
53
+ @name = name.to_sym
54
+ @description = description
55
+ @category = category&.to_sym
56
+ @parameters = parameters
57
+ @implementation = block
58
+ end
59
+
60
+ # Invokes the tool with the given arguments.
61
+ #
62
+ # @param args [Hash] The arguments to pass to the tool implementation.
63
+ # @return [Object] Whatever the implementation block returns.
64
+ def call(args = {})
65
+ @implementation.call(args)
66
+ end
67
+
68
+ # Converts this tool definition to Google Gemini function declaration format.
69
+ #
70
+ # Gemini expects:
71
+ # { name: "...", description: "...", parameters: { ... } }
72
+ #
73
+ # @return [Hash] The tool in Gemini's function declaration format.
74
+ def to_gemini_format
75
+ declaration = {
76
+ name: @name.to_s,
77
+ description: @description
78
+ }
79
+ declaration[:parameters] = @parameters unless @parameters.empty?
80
+ declaration
81
+ end
82
+
83
+ # Converts this tool definition to Anthropic's tool format.
84
+ #
85
+ # Anthropic expects:
86
+ # { name: "...", description: "...", input_schema: { ... } }
87
+ #
88
+ # @return [Hash] The tool in Anthropic's tool format.
89
+ def to_anthropic_format
90
+ tool = {
91
+ name: @name.to_s,
92
+ description: @description
93
+ }
94
+ tool[:input_schema] = @parameters unless @parameters.empty?
95
+ tool
96
+ end
97
+
98
+ # Converts this tool definition to OpenAI's function calling format.
99
+ #
100
+ # OpenAI expects:
101
+ # { type: "function", function: { name: "...", description: "...", parameters: { ... } } }
102
+ #
103
+ # @return [Hash] The tool in OpenAI's function format.
104
+ def to_openai_format
105
+ function = {
106
+ name: @name.to_s,
107
+ description: @description
108
+ }
109
+ function[:parameters] = @parameters unless @parameters.empty?
110
+ {
111
+ type: "function",
112
+ function: function
113
+ }
114
+ end
115
+
116
+ # Provides a human-readable string representation of the definition.
117
+ #
118
+ # @return [String] A summary string for debugging/logging.
119
+ def inspect
120
+ "#<RubyPi::Tools::Definition name=#{@name.inspect} category=#{@category.inspect}>"
121
+ end
122
+ end
123
+ end
124
+
125
+ # Top-level convenience module for defining tools with a short syntax.
126
+ #
127
+ # Usage:
128
+ # tool = RubyPi::Tool.define(name: "my_tool", description: "Does stuff") { |args| ... }
129
+ module Tool
130
+ class << self
131
+ # Creates a new tool Definition using the same arguments as Definition.new.
132
+ #
133
+ # This is the primary public API for defining tools. It provides a cleaner
134
+ # entry point than instantiating Definition directly.
135
+ #
136
+ # @param (see RubyPi::Tools::Definition#initialize)
137
+ # @return [RubyPi::Tools::Definition] The constructed tool definition.
138
+ def define(name:, description:, category: nil, parameters: {}, &block)
139
+ Tools::Definition.new(
140
+ name: name,
141
+ description: description,
142
+ category: category,
143
+ parameters: parameters,
144
+ &block
145
+ )
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/ruby_pi/tools/executor.rb
4
+ #
5
+ # RubyPi::Tools::Executor — Executes tool calls in parallel or sequentially.
6
+ #
7
+ # The Executor takes a Registry of tools and a list of tool call requests,
8
+ # dispatching each call to the appropriate tool. In `:parallel` mode it uses
9
+ # `concurrent-ruby`'s thread pool (Concurrent::Future) to run tools concurrently.
10
+ # In `:sequential` mode, tools are executed one after another.
11
+ #
12
+ # Each execution is wrapped in error handling: if a tool raises an exception,
13
+ # the error is captured in a Result with `success: false`. A configurable
14
+ # per-tool timeout (default 30 seconds) prevents runaway executions.
15
+ #
16
+ # Usage:
17
+ # executor = RubyPi::Tools::Executor.new(registry, mode: :parallel, timeout: 30)
18
+ # results = executor.execute([
19
+ # { name: "create_post", arguments: { content: "Hello" } },
20
+ # { name: "get_analytics", arguments: { period: "7d" } }
21
+ # ])
22
+ # # => Array of RubyPi::Tools::Result
23
+
24
+ require "concurrent"
25
+
26
+ module RubyPi
27
+ module Tools
28
+ class Executor
29
+ # Default timeout for each tool execution, in seconds.
30
+ DEFAULT_TIMEOUT = 30
31
+
32
+ # @return [Symbol] The execution mode (:parallel or :sequential).
33
+ attr_reader :mode
34
+
35
+ # @return [Numeric] The per-tool timeout in seconds.
36
+ attr_reader :timeout
37
+
38
+ # Creates a new Executor.
39
+ #
40
+ # @param registry [RubyPi::Tools::Registry] The registry to look up tools from.
41
+ # @param mode [Symbol] Execution mode — :parallel or :sequential.
42
+ # @param timeout [Numeric] Per-tool timeout in seconds (default: 30).
43
+ # @raise [ArgumentError] If mode is not :parallel or :sequential.
44
+ def initialize(registry, mode: :parallel, timeout: DEFAULT_TIMEOUT)
45
+ unless %i[parallel sequential].include?(mode)
46
+ raise ArgumentError, "Mode must be :parallel or :sequential, got #{mode.inspect}"
47
+ end
48
+
49
+ @registry = registry
50
+ @mode = mode
51
+ @timeout = timeout
52
+ end
53
+
54
+ # Executes a list of tool calls and returns their results.
55
+ #
56
+ # Each call is a hash with `:name` (String or Symbol) and `:arguments` (Hash).
57
+ # Tools are looked up in the registry; if a tool is not found, a failure
58
+ # Result is returned for that call.
59
+ #
60
+ # @param calls [Array<Hash>] Tool call requests, each with :name and :arguments.
61
+ # @return [Array<RubyPi::Tools::Result>] Results in the same order as the calls.
62
+ def execute(calls)
63
+ case @mode
64
+ when :parallel
65
+ execute_parallel(calls)
66
+ when :sequential
67
+ execute_sequential(calls)
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ # Executes tool calls sequentially, one after another.
74
+ #
75
+ # @param calls [Array<Hash>] The tool call requests.
76
+ # @return [Array<RubyPi::Tools::Result>] Ordered results.
77
+ def execute_sequential(calls)
78
+ calls.map { |call| execute_single(call) }
79
+ end
80
+
81
+ # Executes tool calls in parallel using concurrent-ruby Futures.
82
+ #
83
+ # Each call is dispatched as a Future on the global I/O thread pool.
84
+ # Results are collected in order, respecting the per-tool timeout.
85
+ #
86
+ # @param calls [Array<Hash>] The tool call requests.
87
+ # @return [Array<RubyPi::Tools::Result>] Ordered results.
88
+ def execute_parallel(calls)
89
+ futures = calls.map do |call|
90
+ Concurrent::Future.execute(executor: :io) do
91
+ execute_single(call)
92
+ end
93
+ end
94
+
95
+ # Collect results, respecting the configured timeout for each future.
96
+ futures.map do |future|
97
+ future.value(@timeout) || Result.new(
98
+ name: "unknown",
99
+ success: false,
100
+ error: "Tool execution timed out after #{@timeout}s",
101
+ duration_ms: @timeout * 1000.0
102
+ )
103
+ end
104
+ end
105
+
106
+ # Executes a single tool call with error handling and timing.
107
+ #
108
+ # @param call [Hash] A tool call with :name and :arguments keys.
109
+ # @return [RubyPi::Tools::Result] The execution result.
110
+ def execute_single(call)
111
+ tool_name = (call[:name] || call["name"]).to_s
112
+ arguments = call[:arguments] || call["arguments"] || {}
113
+
114
+ tool = @registry.find(tool_name)
115
+
116
+ # Return an error result if the tool is not registered
117
+ unless tool
118
+ return Result.new(
119
+ name: tool_name,
120
+ success: false,
121
+ error: "Tool '#{tool_name}' not found in registry",
122
+ duration_ms: 0.0
123
+ )
124
+ end
125
+
126
+ # Execute the tool with timeout and error handling
127
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
128
+ begin
129
+ value = Timeout.timeout(@timeout) do
130
+ tool.call(arguments)
131
+ end
132
+ elapsed_ms = elapsed_since(start_time)
133
+
134
+ Result.new(
135
+ name: tool_name,
136
+ success: true,
137
+ value: value,
138
+ duration_ms: elapsed_ms
139
+ )
140
+ rescue Timeout::Error
141
+ elapsed_ms = elapsed_since(start_time)
142
+ Result.new(
143
+ name: tool_name,
144
+ success: false,
145
+ error: "Tool '#{tool_name}' timed out after #{@timeout}s",
146
+ duration_ms: elapsed_ms
147
+ )
148
+ rescue StandardError => e
149
+ elapsed_ms = elapsed_since(start_time)
150
+ Result.new(
151
+ name: tool_name,
152
+ success: false,
153
+ error: "#{e.class}: #{e.message}",
154
+ duration_ms: elapsed_ms
155
+ )
156
+ end
157
+ end
158
+
159
+ # Calculates milliseconds elapsed since a monotonic clock timestamp.
160
+ #
161
+ # @param start_time [Float] The start timestamp from Process.clock_gettime.
162
+ # @return [Float] Elapsed time in milliseconds.
163
+ def elapsed_since(start_time)
164
+ (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000.0
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/ruby_pi/tools/registry.rb
4
+ #
5
+ # RubyPi::Tools::Registry — A thread-safe store for tool definitions.
6
+ #
7
+ # The Registry holds a collection of tool definitions and provides methods
8
+ # for looking them up by name, filtering by category, and extracting subsets.
9
+ # It uses a Mutex for thread safety when registering tools concurrently.
10
+ #
11
+ # Usage:
12
+ # registry = RubyPi::Tools::Registry.new
13
+ # registry.register(my_tool)
14
+ # registry.find(:my_tool) # => Definition or nil
15
+ # registry.by_category(:content) # => [Definition, ...]
16
+ # registry.subset([:tool_a, :tool_b])# => Registry (new instance)
17
+ # registry.names # => [:tool_a, :tool_b, ...]
18
+
19
+ module RubyPi
20
+ module Tools
21
+ class Registry
22
+ # Creates a new, empty Registry.
23
+ def initialize
24
+ @tools = {}
25
+ @mutex = Mutex.new
26
+ end
27
+
28
+ # Registers a tool definition in the registry.
29
+ #
30
+ # If a tool with the same name already exists, it will be overwritten
31
+ # and a warning is emitted to stderr.
32
+ #
33
+ # @param tool [RubyPi::Tools::Definition] The tool to register.
34
+ # @return [RubyPi::Tools::Definition] The registered tool.
35
+ # @raise [ArgumentError] If the argument is not a Definition.
36
+ def register(tool)
37
+ unless tool.is_a?(RubyPi::Tools::Definition)
38
+ raise ArgumentError, "Expected a RubyPi::Tools::Definition, got #{tool.class}"
39
+ end
40
+
41
+ @mutex.synchronize do
42
+ if @tools.key?(tool.name)
43
+ warn "RubyPi::Tools::Registry: overwriting existing tool '#{tool.name}'"
44
+ end
45
+ @tools[tool.name] = tool
46
+ end
47
+
48
+ tool
49
+ end
50
+
51
+ # Finds a tool by name.
52
+ #
53
+ # @param name [String, Symbol] The name of the tool to look up.
54
+ # @return [RubyPi::Tools::Definition, nil] The tool, or nil if not found.
55
+ def find(name)
56
+ @tools[name.to_sym]
57
+ end
58
+
59
+ # Returns a new Registry containing only the tools with the given names.
60
+ #
61
+ # Tools that are not found in this registry are silently skipped.
62
+ #
63
+ # @param names [Array<String, Symbol>] The tool names to include.
64
+ # @return [RubyPi::Tools::Registry] A new registry with the matching tools.
65
+ def subset(names)
66
+ sub = Registry.new
67
+ names.each do |name|
68
+ tool = find(name)
69
+ sub.register(tool) if tool
70
+ end
71
+ sub
72
+ end
73
+
74
+ # Returns all tools that belong to the given category.
75
+ #
76
+ # @param category [Symbol, String] The category to filter by.
77
+ # @return [Array<RubyPi::Tools::Definition>] Tools matching the category.
78
+ def by_category(category)
79
+ cat = category.to_sym
80
+ @tools.values.select { |tool| tool.category == cat }
81
+ end
82
+
83
+ # Returns all registered tool definitions.
84
+ #
85
+ # @return [Array<RubyPi::Tools::Definition>] All tools in registration order.
86
+ def all
87
+ @tools.values
88
+ end
89
+
90
+ # Returns the names of all registered tools.
91
+ #
92
+ # @return [Array<Symbol>] An array of tool name symbols.
93
+ def names
94
+ @tools.keys
95
+ end
96
+
97
+ # Returns the number of registered tools.
98
+ #
99
+ # @return [Integer] The count of tools.
100
+ def size
101
+ @tools.size
102
+ end
103
+
104
+ # Checks whether a tool with the given name is registered.
105
+ #
106
+ # @param name [String, Symbol] The tool name to check.
107
+ # @return [Boolean] true if the tool exists in the registry.
108
+ def registered?(name)
109
+ @tools.key?(name.to_sym)
110
+ end
111
+
112
+ # Provides a human-readable string representation.
113
+ #
114
+ # @return [String] Summary of the registry contents.
115
+ def inspect
116
+ "#<RubyPi::Tools::Registry tools=#{names.inspect}>"
117
+ end
118
+ end
119
+ end
120
+ end