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,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/ruby_pi/tools/result.rb
4
+ #
5
+ # RubyPi::Tools::Result — Encapsulates the outcome of a tool execution.
6
+ #
7
+ # Every tool invocation (whether successful or failed) produces a Result object.
8
+ # This provides a uniform interface for inspecting execution outcomes, including
9
+ # the return value, any error messages, and timing information.
10
+ #
11
+ # Usage:
12
+ # result = RubyPi::Tools::Result.new(
13
+ # name: "create_post",
14
+ # success: true,
15
+ # value: { post_id: "123" },
16
+ # duration_ms: 42.5
17
+ # )
18
+ #
19
+ # result.success? # => true
20
+ # result.value # => { post_id: "123" }
21
+ # result.error # => nil
22
+ # result.duration_ms # => 42.5
23
+
24
+ module RubyPi
25
+ module Tools
26
+ class Result
27
+ # @return [String] The name of the tool that was executed.
28
+ attr_reader :name
29
+
30
+ # @return [Object, nil] The return value of the tool (nil if execution failed).
31
+ attr_reader :value
32
+
33
+ # @return [String, nil] The error message if execution failed (nil if successful).
34
+ attr_reader :error
35
+
36
+ # @return [Float] The execution time in milliseconds.
37
+ attr_reader :duration_ms
38
+
39
+ # Creates a new Result instance.
40
+ #
41
+ # @param name [String, Symbol] The name of the tool that produced this result.
42
+ # @param success [Boolean] Whether the tool executed successfully.
43
+ # @param value [Object, nil] The return value from the tool (on success).
44
+ # @param error [String, nil] The error message (on failure).
45
+ # @param duration_ms [Float] How long the tool took to execute, in milliseconds.
46
+ def initialize(name:, success:, value: nil, error: nil, duration_ms: 0.0)
47
+ @name = name.to_s
48
+ @success = success
49
+ @value = value
50
+ @error = error
51
+ @duration_ms = duration_ms.to_f
52
+ end
53
+
54
+ # Returns whether the tool execution was successful.
55
+ #
56
+ # @return [Boolean] true if the tool completed without error.
57
+ def success?
58
+ @success
59
+ end
60
+
61
+ # Returns a hash representation of the result, useful for serialization.
62
+ #
63
+ # @return [Hash] A hash containing all result attributes.
64
+ def to_h
65
+ {
66
+ name: @name,
67
+ success: @success,
68
+ value: @value,
69
+ error: @error,
70
+ duration_ms: @duration_ms
71
+ }
72
+ end
73
+
74
+ # Provides a human-readable string representation of the result.
75
+ #
76
+ # @return [String] A summary string for debugging/logging.
77
+ def inspect
78
+ status = @success ? "success" : "failure"
79
+ "#<RubyPi::Tools::Result name=#{@name.inspect} status=#{status} duration_ms=#{@duration_ms}>"
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/ruby_pi/tools/schema.rb
4
+ #
5
+ # RubyPi::Schema — A DSL for building JSON Schema Draft 7 hashes.
6
+ #
7
+ # This module provides a fluent, Ruby-idiomatic interface for constructing
8
+ # JSON Schema objects used to describe tool parameters. Each builder method
9
+ # (`.string`, `.integer`, `.number`, `.boolean`, `.array`, `.object`) returns
10
+ # a plain Ruby hash that conforms to JSON Schema Draft 7.
11
+ #
12
+ # The `required: true` option on individual property builders is a metadata
13
+ # flag consumed by `.object` to populate the top-level "required" array.
14
+ # It is stripped from the property's own schema hash before inclusion.
15
+ #
16
+ # Usage:
17
+ # schema = RubyPi::Schema.object(
18
+ # name: RubyPi::Schema.string("User's name", required: true),
19
+ # age: RubyPi::Schema.integer("User's age", minimum: 0)
20
+ # )
21
+ #
22
+ # Produces:
23
+ # {
24
+ # type: "object",
25
+ # properties: {
26
+ # name: { type: "string", description: "User's name" },
27
+ # age: { type: "integer", description: "User's age", minimum: 0 }
28
+ # },
29
+ # required: ["name"]
30
+ # }
31
+
32
+ module RubyPi
33
+ module Schema
34
+ class << self
35
+ # Builds a JSON Schema for a string property.
36
+ #
37
+ # @param description [String] Human-readable description of the property.
38
+ # @param required [Boolean] Whether this property is required (used by `.object`).
39
+ # @param enum [Array<String>, nil] Allowed values for the string.
40
+ # @param format [String, nil] JSON Schema format hint (e.g. "date-time", "email").
41
+ # @param min_length [Integer, nil] Minimum string length.
42
+ # @param max_length [Integer, nil] Maximum string length.
43
+ # @param pattern [String, nil] Regex pattern the string must match.
44
+ # @return [Hash] A JSON Schema hash for a string type.
45
+ def string(description = nil, required: false, enum: nil, format: nil,
46
+ min_length: nil, max_length: nil, pattern: nil)
47
+ schema = { type: "string" }
48
+ schema[:description] = description if description
49
+ schema[:enum] = enum if enum
50
+ schema[:format] = format if format
51
+ schema[:minLength] = min_length if min_length
52
+ schema[:maxLength] = max_length if max_length
53
+ schema[:pattern] = pattern if pattern
54
+ schema[:_required] = true if required
55
+ schema
56
+ end
57
+
58
+ # Builds a JSON Schema for an integer property.
59
+ #
60
+ # @param description [String] Human-readable description.
61
+ # @param required [Boolean] Whether this property is required.
62
+ # @param minimum [Integer, nil] Minimum value (inclusive).
63
+ # @param maximum [Integer, nil] Maximum value (inclusive).
64
+ # @param enum [Array<Integer>, nil] Allowed integer values.
65
+ # @return [Hash] A JSON Schema hash for an integer type.
66
+ def integer(description = nil, required: false, minimum: nil, maximum: nil, enum: nil)
67
+ schema = { type: "integer" }
68
+ schema[:description] = description if description
69
+ schema[:minimum] = minimum if minimum
70
+ schema[:maximum] = maximum if maximum
71
+ schema[:enum] = enum if enum
72
+ schema[:_required] = true if required
73
+ schema
74
+ end
75
+
76
+ # Builds a JSON Schema for a number (float/decimal) property.
77
+ #
78
+ # @param description [String] Human-readable description.
79
+ # @param required [Boolean] Whether this property is required.
80
+ # @param minimum [Numeric, nil] Minimum value (inclusive).
81
+ # @param maximum [Numeric, nil] Maximum value (inclusive).
82
+ # @param enum [Array<Numeric>, nil] Allowed numeric values.
83
+ # @return [Hash] A JSON Schema hash for a number type.
84
+ def number(description = nil, required: false, minimum: nil, maximum: nil, enum: nil)
85
+ schema = { type: "number" }
86
+ schema[:description] = description if description
87
+ schema[:minimum] = minimum if minimum
88
+ schema[:maximum] = maximum if maximum
89
+ schema[:enum] = enum if enum
90
+ schema[:_required] = true if required
91
+ schema
92
+ end
93
+
94
+ # Builds a JSON Schema for a boolean property.
95
+ #
96
+ # @param description [String] Human-readable description.
97
+ # @param required [Boolean] Whether this property is required.
98
+ # @return [Hash] A JSON Schema hash for a boolean type.
99
+ def boolean(description = nil, required: false)
100
+ schema = { type: "boolean" }
101
+ schema[:description] = description if description
102
+ schema[:_required] = true if required
103
+ schema
104
+ end
105
+
106
+ # Builds a JSON Schema for an array property.
107
+ #
108
+ # @param description [String, nil] Human-readable description.
109
+ # @param required [Boolean] Whether this property is required.
110
+ # @param items [Hash, nil] JSON Schema hash describing each array element.
111
+ # @param min_items [Integer, nil] Minimum number of items.
112
+ # @param max_items [Integer, nil] Maximum number of items.
113
+ # @param unique_items [Boolean, nil] Whether items must be unique.
114
+ # @return [Hash] A JSON Schema hash for an array type.
115
+ def array(description: nil, required: false, items: nil,
116
+ min_items: nil, max_items: nil, unique_items: nil)
117
+ schema = { type: "array" }
118
+ schema[:description] = description if description
119
+ # Strip internal _required flag from item schemas if present
120
+ schema[:items] = sanitize(items) if items
121
+ schema[:minItems] = min_items if min_items
122
+ schema[:maxItems] = max_items if max_items
123
+ schema[:uniqueItems] = unique_items unless unique_items.nil?
124
+ schema[:_required] = true if required
125
+ schema
126
+ end
127
+
128
+ # Builds a JSON Schema for an object with named properties.
129
+ #
130
+ # Each keyword argument key is a property name, and its value is a schema
131
+ # hash produced by one of the other builder methods (`.string`, `.integer`,
132
+ # etc.). Properties marked with `required: true` are collected into the
133
+ # top-level "required" array.
134
+ #
135
+ # @param properties [Hash{Symbol => Hash}] Property name to schema mappings.
136
+ # @return [Hash] A JSON Schema hash for an object type.
137
+ def object(**properties)
138
+ required_fields = []
139
+ cleaned_properties = {}
140
+
141
+ properties.each do |name, prop_schema|
142
+ # Extract and consume the internal _required flag
143
+ if prop_schema[:_required]
144
+ required_fields << name.to_s
145
+ end
146
+ # Store a sanitized copy (without _required) under the property name
147
+ cleaned_properties[name] = sanitize(prop_schema)
148
+ end
149
+
150
+ schema = {
151
+ type: "object",
152
+ properties: cleaned_properties
153
+ }
154
+ schema[:required] = required_fields unless required_fields.empty?
155
+ schema
156
+ end
157
+
158
+ private
159
+
160
+ # Removes internal metadata keys (prefixed with underscore) from a schema hash.
161
+ # Returns a new hash without mutating the original.
162
+ #
163
+ # @param schema [Hash] A schema hash potentially containing internal keys.
164
+ # @return [Hash] A clean copy suitable for JSON Schema output.
165
+ def sanitize(schema)
166
+ schema.reject { |key, _| key.to_s.start_with?("_") }
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/ruby_pi/version.rb
4
+ #
5
+ # Defines the current version of the RubyPi gem. This constant is referenced
6
+ # by the gemspec and can be queried at runtime via RubyPi::VERSION.
7
+
8
+ module RubyPi
9
+ # The current version of the RubyPi gem, following Semantic Versioning.
10
+ VERSION = "0.1.0"
11
+ end
data/lib/ruby_pi.rb ADDED
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/ruby_pi.rb
4
+ #
5
+ # Main entry point for the RubyPi gem. Requiring this file loads the complete
6
+ # public API, including configuration, error classes, all LLM provider
7
+ # implementations, tool definitions, agent core, context management, and
8
+ # extensions. This file also exposes module-level convenience methods for
9
+ # configuration and model construction.
10
+
11
+ require "json"
12
+ require "faraday"
13
+ require "faraday/retry"
14
+ require "faraday/net_http"
15
+ require "concurrent-ruby"
16
+
17
+ require_relative "ruby_pi/version"
18
+ require_relative "ruby_pi/configuration"
19
+ require_relative "ruby_pi/errors"
20
+ require_relative "ruby_pi/llm/response"
21
+ require_relative "ruby_pi/llm/tool_call"
22
+ require_relative "ruby_pi/llm/stream_event"
23
+ require_relative "ruby_pi/llm/model"
24
+ require_relative "ruby_pi/llm/base_provider"
25
+ require_relative "ruby_pi/llm/gemini"
26
+ require_relative "ruby_pi/llm/anthropic"
27
+ require_relative "ruby_pi/llm/openai"
28
+ require_relative "ruby_pi/llm/fallback"
29
+
30
+ # Tools
31
+ require_relative "ruby_pi/tools/schema"
32
+ require_relative "ruby_pi/tools/definition"
33
+ require_relative "ruby_pi/tools/registry"
34
+ require_relative "ruby_pi/tools/result"
35
+ require_relative "ruby_pi/tools/executor"
36
+
37
+ # Agent
38
+ require_relative "ruby_pi/agent/events"
39
+ require_relative "ruby_pi/agent/state"
40
+ require_relative "ruby_pi/agent/result"
41
+ require_relative "ruby_pi/agent/loop"
42
+ require_relative "ruby_pi/agent/core"
43
+
44
+ # Context
45
+ require_relative "ruby_pi/context/compaction"
46
+ require_relative "ruby_pi/context/transform"
47
+
48
+ # Extensions
49
+ require_relative "ruby_pi/extensions/base"
50
+
51
+ # Top-level namespace for the RubyPi framework.
52
+ module RubyPi
53
+ class << self
54
+ # Returns the global configuration object.
55
+ #
56
+ # @return [RubyPi::Configuration] the current global configuration
57
+ def configuration
58
+ @configuration ||= Configuration.new
59
+ end
60
+
61
+ # Yields the global configuration object to a block for mutation.
62
+ #
63
+ # @yield [configuration] the global configuration instance
64
+ # @return [RubyPi::Configuration] the updated configuration
65
+ #
66
+ # @example Configure API keys
67
+ # RubyPi.configure do |config|
68
+ # config.openai_api_key = ENV["OPENAI_API_KEY"]
69
+ # config.max_retries = 5
70
+ # end
71
+ def configure
72
+ yield(configuration) if block_given?
73
+ configuration
74
+ end
75
+
76
+ # Resets the global configuration to default values.
77
+ #
78
+ # @return [void]
79
+ def reset_configuration!
80
+ @configuration = Configuration.new
81
+ end
82
+ end
83
+
84
+ # Namespace for large language model providers and related abstractions.
85
+ module LLM
86
+ class << self
87
+ # Factory method for constructing a provider instance from a provider name
88
+ # and model identifier.
89
+ #
90
+ # @param provider [Symbol, String] provider identifier (:gemini, :anthropic, :openai)
91
+ # @param name [String] the model name to use with the provider
92
+ # @param options [Hash] provider-specific initialization options
93
+ # @return [RubyPi::LLM::BaseProvider] configured provider instance
94
+ # @raise [ArgumentError] if the provider is unsupported
95
+ #
96
+ # @example Build a Gemini model
97
+ # model = RubyPi::LLM.model(:gemini, "gemini-2.0-flash")
98
+ def model(provider, name, **options)
99
+ case provider.to_sym
100
+ when :gemini
101
+ Gemini.new(model: name, **options)
102
+ when :anthropic
103
+ Anthropic.new(model: name, **options)
104
+ when :openai
105
+ OpenAI.new(model: name, **options)
106
+ else
107
+ raise ArgumentError, "Unsupported provider: #{provider}"
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
metadata ADDED
@@ -0,0 +1,192 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-pi
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - RubyPi Contributors
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: faraday
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: faraday-retry
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: faraday-net_http
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ - - "<"
48
+ - !ruby/object:Gem::Version
49
+ version: '3.4'
50
+ type: :runtime
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '3.0'
57
+ - - "<"
58
+ - !ruby/object:Gem::Version
59
+ version: '3.4'
60
+ - !ruby/object:Gem::Dependency
61
+ name: concurrent-ruby
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '1.2'
67
+ type: :runtime
68
+ prerelease: false
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '1.2'
74
+ - !ruby/object:Gem::Dependency
75
+ name: ostruct
76
+ requirement: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '0.6'
81
+ type: :runtime
82
+ prerelease: false
83
+ version_requirements: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '0.6'
88
+ - !ruby/object:Gem::Dependency
89
+ name: rspec
90
+ requirement: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: '3.12'
95
+ type: :development
96
+ prerelease: false
97
+ version_requirements: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - "~>"
100
+ - !ruby/object:Gem::Version
101
+ version: '3.12'
102
+ - !ruby/object:Gem::Dependency
103
+ name: webmock
104
+ requirement: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - "~>"
107
+ - !ruby/object:Gem::Version
108
+ version: '3.18'
109
+ type: :development
110
+ prerelease: false
111
+ version_requirements: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - "~>"
114
+ - !ruby/object:Gem::Version
115
+ version: '3.18'
116
+ - !ruby/object:Gem::Dependency
117
+ name: rake
118
+ requirement: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - "~>"
121
+ - !ruby/object:Gem::Version
122
+ version: '13.0'
123
+ type: :development
124
+ prerelease: false
125
+ version_requirements: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - "~>"
128
+ - !ruby/object:Gem::Version
129
+ version: '13.0'
130
+ description: RubyPi provides idiomatic Ruby patterns for building AI agents. It offers
131
+ a unified interface to multiple LLM providers (Gemini, Anthropic, OpenAI) with streaming
132
+ support, tool calling, automatic retries, and fallback strategies.
133
+ email:
134
+ - ruby-pi@example.com
135
+ executables: []
136
+ extensions: []
137
+ extra_rdoc_files: []
138
+ files:
139
+ - CHANGELOG.md
140
+ - LICENSE
141
+ - README.md
142
+ - lib/ruby_pi.rb
143
+ - lib/ruby_pi/agent/core.rb
144
+ - lib/ruby_pi/agent/events.rb
145
+ - lib/ruby_pi/agent/loop.rb
146
+ - lib/ruby_pi/agent/result.rb
147
+ - lib/ruby_pi/agent/state.rb
148
+ - lib/ruby_pi/configuration.rb
149
+ - lib/ruby_pi/context/compaction.rb
150
+ - lib/ruby_pi/context/transform.rb
151
+ - lib/ruby_pi/errors.rb
152
+ - lib/ruby_pi/extensions/base.rb
153
+ - lib/ruby_pi/llm/anthropic.rb
154
+ - lib/ruby_pi/llm/base_provider.rb
155
+ - lib/ruby_pi/llm/fallback.rb
156
+ - lib/ruby_pi/llm/gemini.rb
157
+ - lib/ruby_pi/llm/model.rb
158
+ - lib/ruby_pi/llm/openai.rb
159
+ - lib/ruby_pi/llm/response.rb
160
+ - lib/ruby_pi/llm/stream_event.rb
161
+ - lib/ruby_pi/llm/tool_call.rb
162
+ - lib/ruby_pi/tools/definition.rb
163
+ - lib/ruby_pi/tools/executor.rb
164
+ - lib/ruby_pi/tools/registry.rb
165
+ - lib/ruby_pi/tools/result.rb
166
+ - lib/ruby_pi/tools/schema.rb
167
+ - lib/ruby_pi/version.rb
168
+ homepage: https://github.com/ejwhite7/ruby-pi
169
+ licenses:
170
+ - MIT
171
+ metadata:
172
+ homepage_uri: https://github.com/ejwhite7/ruby-pi
173
+ source_code_uri: https://github.com/ejwhite7/ruby-pi
174
+ changelog_uri: https://github.com/ejwhite7/ruby-pi/blob/main/CHANGELOG.md
175
+ rdoc_options: []
176
+ require_paths:
177
+ - lib
178
+ required_ruby_version: !ruby/object:Gem::Requirement
179
+ requirements:
180
+ - - ">="
181
+ - !ruby/object:Gem::Version
182
+ version: 3.2.0
183
+ required_rubygems_version: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ requirements: []
189
+ rubygems_version: 4.0.3
190
+ specification_version: 4
191
+ summary: A Ruby agent harness inspired by Pi — minimal, composable, production-ready.
192
+ test_files: []