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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +284 -0
- data/lib/ask/conversation.rb +235 -0
- data/lib/ask/errors.rb +66 -0
- data/lib/ask/models.rb +438 -0
- data/lib/ask/provider.rb +232 -0
- data/lib/ask/result.rb +109 -0
- data/lib/ask/stream.rb +123 -0
- data/lib/ask/tool_def.rb +114 -0
- data/lib/ask/version.rb +5 -0
- data/lib/ask.rb +23 -0
- metadata +95 -0
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
|
data/lib/ask/tool_def.rb
ADDED
|
@@ -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
|
data/lib/ask/version.rb
ADDED
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: []
|