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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9ab6b3602459d44db528b10dc551037f0c7da6d54d4876aeda87fc572ddbca91
|
|
4
|
+
data.tar.gz: 5500b00c8fc487780f7b96a5f574fde70e6c9af20ff9ced561ef18bf7a46316f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ab1f35dfe6939eca8df702e84a0321c3bbc88c3ce6415509f1f46f0cd3dc459167b9f21b01fdf51a31bd70247d4e3b60921962e3f8c4c1ea8612d58b41a5176b
|
|
7
|
+
data.tar.gz: e7f6df8a119b318362908287031051de6917fd9fc775732c45af33dec4a5ee15c6304cec2e9b6cf4a3ade75ca415b8a80213cb4ef068fb7fe9d86429ffd06f16
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kaka Ruto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# ask-core
|
|
2
|
+
|
|
3
|
+
Foundation gem for the ask-rb ecosystem. Provides the types and interfaces that every provider gem builds on.
|
|
4
|
+
|
|
5
|
+
**Zero external dependencies.** Uses only Ruby stdlib (`json`, `net/http`, `date`, `time`).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# In your Gemfile
|
|
11
|
+
gem "ask-core"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## What it provides
|
|
15
|
+
|
|
16
|
+
| Component | File | Purpose |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| `Ask::Provider` | `lib/ask/provider.rb` | Abstract base class for all LLM providers |
|
|
19
|
+
| `Ask::Conversation` | `lib/ask/conversation.rb` | Message container with role normalization |
|
|
20
|
+
| `Ask::Stream` / `Ask::Chunk` | `lib/ask/stream.rb` | Streaming primitives |
|
|
21
|
+
| `Ask::ModelCatalog` | `lib/ask/models.rb` | Model name to provider resolution |
|
|
22
|
+
| `Ask::ToolDef` | `lib/ask/tool_def.rb` | Immutable tool metadata struct |
|
|
23
|
+
| `Ask::Result` | `lib/ask/result.rb` | Standardized tool return value |
|
|
24
|
+
| `Ask::Error` | `lib/ask/errors.rb` | Structured error types |
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
### Provider (abstract base class)
|
|
29
|
+
|
|
30
|
+
Provider gems subclass `Ask::Provider` and implement the abstract methods:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
class MyProvider < Ask::Provider
|
|
34
|
+
def api_base
|
|
35
|
+
"https://api.example.com/v1"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def headers
|
|
39
|
+
{ "Authorization" => "Bearer #{@config.api_key}" }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def chat(messages, model:, tools: nil, temperature: nil, stream: nil, schema: nil, **params, &block)
|
|
43
|
+
# Return an Ask::Message or yield Ask::Chunks
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def embed(text, model:)
|
|
47
|
+
# Return an array of floats
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def list_models
|
|
51
|
+
# Return an array of Ask::ModelInfo
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class << self
|
|
55
|
+
def configuration_options
|
|
56
|
+
[:api_key, :api_base]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def configuration_requirements
|
|
60
|
+
[:api_key]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Register the provider
|
|
66
|
+
Ask::Provider.register(:my_provider, MyProvider)
|
|
67
|
+
|
|
68
|
+
# Resolve by name
|
|
69
|
+
Ask::Provider.resolve(:my_provider) # => MyProvider
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Conversation
|
|
73
|
+
|
|
74
|
+
Build and manipulate conversations with role-normalized messages:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
conv = Ask::Conversation.new
|
|
78
|
+
|
|
79
|
+
# Convenience methods
|
|
80
|
+
conv.system("You are a helpful assistant.")
|
|
81
|
+
conv.user("What's the weather in Tokyo?")
|
|
82
|
+
conv.assistant("Let me check...", tool_calls: [{ name: "get_weather", arguments: { location: "Tokyo" } }])
|
|
83
|
+
conv.tool_result("72°F, sunny", tool_call_id: "call_123")
|
|
84
|
+
|
|
85
|
+
# Iteration
|
|
86
|
+
conv.each { |msg| puts "#{msg.role}: #{msg.content}" }
|
|
87
|
+
|
|
88
|
+
# Filtering by role
|
|
89
|
+
conv.user_messages # => [Ask::Message, ...]
|
|
90
|
+
conv.system_messages # => [Ask::Message, ...]
|
|
91
|
+
|
|
92
|
+
# Serialization
|
|
93
|
+
conv.to_a # => [{ role: :user, content: "..." }, ...]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Messages
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
msg = Ask::Message.new(role: :user, content: "Hello")
|
|
100
|
+
msg.user? # => true
|
|
101
|
+
msg.system? # => false
|
|
102
|
+
msg.assistant? # => false
|
|
103
|
+
msg.tool? # => false
|
|
104
|
+
|
|
105
|
+
msg = Ask::Message.new(role: :assistant, tool_calls: [{ name: "f", arguments: {} }])
|
|
106
|
+
msg.tool_call? # => true
|
|
107
|
+
|
|
108
|
+
msg = Ask::Message.new(role: :tool, content: "result", tool_call_id: "call_1")
|
|
109
|
+
msg.tool_result? # => true
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Valid roles: `:system`, `:user`, `:assistant`, `:tool`
|
|
113
|
+
|
|
114
|
+
### Streaming
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
stream = Ask::Stream.new
|
|
118
|
+
|
|
119
|
+
# Add chunks as they arrive from the provider
|
|
120
|
+
stream.add(Ask::Chunk.new(content: "Hello "))
|
|
121
|
+
stream.add(Ask::Chunk.new(content: "World"))
|
|
122
|
+
stream.add(Ask::Chunk.new(content: "", finish_reason: "stop"))
|
|
123
|
+
stream.finish!
|
|
124
|
+
|
|
125
|
+
# Accumulate the full response
|
|
126
|
+
stream.accumulated_text # => "Hello World"
|
|
127
|
+
stream.to_s # => "Hello World"
|
|
128
|
+
|
|
129
|
+
# Track token usage
|
|
130
|
+
stream.accumulated_usage # => { input_tokens: 10, output_tokens: 20 }
|
|
131
|
+
|
|
132
|
+
# Iterate
|
|
133
|
+
stream.each { |chunk| print chunk.content }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Chunks
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
chunk = Ask::Chunk.new(content: "Hello")
|
|
140
|
+
chunk.content # => "Hello"
|
|
141
|
+
chunk.finished? # => false
|
|
142
|
+
chunk.tool_call? # => false
|
|
143
|
+
chunk.finish_reason # => nil (or "stop", "length", "tool_calls")
|
|
144
|
+
|
|
145
|
+
chunk = Ask::Chunk.new(tool_calls: [{ name: "get_weather" }])
|
|
146
|
+
chunk.tool_call? # => true
|
|
147
|
+
|
|
148
|
+
chunk = Ask::Chunk.new(usage: { input_tokens: 10, output_tokens: 20 })
|
|
149
|
+
chunk.usage # => { input_tokens: 10, output_tokens: 20 }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Model Catalog
|
|
153
|
+
|
|
154
|
+
Query available models from the registry:
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
catalog = Ask::ModelCatalog.new([
|
|
158
|
+
Ask::ModelInfo.new(id: "gpt-4o", provider: "openai", capabilities: ["function_calling", "vision"]),
|
|
159
|
+
Ask::ModelInfo.new(id: "claude-sonnet-4", provider: "anthropic", capabilities: ["function_calling", "reasoning"])
|
|
160
|
+
])
|
|
161
|
+
|
|
162
|
+
# Find by ID (prefers most common provider)
|
|
163
|
+
catalog.find("gpt-4o")
|
|
164
|
+
|
|
165
|
+
# Find with specific provider
|
|
166
|
+
catalog.find("gpt-4o", "openai")
|
|
167
|
+
|
|
168
|
+
# Filter by type
|
|
169
|
+
catalog.chat_models
|
|
170
|
+
catalog.embedding_models
|
|
171
|
+
|
|
172
|
+
# Filter by provider or family
|
|
173
|
+
catalog.by_provider("openai")
|
|
174
|
+
catalog.by_family("gpt")
|
|
175
|
+
|
|
176
|
+
# Singleton instance
|
|
177
|
+
Ask::ModelCatalog.instance
|
|
178
|
+
Ask::ModelCatalog.find("gpt-4o")
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### ModelInfo
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
info = Ask::ModelInfo.new(
|
|
185
|
+
id: "gpt-4o",
|
|
186
|
+
provider: "openai",
|
|
187
|
+
capabilities: ["function_calling", "vision"],
|
|
188
|
+
context_window: 128_000,
|
|
189
|
+
pricing: { text_tokens: { standard: { input_per_million: 2.5, output_per_million: 10 } } }
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
info.supports?(:function_calling) # => true
|
|
193
|
+
info.chat? # => true
|
|
194
|
+
info.embedding? # => false
|
|
195
|
+
info.context_window # => 128_000
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Tool Definitions
|
|
199
|
+
|
|
200
|
+
Immutable tool metadata for provider function calling:
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
tool = Ask::ToolDef.new(
|
|
204
|
+
name: "get_weather",
|
|
205
|
+
description: "Get current weather for a location",
|
|
206
|
+
parameters: {
|
|
207
|
+
type: "object",
|
|
208
|
+
properties: {
|
|
209
|
+
location: { type: "string", description: "City name" },
|
|
210
|
+
unit: { type: "string", enum: ["celsius", "fahrenheit"] }
|
|
211
|
+
},
|
|
212
|
+
required: ["location"]
|
|
213
|
+
}
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
tool.name # => "get_weather"
|
|
217
|
+
tool.description # => "Get current weather for a location"
|
|
218
|
+
|
|
219
|
+
# Provider-specific format
|
|
220
|
+
tool.to_provider_format { |t| { type: "function", function: t.to_h } }
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Tool Results
|
|
224
|
+
|
|
225
|
+
Standardized return values from tool execution:
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
Ask::Result.success("Data processed")
|
|
229
|
+
Ask::Result.success(updated_record, metadata: { duration: 1.2 })
|
|
230
|
+
Ask::Result.failure("API returned 500", error: "Timeout")
|
|
231
|
+
Ask::Result.aborted("Cancelled by sibling failure")
|
|
232
|
+
Ask::Result.blocked("Permission denied")
|
|
233
|
+
|
|
234
|
+
result = Ask::Result.success("OK")
|
|
235
|
+
result.success? # => true
|
|
236
|
+
result.error? # => false
|
|
237
|
+
result.aborted? # => false
|
|
238
|
+
result.blocked? # => false
|
|
239
|
+
result.to_s # => "OK"
|
|
240
|
+
result.to_h # => { content: "OK", status: :success, metadata: {} }
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Error Types
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
Ask::Error # Base class (rescue Ask::Error to catch all)
|
|
247
|
+
Ask::ConfigurationError # Missing/incorrect configuration
|
|
248
|
+
Ask::UnknownProvider # Provider not registered
|
|
249
|
+
Ask::ModelNotFound # Model not in catalog
|
|
250
|
+
Ask::InvalidRole # Invalid message role
|
|
251
|
+
Ask::InvalidToolDefinition # Invalid tool name/definition
|
|
252
|
+
Ask::ProviderError # Provider API error (with status_code, response_body)
|
|
253
|
+
Ask::ContextLengthExceeded # Context window exceeded
|
|
254
|
+
Ask::RateLimitError # Rate limited
|
|
255
|
+
Ask::Unauthorized # Authentication failure
|
|
256
|
+
Ask::ServerError # 5xx server error
|
|
257
|
+
Ask::ServiceUnavailable # Service temporarily unavailable
|
|
258
|
+
Ask::UnsupportedFeature # Feature not supported by provider/model
|
|
259
|
+
Ask::MissingCredential # Required credential not found
|
|
260
|
+
Ask::InvalidCredential # Credential is invalid/expired
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Development
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
bundle exec rake test
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Testing
|
|
270
|
+
|
|
271
|
+
- Uses Minitest (not RSpec) — consistent with the ask-rb ecosystem.
|
|
272
|
+
- Unit tests for every public method.
|
|
273
|
+
- Run the full suite before every commit: `bundle exec rake test`.
|
|
274
|
+
|
|
275
|
+
## Design Principles
|
|
276
|
+
|
|
277
|
+
1. **Zero runtime dependencies** — stdlib only. Provider gems add their own HTTP clients.
|
|
278
|
+
2. **Immutable value objects** — `Message`, `ToolDef`, `Result`, `Chunk`, and `ModelInfo` are frozen after construction.
|
|
279
|
+
3. **Abstract interface** — `Ask::Provider` defines the contract. Provider gems implement the wire format.
|
|
280
|
+
4. **Provider registry** — providers register themselves for runtime resolution by name.
|
|
281
|
+
|
|
282
|
+
## License
|
|
283
|
+
|
|
284
|
+
MIT
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ask
|
|
4
|
+
# A single message in a conversation. Immutable after creation.
|
|
5
|
+
# Valid roles are: +:system+, +:user+, +:assistant+, +:tool+.
|
|
6
|
+
class Message
|
|
7
|
+
VALID_ROLES = %i[system user assistant tool].freeze
|
|
8
|
+
|
|
9
|
+
# @return [Symbol] message role (:system, :user, :assistant, :tool)
|
|
10
|
+
attr_reader :role
|
|
11
|
+
|
|
12
|
+
# @return [String, nil] message text content
|
|
13
|
+
attr_reader :content
|
|
14
|
+
|
|
15
|
+
# @return [String, nil] optional participant name (for multi-agent scenarios)
|
|
16
|
+
attr_reader :name
|
|
17
|
+
|
|
18
|
+
# @return [String, nil] tool call ID this message is responding to
|
|
19
|
+
attr_reader :tool_call_id
|
|
20
|
+
|
|
21
|
+
# @return [Array<Hash>, nil] tool calls included in this message
|
|
22
|
+
attr_reader :tool_calls
|
|
23
|
+
|
|
24
|
+
# @return [Hash] arbitrary metadata attached to this message
|
|
25
|
+
attr_reader :metadata
|
|
26
|
+
|
|
27
|
+
def initialize(role:, content: nil, name: nil, tool_call_id: nil, tool_calls: nil, metadata: {})
|
|
28
|
+
@role = normalize_role!(role)
|
|
29
|
+
@content = content
|
|
30
|
+
@name = normalize_name(name)
|
|
31
|
+
@tool_call_id = tool_call_id
|
|
32
|
+
@tool_calls = tool_calls
|
|
33
|
+
@metadata = metadata.dup.freeze
|
|
34
|
+
validate!
|
|
35
|
+
freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [Boolean] true if this message contains tool calls
|
|
39
|
+
def tool_call? = @tool_calls&.any? == true
|
|
40
|
+
|
|
41
|
+
# @return [Boolean] true if this is a tool result message
|
|
42
|
+
def tool_result? = !@tool_call_id.nil?
|
|
43
|
+
|
|
44
|
+
# @return [Boolean] true if role is :system
|
|
45
|
+
def system? = @role == :system
|
|
46
|
+
|
|
47
|
+
# @return [Boolean] true if role is :user
|
|
48
|
+
def user? = @role == :user
|
|
49
|
+
|
|
50
|
+
# @return [Boolean] true if role is :assistant
|
|
51
|
+
def assistant? = @role == :assistant
|
|
52
|
+
|
|
53
|
+
# @return [Boolean] true if role is :tool
|
|
54
|
+
def tool? = @role == :tool
|
|
55
|
+
|
|
56
|
+
# Convert to a hash suitable for provider wire format serialization.
|
|
57
|
+
# Omits nil-valued keys.
|
|
58
|
+
# @return [Hash]
|
|
59
|
+
def to_h
|
|
60
|
+
base = { role: @role }
|
|
61
|
+
base[:content] = @content if @content
|
|
62
|
+
base[:name] = @name if @name
|
|
63
|
+
base[:tool_call_id] = @tool_call_id if @tool_call_id
|
|
64
|
+
base[:tool_calls] = @tool_calls if @tool_calls
|
|
65
|
+
base
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [Boolean] true if role, content, name, and tool metadata all match
|
|
69
|
+
def ==(other)
|
|
70
|
+
return false unless other.is_a?(Message)
|
|
71
|
+
|
|
72
|
+
@role == other.role && @content == other.content &&
|
|
73
|
+
@name == other.name && @tool_call_id == other.tool_call_id &&
|
|
74
|
+
@tool_calls == other.tool_calls
|
|
75
|
+
end
|
|
76
|
+
alias eql? ==
|
|
77
|
+
|
|
78
|
+
def hash
|
|
79
|
+
[@role, @content, @name, @tool_call_id, @tool_calls].hash
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @return [String]
|
|
83
|
+
def inspect
|
|
84
|
+
"#<Ask::Message role=#{@role.inspect} content=#{@content && @content.length > 57 ? @content[0,57].inspect + "..." : @content.inspect}>"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def normalize_role!(role)
|
|
90
|
+
sym = role.to_s.downcase.to_sym
|
|
91
|
+
raise InvalidRole, "Invalid role: #{role.inspect}. Valid: #{VALID_ROLES.join(', ')}" unless VALID_ROLES.include?(sym)
|
|
92
|
+
|
|
93
|
+
sym
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def normalize_name(name)
|
|
97
|
+
return nil if name.nil?
|
|
98
|
+
name.to_s.strip.empty? ? nil : name.to_s.strip
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def validate!
|
|
102
|
+
# Allow nil content for assistant messages with tool calls
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Ordered collection of messages comprising a conversation with an LLM.
|
|
107
|
+
# Provides role normalization, serialization helpers, and Enumerable access.
|
|
108
|
+
#
|
|
109
|
+
# conv = Ask::Conversation.new
|
|
110
|
+
# conv << Ask::Message.new(role: :user, content: "Hello")
|
|
111
|
+
# conv.system("Be helpful")
|
|
112
|
+
# conv.to_a # => [{ role: :user, content: "Hello" }, ...]
|
|
113
|
+
#
|
|
114
|
+
class Conversation
|
|
115
|
+
include Enumerable
|
|
116
|
+
|
|
117
|
+
# @param messages [Array<Ask::Message>] initial messages
|
|
118
|
+
def initialize(messages = [])
|
|
119
|
+
@messages = []
|
|
120
|
+
messages.each { |m| self << m }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Add a message by object or by attributes.
|
|
124
|
+
# @param message [Ask::Message, Hash, String] message or attributes
|
|
125
|
+
# @return [self]
|
|
126
|
+
def <<(message)
|
|
127
|
+
msg = message.is_a?(Message) ? message : build_message(message)
|
|
128
|
+
@messages << msg
|
|
129
|
+
self
|
|
130
|
+
end
|
|
131
|
+
alias add <<
|
|
132
|
+
|
|
133
|
+
# Add a system message.
|
|
134
|
+
# @param text [String] message content
|
|
135
|
+
# @return [self]
|
|
136
|
+
def system(text, **options)
|
|
137
|
+
self << Message.new(role: :system, content: text, **options)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Add a user message.
|
|
141
|
+
# @param text [String] message content
|
|
142
|
+
# @return [self]
|
|
143
|
+
def user(text, **options)
|
|
144
|
+
self << Message.new(role: :user, content: text, **options)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Add an assistant message.
|
|
148
|
+
# @param text [String, nil] message content (nil when tool_calls are present)
|
|
149
|
+
# @param tool_calls [Array<Hash>, nil] tool call invocations
|
|
150
|
+
# @return [self]
|
|
151
|
+
def assistant(text = nil, tool_calls: nil, **options)
|
|
152
|
+
self << Message.new(role: :assistant, content: text, tool_calls: tool_calls, **options)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Add a tool result message.
|
|
156
|
+
# @param content [String] tool output
|
|
157
|
+
# @param tool_call_id [String] ID of the tool call this result is for
|
|
158
|
+
# @return [self]
|
|
159
|
+
def tool_result(content, tool_call_id:, **options)
|
|
160
|
+
self << Message.new(role: :tool, content: content, tool_call_id: tool_call_id, **options)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# @yield [Message] yields each message in order
|
|
164
|
+
# @return [Enumerator] if no block given
|
|
165
|
+
def each(&block)
|
|
166
|
+
@messages.each(&block)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# @return [Ask::Message, Array<Ask::Message>] last message or last n messages
|
|
170
|
+
def last(n = nil)
|
|
171
|
+
n ? @messages.last(n) : @messages.last
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# @return [Integer] number of messages
|
|
175
|
+
def length = @messages.length
|
|
176
|
+
alias size length
|
|
177
|
+
|
|
178
|
+
# @return [Boolean] true if there are no messages
|
|
179
|
+
def empty? = @messages.empty?
|
|
180
|
+
|
|
181
|
+
# Remove all messages.
|
|
182
|
+
# @return [self]
|
|
183
|
+
def clear
|
|
184
|
+
@messages.clear
|
|
185
|
+
self
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Access message by index.
|
|
189
|
+
# @param index [Integer] zero-based index
|
|
190
|
+
# @return [Ask::Message, nil]
|
|
191
|
+
def [](index)
|
|
192
|
+
@messages[index]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# @return [Array<Hash>] messages as an array of hashes
|
|
196
|
+
def to_a
|
|
197
|
+
@messages.map(&:to_h)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# @param role [Symbol, String] role to filter by
|
|
201
|
+
# @return [Array<Ask::Message>] messages with the given role
|
|
202
|
+
def by_role(role)
|
|
203
|
+
@messages.select { |m| m.role == role.to_sym }
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# @return [Array<Ask::Message>] system messages
|
|
207
|
+
def system_messages = by_role(:system)
|
|
208
|
+
|
|
209
|
+
# @return [Array<Ask::Message>] user messages
|
|
210
|
+
def user_messages = by_role(:user)
|
|
211
|
+
|
|
212
|
+
# @return [Array<Ask::Message>] assistant messages
|
|
213
|
+
def assistant_messages = by_role(:assistant)
|
|
214
|
+
|
|
215
|
+
# @return [Array<Ask::Message>] tool messages
|
|
216
|
+
def tool_messages = by_role(:tool)
|
|
217
|
+
|
|
218
|
+
# Deep copy of this conversation.
|
|
219
|
+
# @return [Ask::Conversation]
|
|
220
|
+
def dup
|
|
221
|
+
Conversation.new(@messages.map { |m| Message.new(**m.to_h) })
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# @return [String]
|
|
225
|
+
def inspect
|
|
226
|
+
"#<Ask::Conversation messages=#{@messages.length}>"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
private
|
|
230
|
+
|
|
231
|
+
def build_message(attrs)
|
|
232
|
+
attrs.is_a?(Hash) ? Message.new(**attrs) : Message.new(role: :user, content: attrs.to_s)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
data/lib/ask/errors.rb
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ask
|
|
4
|
+
# Base error class for all ask-rb errors.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when a provider is not configured properly.
|
|
8
|
+
class ConfigurationError < Error; end
|
|
9
|
+
|
|
10
|
+
# Raised when a required credential is missing.
|
|
11
|
+
class MissingCredential < Error; end
|
|
12
|
+
|
|
13
|
+
# Raised when a credential is invalid or expired.
|
|
14
|
+
class InvalidCredential < Error; end
|
|
15
|
+
|
|
16
|
+
# Raised when an unknown provider is requested.
|
|
17
|
+
class UnknownProvider < Error; end
|
|
18
|
+
|
|
19
|
+
# Raised when a model is not found in the catalog.
|
|
20
|
+
class ModelNotFound < Error; end
|
|
21
|
+
|
|
22
|
+
# Raised when a message has an invalid role.
|
|
23
|
+
class InvalidRole < Error; end
|
|
24
|
+
|
|
25
|
+
# Raised when a tool definition is invalid.
|
|
26
|
+
class InvalidToolDefinition < Error; end
|
|
27
|
+
|
|
28
|
+
# Raised when the context window is exceeded.
|
|
29
|
+
class ContextLengthExceeded < Error; end
|
|
30
|
+
|
|
31
|
+
# Raised when the API rate limit is hit.
|
|
32
|
+
class RateLimitError < Error; end
|
|
33
|
+
|
|
34
|
+
# Raised when authentication fails.
|
|
35
|
+
class Unauthorized < Error; end
|
|
36
|
+
|
|
37
|
+
# Raised when the server returns a 5xx error.
|
|
38
|
+
class ServerError < Error; end
|
|
39
|
+
|
|
40
|
+
# Raised when the service is unavailable.
|
|
41
|
+
class ServiceUnavailable < Error; end
|
|
42
|
+
|
|
43
|
+
# Raised when a provider's API returns an unexpected response.
|
|
44
|
+
# @!attribute [r] status_code
|
|
45
|
+
# @return [Integer, nil] the HTTP status code
|
|
46
|
+
# @!attribute [r] response_body
|
|
47
|
+
# @return [String, nil] the raw response body
|
|
48
|
+
class ProviderError < Error
|
|
49
|
+
attr_reader :status_code, :response_body
|
|
50
|
+
|
|
51
|
+
def initialize(message = nil, status_code: nil, response_body: nil)
|
|
52
|
+
@status_code = status_code
|
|
53
|
+
@response_body = response_body
|
|
54
|
+
super(message)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Raised when a conversation operation receives an invalid state.
|
|
59
|
+
class ConversationError < Error; end
|
|
60
|
+
|
|
61
|
+
# Raised when streaming encounters an error.
|
|
62
|
+
class StreamError < Error; end
|
|
63
|
+
|
|
64
|
+
# Raised when a feature is not supported by the selected provider/model.
|
|
65
|
+
class UnsupportedFeature < Error; end
|
|
66
|
+
end
|