ask-core 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.
data/lib/ask/result.rb ADDED
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ # Standardized return value from tool execution. Wraps the outcome of a tool
5
+ # call with status, content, and optional error metadata.
6
+ #
7
+ # Ask::Result.success("Data processed")
8
+ # Ask::Result.failure("API returned 500")
9
+ #
10
+ class Result
11
+ STATUSES = %i[success error aborted blocked short_circuited].freeze
12
+
13
+ class << self
14
+ # @!group Factory Methods
15
+
16
+ # Create a successful result.
17
+ # @param content [Object, nil] the result content
18
+ # @param metadata [Hash] additional metadata
19
+ # @return [Ask::Result]
20
+ def success(content = nil, metadata: {})
21
+ new(content: content, status: :success, metadata: metadata)
22
+ end
23
+
24
+ # Create a failure result.
25
+ # @param message [String] the error description
26
+ # @param error [Object, nil] the underlying error object
27
+ # @param metadata [Hash] additional metadata
28
+ # @return [Ask::Result]
29
+ def failure(message, error: nil, metadata: {})
30
+ new(content: message, status: :error, error: error, metadata: metadata)
31
+ end
32
+
33
+ # Create an aborted result (cancelled by sibling failure).
34
+ # @param reason [String] the abort reason
35
+ # @return [Ask::Result]
36
+ def aborted(reason = "Aborted")
37
+ new(content: reason, status: :aborted)
38
+ end
39
+
40
+ # Create a blocked result (prevented by a hook or guard).
41
+ # @param reason [String] the block reason
42
+ # @return [Ask::Result]
43
+ def blocked(reason)
44
+ new(content: reason, status: :blocked)
45
+ end
46
+ # @!endgroup
47
+ end
48
+
49
+ # @return [Object, nil] the result content
50
+ attr_reader :content
51
+
52
+ # @return [Symbol] the status (:success, :error, :aborted, :blocked, :short_circuited)
53
+ attr_reader :status
54
+
55
+ # @return [Object, nil] the underlying error, if any
56
+ attr_reader :error
57
+
58
+ # @return [Hash] additional metadata
59
+ attr_reader :metadata
60
+
61
+ def initialize(content: nil, status: :success, error: nil, metadata: {})
62
+ @content = content
63
+ @status = validate_status!(status)
64
+ @error = error
65
+ @metadata = metadata.dup.freeze
66
+ freeze
67
+ end
68
+
69
+ # @return [Boolean] true if status is :success
70
+ def success? = @status == :success
71
+
72
+ # @return [Boolean] true if status is :error
73
+ def error? = @status == :error
74
+
75
+ # @return [Boolean] true if status is :aborted
76
+ def aborted? = @status == :aborted
77
+
78
+ # @return [Boolean] true if status is :blocked
79
+ def blocked? = @status == :blocked
80
+
81
+ # @return [String] the content as a string
82
+ def to_s
83
+ @content.to_s
84
+ end
85
+
86
+ # @return [Hash] serialized representation
87
+ def to_h
88
+ {
89
+ content: @content,
90
+ status: @status,
91
+ error: @error,
92
+ metadata: @metadata
93
+ }.compact
94
+ end
95
+
96
+ # @return [String] human-readable representation
97
+ def inspect
98
+ "#<Ask::Result status=#{@status.inspect} content=#{@content.inspect}>"
99
+ end
100
+
101
+ private
102
+
103
+ def validate_status!(status)
104
+ return status if STATUSES.include?(status)
105
+
106
+ raise ArgumentError, "Invalid status #{status.inspect}. Valid: #{STATUSES.join(', ')}"
107
+ end
108
+ end
109
+ end
data/lib/ask/stream.rb ADDED
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ # A single chunk of streaming output from an LLM provider.
5
+ # Yielded by {Ask::Stream} during a streaming response.
6
+ class Chunk
7
+ # @return [String, nil] text content of this chunk
8
+ attr_reader :content
9
+
10
+ # @return [Array<Hash>, nil] tool call invocations in this chunk
11
+ attr_reader :tool_calls
12
+
13
+ # @return [String, nil] reason the stream finished ("stop", "length", "tool_calls")
14
+ attr_reader :finish_reason
15
+
16
+ # @return [Hash, nil] token usage metadata
17
+ attr_reader :usage
18
+
19
+ # @return [Hash, nil] raw response data from the provider
20
+ attr_reader :raw
21
+
22
+ def initialize(content: nil, tool_calls: nil, finish_reason: nil, usage: nil, raw: nil)
23
+ @content = content
24
+ @tool_calls = tool_calls
25
+ @finish_reason = finish_reason
26
+ @usage = usage
27
+ @raw = raw
28
+ end
29
+
30
+ # @return [Boolean] true if this is the final chunk in a stream
31
+ def finished? = !@finish_reason.nil?
32
+
33
+ # @return [Boolean] true if this chunk contains tool calls
34
+ def tool_call? = @tool_calls&.any? == true
35
+
36
+ # @return [String] text content as a plain string
37
+ def to_s
38
+ @content.to_s
39
+ end
40
+
41
+ # @return [String] human-readable representation
42
+ def inspect
43
+ "#<Ask::Chunk content=#{@content.inspect} finish_reason=#{@finish_reason.inspect}>"
44
+ end
45
+ end
46
+
47
+ # Streaming response from an LLM provider. Wraps an enumerable of {Chunk}s and
48
+ # provides accumulation into a single string or message.
49
+ #
50
+ # stream = Ask::Stream.new { |chunk| puts chunk.content }
51
+ # stream.each { |chunk| ... }
52
+ # stream.accumulated_text # => "full response text"
53
+ #
54
+ class Stream
55
+ include Enumerable
56
+
57
+ # @param chunk_handler [Proc, nil] optional callback invoked for each chunk as it arrives
58
+ def initialize(&chunk_handler)
59
+ @chunks = []
60
+ @chunk_handler = chunk_handler
61
+ @accumulated = +""
62
+ @finished = false
63
+ end
64
+
65
+ # Iterate over all accumulated chunks.
66
+ # @yield [Chunk] each chunk
67
+ # @return [Enumerator] if no block given
68
+ def each(&block)
69
+ return enum_for(:each) unless block
70
+
71
+ @chunks.each(&block)
72
+ self
73
+ end
74
+
75
+ # Add a chunk to the stream.
76
+ # @param chunk [Chunk, String] chunk to add (strings are wrapped in Chunk)
77
+ # @return [Chunk] the added chunk
78
+ def add(chunk)
79
+ chunk = Chunk.new(content: chunk) if chunk.is_a?(String)
80
+
81
+ @chunks << chunk
82
+ @accumulated << chunk.content.to_s if chunk.content
83
+ @chunk_handler&.call(chunk)
84
+ chunk
85
+ end
86
+
87
+ # Mark the stream as finished.
88
+ def finish!
89
+ @finished = true
90
+ end
91
+
92
+ # @return [Boolean] true if the stream has been finished
93
+ def finished? = @finished
94
+
95
+ # @return [String] full accumulated text from all chunks
96
+ def accumulated_text
97
+ @accumulated.dup
98
+ end
99
+ alias to_s accumulated_text
100
+
101
+ # @return [Hash] accumulated usage across all chunks, merged by key
102
+ def accumulated_usage
103
+ @chunks
104
+ .map(&:usage)
105
+ .compact
106
+ .reduce({}) do |acc, u|
107
+ acc.merge(u) { |_key, old, new| old + new }
108
+ end
109
+ end
110
+
111
+ # @return [Array<Chunk>] a copy of all accumulated chunks
112
+ def chunks = @chunks.dup
113
+
114
+ # @return [Integer] number of chunks accumulated
115
+ def length = @chunks.length
116
+
117
+ # @return [String] human-readable representation
118
+ def inspect
119
+ finished = @finished ? "finished" : "streaming"
120
+ "#<Ask::Stream #{finished} chunks=#{@chunks.length}>"
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ # Immutable value object representing a tool (function) definition that can be
5
+ # passed to LLM providers. Describes a callable tool's name, description, and
6
+ # parameter schema.
7
+ #
8
+ # ToolDef.new(
9
+ # name: "get_weather",
10
+ # description: "Get current weather for a location",
11
+ # parameters: {
12
+ # type: "object",
13
+ # properties: { location: { type: "string" } },
14
+ # required: ["location"]
15
+ # }
16
+ # )
17
+ #
18
+ class ToolDef
19
+ class << self
20
+ # Build a ToolDef from an object that responds to #name, #description,
21
+ # #params_schema, and #provider_params.
22
+ # @param tool [#name, #description, #params_schema] a tool-like object
23
+ # @return [Ask::ToolDef]
24
+ def from_tool(tool)
25
+ schema = tool.params_schema
26
+ new(
27
+ name: tool.name,
28
+ description: tool.description,
29
+ parameters: schema,
30
+ provider_params: tool.provider_params
31
+ )
32
+ end
33
+ end
34
+
35
+ # @return [String] tool name (must match /\\A[a-zA-Z_][a-zA-Z0-9_-]*\\z/)
36
+ attr_reader :name
37
+
38
+ # @return [String] description of what the tool does
39
+ attr_reader :description
40
+
41
+ # @return [Hash, nil] JSON Schema parameter definition
42
+ attr_reader :parameters
43
+
44
+ # @return [Hash] provider-specific parameters (e.g. concurrency limits)
45
+ attr_reader :provider_params
46
+
47
+ def initialize(name:, description: "", parameters: nil, provider_params: {})
48
+ @name = validate_name!(name)
49
+ @description = description.to_s
50
+ @parameters = parameters
51
+ @provider_params = provider_params.dup.freeze
52
+ freeze
53
+ end
54
+
55
+ # Convert to a provider-specific format.
56
+ # @yield [self] yields the tool def for custom formatting
57
+ # @return [Hash] provider-ready parameters
58
+ def to_provider_format(&block)
59
+ return @parameters || default_parameters unless block
60
+
61
+ block.call(self)
62
+ end
63
+
64
+ # Two ToolDefs are equal if they have the same name.
65
+ # @return [Boolean]
66
+ def ==(other)
67
+ return false unless other.is_a?(ToolDef)
68
+
69
+ name == other.name
70
+ end
71
+ alias eql? ==
72
+
73
+ def hash
74
+ name.hash
75
+ end
76
+
77
+ # @return [Hash] serialized representation
78
+ def to_h
79
+ {
80
+ name: @name,
81
+ description: @description,
82
+ parameters: @parameters,
83
+ provider_params: @provider_params
84
+ }
85
+ end
86
+
87
+ # @return [String]
88
+ def inspect
89
+ "#<Ask::ToolDef name=#{@name.inspect}>"
90
+ end
91
+
92
+ private
93
+
94
+ def validate_name!(name)
95
+ raise InvalidToolDefinition, "Tool name is required" if name.nil? || name.to_s.strip.empty?
96
+
97
+ normalized = name.to_s.strip
98
+ unless normalized.match?(/\A[a-zA-Z_][a-zA-Z0-9_-]*\z/)
99
+ raise InvalidToolDefinition,
100
+ "Tool name must start with a letter or underscore and contain only " \
101
+ "letters, numbers, hyphens, and underscores: #{normalized.inspect}"
102
+ end
103
+ normalized
104
+ end
105
+
106
+ def default_parameters
107
+ {
108
+ type: "object",
109
+ properties: {},
110
+ required: []
111
+ }
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ VERSION = "0.1.0"
5
+ end
data/lib/ask.rb ADDED
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ask/version"
4
+
5
+ # Main namespace for the ask-rb ecosystem.
6
+ #
7
+ # Ask::Provider is the abstract base class for LLM providers.
8
+ # Ask::Conversation is a message container with role normalization.
9
+ # Ask::Stream provides streaming primitives for incremental responses.
10
+ # Ask::ModelCatalog resolves model names to provider metadata.
11
+ # Ask::ToolDef is an immutable tool definition struct.
12
+ # Ask::Result standardizes tool execution return values.
13
+ # Ask::Error provides structured error types.
14
+ module Ask
15
+ end
16
+
17
+ require_relative "ask/errors"
18
+ require_relative "ask/tool_def"
19
+ require_relative "ask/result"
20
+ require_relative "ask/stream"
21
+ require_relative "ask/conversation"
22
+ require_relative "ask/provider"
23
+ require_relative "ask/models"
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ask-core
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kaka Ruto
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: minitest
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '5.25'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '5.25'
26
+ - !ruby/object:Gem::Dependency
27
+ name: mocha
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.1'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.1'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '13.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '13.0'
54
+ description: Provides Ask::Provider (abstract interface), Ask::Conversation, Ask::Stream,
55
+ Ask::ModelCatalog, Ask::ToolDef, Ask::Result, and structured error types. Zero runtime
56
+ dependencies.
57
+ email:
58
+ - kaka@myrrlabs.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - LICENSE
64
+ - README.md
65
+ - lib/ask.rb
66
+ - lib/ask/conversation.rb
67
+ - lib/ask/errors.rb
68
+ - lib/ask/models.rb
69
+ - lib/ask/provider.rb
70
+ - lib/ask/result.rb
71
+ - lib/ask/stream.rb
72
+ - lib/ask/tool_def.rb
73
+ - lib/ask/version.rb
74
+ homepage: https://github.com/ask-rb/ask-core
75
+ licenses:
76
+ - MIT
77
+ metadata: {}
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '3.2'
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 4.0.3
93
+ specification_version: 4
94
+ summary: Foundation gem for the ask-rb ecosystem
95
+ test_files: []