ollama-client 0.2.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/CHANGELOG.md +12 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/CONTRIBUTING.md +36 -0
- data/LICENSE.txt +21 -0
- data/PRODUCTION_FIXES.md +172 -0
- data/README.md +690 -0
- data/Rakefile +12 -0
- data/TESTING.md +286 -0
- data/examples/advanced_complex_schemas.rb +363 -0
- data/examples/advanced_edge_cases.rb +241 -0
- data/examples/advanced_error_handling.rb +200 -0
- data/examples/advanced_multi_step_agent.rb +258 -0
- data/examples/advanced_performance_testing.rb +186 -0
- data/examples/complete_workflow.rb +235 -0
- data/examples/dhanhq_agent.rb +752 -0
- data/examples/dhanhq_tools.rb +563 -0
- data/examples/structured_outputs_chat.rb +72 -0
- data/examples/tool_calling_pattern.rb +266 -0
- data/exe/ollama-client +4 -0
- data/lib/ollama/agent/executor.rb +157 -0
- data/lib/ollama/agent/messages.rb +31 -0
- data/lib/ollama/agent/planner.rb +47 -0
- data/lib/ollama/client.rb +775 -0
- data/lib/ollama/config.rb +29 -0
- data/lib/ollama/errors.rb +54 -0
- data/lib/ollama/schema_validator.rb +79 -0
- data/lib/ollama/schemas/base.json +5 -0
- data/lib/ollama/streaming_observer.rb +22 -0
- data/lib/ollama/version.rb +5 -0
- data/lib/ollama_client.rb +46 -0
- data/sig/ollama/client.rbs +6 -0
- metadata +108 -0
data/README.md
ADDED
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
# Ollama::Client
|
|
2
|
+
|
|
3
|
+
> An **agent-first Ruby client for Ollama**, optimized for **deterministic planners** and **safe tool-using executors**.
|
|
4
|
+
|
|
5
|
+
This is **NOT** a chatbot UI,
|
|
6
|
+
**NOT** domain-specific,
|
|
7
|
+
**NOT** a general-purpose “everything Ollama supports” wrapper.
|
|
8
|
+
|
|
9
|
+
This gem provides:
|
|
10
|
+
|
|
11
|
+
* ✅ Safe LLM calls
|
|
12
|
+
* ✅ Strict output contracts
|
|
13
|
+
* ✅ Retry & timeout handling
|
|
14
|
+
* ✅ Explicit state (Planner is stateless; Executor is intentionally stateful via `messages`)
|
|
15
|
+
* ✅ Extensible schemas
|
|
16
|
+
|
|
17
|
+
Domain tools and application logic live **outside** this gem. For convenience, it includes a small `Ollama::Agent` layer (Planner + Executor) that encodes correct agent usage.
|
|
18
|
+
|
|
19
|
+
## 🎯 What This Gem IS
|
|
20
|
+
|
|
21
|
+
* LLM call executor
|
|
22
|
+
* Output validator
|
|
23
|
+
* Retry + timeout manager
|
|
24
|
+
* Schema enforcer
|
|
25
|
+
* A minimal agent layer (`Ollama::Agent::Planner` + `Ollama::Agent::Executor`)
|
|
26
|
+
|
|
27
|
+
## 🚫 What This Gem IS NOT
|
|
28
|
+
|
|
29
|
+
* ❌ Domain tool implementations
|
|
30
|
+
* ❌ Domain logic
|
|
31
|
+
* ❌ Memory store
|
|
32
|
+
* ❌ Chat UI
|
|
33
|
+
* ❌ A promise of full Ollama API coverage (it focuses on agent workflows)
|
|
34
|
+
|
|
35
|
+
This keeps it **clean and future-proof**.
|
|
36
|
+
|
|
37
|
+
## 🔒 Guarantees
|
|
38
|
+
|
|
39
|
+
| Guarantee | Yes |
|
|
40
|
+
| -------------------------------------- | --- |
|
|
41
|
+
| Client requests are explicit | ✅ |
|
|
42
|
+
| Planner is stateless (no hidden memory)| ✅ |
|
|
43
|
+
| Executor is stateful (explicit messages)| ✅ |
|
|
44
|
+
| Retry bounded | ✅ |
|
|
45
|
+
| Schema validated (when schema provided)| ✅ |
|
|
46
|
+
| Tools run in Ruby (not in the LLM) | ✅ |
|
|
47
|
+
| Streaming is display-only (Executor) | ✅ |
|
|
48
|
+
|
|
49
|
+
**Non-negotiable safety rule:** the **LLM never executes side effects**. It may request a tool call; **your Ruby code** executes the tool.
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
Add this line to your application's Gemfile:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
gem "ollama-client"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
And then execute:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
bundle install
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or install it yourself as:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
gem install ollama-client
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
**Note:** You can use `require "ollama_client"` (recommended) or `require "ollama/client"` directly. The client works with or without the global `OllamaClient` configuration module.
|
|
74
|
+
|
|
75
|
+
### Primary API: `generate()`
|
|
76
|
+
|
|
77
|
+
**`generate(prompt:, schema:)`** is the **primary and recommended method** for agent-grade usage:
|
|
78
|
+
|
|
79
|
+
- ✅ Stateless, explicit state injection
|
|
80
|
+
- ✅ Uses `/api/generate` endpoint
|
|
81
|
+
- ✅ Ideal for: agent planning, tool routing, one-shot analysis, classification, extraction
|
|
82
|
+
- ✅ No implicit memory or conversation history
|
|
83
|
+
|
|
84
|
+
**This is the method you should use for hybrid agents.**
|
|
85
|
+
|
|
86
|
+
### Choosing the Correct API (generate vs chat)
|
|
87
|
+
|
|
88
|
+
- **Use `/api/generate`** (via `Ollama::Client#generate` or `Ollama::Agent::Planner`) for **stateless planner/router** steps where you want strict, deterministic structured outputs.
|
|
89
|
+
- **Use `/api/chat`** (via `Ollama::Agent::Executor`) for **stateful tool-using** workflows where the model may request tool calls across multiple turns.
|
|
90
|
+
|
|
91
|
+
**Warnings:**
|
|
92
|
+
- Don’t use `generate()` for tool-calling loops (you’ll end up re-implementing message/tool lifecycles).
|
|
93
|
+
- Don’t use `chat()` for deterministic planners unless you’re intentionally managing conversation state.
|
|
94
|
+
- Don’t let streaming output drive decisions (streaming is presentation-only).
|
|
95
|
+
|
|
96
|
+
### Scope / endpoint coverage
|
|
97
|
+
|
|
98
|
+
This gem intentionally focuses on **agent building blocks**:
|
|
99
|
+
|
|
100
|
+
- **Supported**: `/api/generate`, `/api/chat`, `/api/tags`, `/api/ping`
|
|
101
|
+
- **Not guaranteed**: full endpoint parity with every Ollama release (embeddings, advanced model mgmt, etc.)
|
|
102
|
+
|
|
103
|
+
### Agent endpoint mapping (unambiguous)
|
|
104
|
+
|
|
105
|
+
Within `Ollama::Agent`:
|
|
106
|
+
|
|
107
|
+
- `Ollama::Agent::Planner` **always** uses `/api/generate`
|
|
108
|
+
- `Ollama::Agent::Executor` **always** uses `/api/chat`
|
|
109
|
+
|
|
110
|
+
(`Ollama::Client` remains the low-level API surface.)
|
|
111
|
+
|
|
112
|
+
### Planner Agent (stateless, /api/generate)
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
require "ollama_client"
|
|
116
|
+
|
|
117
|
+
client = Ollama::Client.new
|
|
118
|
+
planner = Ollama::Agent::Planner.new(client)
|
|
119
|
+
|
|
120
|
+
plan = planner.run(
|
|
121
|
+
prompt: <<~PROMPT,
|
|
122
|
+
Given the user request, output a JSON plan with steps.
|
|
123
|
+
Return ONLY valid JSON.
|
|
124
|
+
PROMPT
|
|
125
|
+
context: { user_request: "Plan a weekend trip to Rome" }
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
puts plan
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Executor Agent (tool loop, /api/chat)
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
require "ollama_client"
|
|
135
|
+
require "json"
|
|
136
|
+
|
|
137
|
+
client = Ollama::Client.new
|
|
138
|
+
|
|
139
|
+
tools = {
|
|
140
|
+
"fetch_weather" => ->(city:) { { city: city, forecast: "sunny", high_c: 18, low_c: 10 } },
|
|
141
|
+
"find_hotels" => ->(city:, max_price:) { [{ name: "Hotel Example", city: city, price_per_night: max_price }] }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
executor = Ollama::Agent::Executor.new(client, tools: tools)
|
|
145
|
+
|
|
146
|
+
answer = executor.run(
|
|
147
|
+
system: "You are a travel assistant. Use tools when you need real data.",
|
|
148
|
+
user: "Plan a 3-day trip to Paris in October. Use tools for weather and hotels."
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
puts answer
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Streaming (Executor only; presentation-only)
|
|
155
|
+
|
|
156
|
+
Streaming is treated as **presentation**, not control. The agent buffers the full assistant message and only
|
|
157
|
+
executes tools after the streamed message is complete and parsed.
|
|
158
|
+
|
|
159
|
+
**Streaming format support:**
|
|
160
|
+
- The streaming parser accepts **NDJSON** (one JSON object per line).
|
|
161
|
+
- It also tolerates **SSE-style** lines prefixed with `data: ` (common in proxies), as long as the payload is JSON.
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
observer = Ollama::StreamingObserver.new do |event|
|
|
165
|
+
case event.type
|
|
166
|
+
when :token
|
|
167
|
+
print event.text
|
|
168
|
+
when :tool_call_detected
|
|
169
|
+
puts "\n[Tool requested: #{event.name}]"
|
|
170
|
+
when :final
|
|
171
|
+
puts "\n--- DONE ---"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
executor = Ollama::Agent::Executor.new(client, tools: tools, stream: observer)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### JSON & schema contracts (including “no extra fields”)
|
|
179
|
+
|
|
180
|
+
This gem is contract-first:
|
|
181
|
+
|
|
182
|
+
- **JSON parsing**: invalid JSON raises `Ollama::InvalidJSONError` (no silent fallback to text).
|
|
183
|
+
- **Schema validation**: invalid outputs raise `Ollama::SchemaViolationError`.
|
|
184
|
+
- **No extra fields by default**: object schemas are treated as strict shapes unless you explicitly allow more fields.
|
|
185
|
+
- To allow extras, set `"additionalProperties" => true` on the relevant object schema.
|
|
186
|
+
|
|
187
|
+
**Strictness control:** methods accept `strict:` to fail fast (no retries on invalid JSON/schema) vs retry within configured bounds.
|
|
188
|
+
|
|
189
|
+
### Basic Configuration
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
require "ollama_client"
|
|
193
|
+
|
|
194
|
+
# Configure global defaults
|
|
195
|
+
OllamaClient.configure do |c|
|
|
196
|
+
c.base_url = "http://localhost:11434"
|
|
197
|
+
c.model = "llama3.1"
|
|
198
|
+
c.timeout = 30
|
|
199
|
+
c.retries = 3
|
|
200
|
+
c.temperature = 0.2
|
|
201
|
+
end
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Quick Start Pattern
|
|
205
|
+
|
|
206
|
+
The basic pattern for using structured outputs:
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
require "ollama_client"
|
|
210
|
+
|
|
211
|
+
client = Ollama::Client.new
|
|
212
|
+
|
|
213
|
+
# 1. Define your JSON schema
|
|
214
|
+
schema = {
|
|
215
|
+
"type" => "object",
|
|
216
|
+
"required" => ["field1", "field2"],
|
|
217
|
+
"properties" => {
|
|
218
|
+
"field1" => { "type" => "string" },
|
|
219
|
+
"field2" => { "type" => "number" }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# 2. Call the LLM with your schema
|
|
224
|
+
begin
|
|
225
|
+
result = client.generate(
|
|
226
|
+
prompt: "Your prompt here",
|
|
227
|
+
schema: schema
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# 3. Use the validated structured output
|
|
231
|
+
puts result["field1"]
|
|
232
|
+
puts result["field2"]
|
|
233
|
+
|
|
234
|
+
# The result is guaranteed to match your schema!
|
|
235
|
+
|
|
236
|
+
rescue Ollama::SchemaViolationError => e
|
|
237
|
+
# Handle validation errors (rare with format parameter)
|
|
238
|
+
puts "Invalid response: #{e.message}"
|
|
239
|
+
rescue Ollama::Error => e
|
|
240
|
+
# Handle other errors
|
|
241
|
+
puts "Error: #{e.message}"
|
|
242
|
+
end
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Example: Planning Agent (Complete Workflow)
|
|
246
|
+
|
|
247
|
+
```ruby
|
|
248
|
+
require "ollama_client"
|
|
249
|
+
|
|
250
|
+
client = Ollama::Client.new
|
|
251
|
+
|
|
252
|
+
# Define the schema for decision-making
|
|
253
|
+
decision_schema = {
|
|
254
|
+
"type" => "object",
|
|
255
|
+
"required" => ["action", "reasoning", "confidence"],
|
|
256
|
+
"properties" => {
|
|
257
|
+
"action" => {
|
|
258
|
+
"type" => "string",
|
|
259
|
+
"enum" => ["search", "calculate", "finish"],
|
|
260
|
+
"description" => "The action to take: 'search', 'calculate', or 'finish'"
|
|
261
|
+
},
|
|
262
|
+
"reasoning" => {
|
|
263
|
+
"type" => "string",
|
|
264
|
+
"description" => "Why this action was chosen"
|
|
265
|
+
},
|
|
266
|
+
"confidence" => {
|
|
267
|
+
"type" => "number",
|
|
268
|
+
"minimum" => 0,
|
|
269
|
+
"maximum" => 1,
|
|
270
|
+
"description" => "Confidence level in this decision"
|
|
271
|
+
},
|
|
272
|
+
"parameters" => {
|
|
273
|
+
"type" => "object",
|
|
274
|
+
"description" => "Parameters needed for the action"
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
# Get structured decision from LLM
|
|
280
|
+
begin
|
|
281
|
+
result = client.generate(
|
|
282
|
+
prompt: "Analyze the current situation and decide the next step. Context: User asked about weather in Paris.",
|
|
283
|
+
schema: decision_schema
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Use the structured output
|
|
287
|
+
puts "Action: #{result['action']}"
|
|
288
|
+
puts "Reasoning: #{result['reasoning']}"
|
|
289
|
+
puts "Confidence: #{(result['confidence'] * 100).round}%"
|
|
290
|
+
|
|
291
|
+
# Route based on action
|
|
292
|
+
case result["action"]
|
|
293
|
+
when "search"
|
|
294
|
+
# Execute search with parameters
|
|
295
|
+
query = result.dig("parameters", "query") || "default query"
|
|
296
|
+
puts "Executing search: #{query}"
|
|
297
|
+
# ... your search logic here
|
|
298
|
+
when "calculate"
|
|
299
|
+
# Execute calculation
|
|
300
|
+
puts "Executing calculation with params: #{result['parameters']}"
|
|
301
|
+
# ... your calculation logic here
|
|
302
|
+
when "finish"
|
|
303
|
+
puts "Task complete!"
|
|
304
|
+
else
|
|
305
|
+
puts "Unknown action: #{result['action']}"
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
rescue Ollama::SchemaViolationError => e
|
|
309
|
+
puts "LLM returned invalid structure: #{e.message}"
|
|
310
|
+
# Handle gracefully - maybe retry or use fallback
|
|
311
|
+
rescue Ollama::Error => e
|
|
312
|
+
puts "Error: #{e.message}"
|
|
313
|
+
end
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**Note:** The gem uses Ollama's native `format` parameter for structured outputs, which enforces the JSON schema server-side. This ensures reliable, consistent JSON responses that match your schema exactly.
|
|
317
|
+
|
|
318
|
+
### Advanced: When (Rarely) to Use `chat()`
|
|
319
|
+
|
|
320
|
+
⚠️ **Warning:** `chat()` is **NOT recommended** for agent planning or tool routing.
|
|
321
|
+
|
|
322
|
+
**Safety gate:** `chat()` requires explicit opt-in (`allow_chat: true`) so you don’t accidentally use it inside agent internals.
|
|
323
|
+
|
|
324
|
+
**Why?**
|
|
325
|
+
- Chat encourages implicit memory and conversation history
|
|
326
|
+
- Message history grows silently over time
|
|
327
|
+
- Schema validation becomes weaker with accumulated context
|
|
328
|
+
- Harder to reason about state in agent systems
|
|
329
|
+
|
|
330
|
+
**When to use `chat()`:**
|
|
331
|
+
- User-facing chat interfaces (not agent internals)
|
|
332
|
+
- Explicit multi-turn conversations where you control message history
|
|
333
|
+
- When you need conversation context for a specific use case
|
|
334
|
+
|
|
335
|
+
**For agents, prefer `generate()` with explicit state injection:**
|
|
336
|
+
|
|
337
|
+
```ruby
|
|
338
|
+
# ✅ GOOD: Explicit state in prompt
|
|
339
|
+
context = "Previous actions: #{actions.join(', ')}"
|
|
340
|
+
result = client.generate(
|
|
341
|
+
prompt: "Given context: #{context}. Decide next action.",
|
|
342
|
+
schema: decision_schema
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# ❌ AVOID: Implicit conversation history
|
|
346
|
+
messages = [{ role: "user", content: "..." }]
|
|
347
|
+
result = client.chat(messages: messages, format: schema, allow_chat: true) # History grows silently
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Example: Chat API (Advanced Use Case)
|
|
351
|
+
|
|
352
|
+
```ruby
|
|
353
|
+
require "ollama_client"
|
|
354
|
+
require "json"
|
|
355
|
+
|
|
356
|
+
client = Ollama::Client.new
|
|
357
|
+
|
|
358
|
+
# Define schema for friend list
|
|
359
|
+
friend_list_schema = {
|
|
360
|
+
"type" => "object",
|
|
361
|
+
"required" => ["friends"],
|
|
362
|
+
"properties" => {
|
|
363
|
+
"friends" => {
|
|
364
|
+
"type" => "array",
|
|
365
|
+
"items" => {
|
|
366
|
+
"type" => "object",
|
|
367
|
+
"required" => ["name", "age", "is_available"],
|
|
368
|
+
"properties" => {
|
|
369
|
+
"name" => { "type" => "string" },
|
|
370
|
+
"age" => { "type" => "integer" },
|
|
371
|
+
"is_available" => { "type" => "boolean" }
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
# Use chat API with messages (for user-facing interfaces, not agent internals)
|
|
379
|
+
messages = [
|
|
380
|
+
{
|
|
381
|
+
role: "user",
|
|
382
|
+
content: "I have two friends. The first is Ollama 22 years old busy saving the world, and the second is Alonso 23 years old and wants to hang out. Return a list of friends in JSON format"
|
|
383
|
+
}
|
|
384
|
+
]
|
|
385
|
+
|
|
386
|
+
begin
|
|
387
|
+
response = client.chat(
|
|
388
|
+
model: "llama3.1:8b",
|
|
389
|
+
messages: messages,
|
|
390
|
+
format: friend_list_schema,
|
|
391
|
+
allow_chat: true,
|
|
392
|
+
options: {
|
|
393
|
+
temperature: 0 # More deterministic
|
|
394
|
+
}
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# Response is already parsed and validated
|
|
398
|
+
response["friends"].each do |friend|
|
|
399
|
+
status = friend["is_available"] ? "available" : "busy"
|
|
400
|
+
puts "#{friend['name']} (#{friend['age']}) - #{status}"
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
rescue Ollama::SchemaViolationError => e
|
|
404
|
+
puts "Response didn't match schema: #{e.message}"
|
|
405
|
+
rescue Ollama::Error => e
|
|
406
|
+
puts "Error: #{e.message}"
|
|
407
|
+
end
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Example: Data Analysis with Validation
|
|
411
|
+
|
|
412
|
+
```ruby
|
|
413
|
+
require "ollama_client"
|
|
414
|
+
|
|
415
|
+
client = Ollama::Client.new
|
|
416
|
+
|
|
417
|
+
analysis_schema = {
|
|
418
|
+
"type" => "object",
|
|
419
|
+
"required" => ["summary", "confidence", "key_points"],
|
|
420
|
+
"properties" => {
|
|
421
|
+
"summary" => { "type" => "string" },
|
|
422
|
+
"confidence" => {
|
|
423
|
+
"type" => "number",
|
|
424
|
+
"minimum" => 0,
|
|
425
|
+
"maximum" => 1
|
|
426
|
+
},
|
|
427
|
+
"key_points" => {
|
|
428
|
+
"type" => "array",
|
|
429
|
+
"items" => { "type" => "string" },
|
|
430
|
+
"minItems" => 1,
|
|
431
|
+
"maxItems" => 5
|
|
432
|
+
},
|
|
433
|
+
"sentiment" => {
|
|
434
|
+
"type" => "string",
|
|
435
|
+
"enum" => ["positive", "neutral", "negative"]
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
data = "Sales increased 25% this quarter, customer satisfaction is at 4.8/5"
|
|
441
|
+
|
|
442
|
+
begin
|
|
443
|
+
result = client.generate(
|
|
444
|
+
prompt: "Analyze this data: #{data}",
|
|
445
|
+
schema: analysis_schema
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# Use the validated structured output
|
|
449
|
+
puts "Summary: #{result['summary']}"
|
|
450
|
+
puts "Confidence: #{(result['confidence'] * 100).round}%"
|
|
451
|
+
puts "Sentiment: #{result['sentiment']}"
|
|
452
|
+
puts "\nKey Points:"
|
|
453
|
+
result["key_points"].each_with_index do |point, i|
|
|
454
|
+
puts " #{i + 1}. #{point}"
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# Make decisions based on structured data
|
|
458
|
+
if result["confidence"] > 0.8 && result["sentiment"] == "positive"
|
|
459
|
+
puts "\n✅ High confidence positive analysis - proceed with action"
|
|
460
|
+
elsif result["confidence"] < 0.5
|
|
461
|
+
puts "\n⚠️ Low confidence - review manually"
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
rescue Ollama::SchemaViolationError => e
|
|
465
|
+
puts "Analysis failed validation: #{e.message}"
|
|
466
|
+
# Could retry or use fallback logic
|
|
467
|
+
rescue Ollama::TimeoutError => e
|
|
468
|
+
puts "Request timed out: #{e.message}"
|
|
469
|
+
rescue Ollama::Error => e
|
|
470
|
+
puts "Error: #{e.message}"
|
|
471
|
+
end
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### Custom Configuration Per Client
|
|
475
|
+
|
|
476
|
+
**Important:** For production agents, prefer per-client configuration over global config to avoid thread-safety issues.
|
|
477
|
+
|
|
478
|
+
```ruby
|
|
479
|
+
require "ollama_client"
|
|
480
|
+
|
|
481
|
+
# Prefer per-client config for agents (thread-safe)
|
|
482
|
+
custom_config = Ollama::Config.new
|
|
483
|
+
custom_config.model = "qwen2.5:14b"
|
|
484
|
+
custom_config.temperature = 0.1
|
|
485
|
+
custom_config.timeout = 60 # Increase timeout for complex schemas
|
|
486
|
+
|
|
487
|
+
client = Ollama::Client.new(config: custom_config)
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
**Note:** Global `OllamaClient.configure` is convenient for defaults, but is **not thread-safe by default**. For concurrent agents, use per-client configuration.
|
|
491
|
+
|
|
492
|
+
**Timeout Tips:**
|
|
493
|
+
- Default timeout is 20 seconds
|
|
494
|
+
- For complex schemas or large prompts, increase to 60-120 seconds
|
|
495
|
+
- For simple schemas, 20 seconds is usually sufficient
|
|
496
|
+
- Timeout applies per request (not total workflow time)
|
|
497
|
+
|
|
498
|
+
### Listing Available Models
|
|
499
|
+
|
|
500
|
+
```ruby
|
|
501
|
+
require "ollama_client"
|
|
502
|
+
|
|
503
|
+
client = Ollama::Client.new
|
|
504
|
+
models = client.list_models
|
|
505
|
+
puts "Available models: #{models.join(', ')}"
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Error Handling
|
|
509
|
+
|
|
510
|
+
```ruby
|
|
511
|
+
require "ollama_client"
|
|
512
|
+
|
|
513
|
+
begin
|
|
514
|
+
result = client.generate(prompt: prompt, schema: schema)
|
|
515
|
+
rescue Ollama::NotFoundError => e
|
|
516
|
+
# 404 Not Found - model or endpoint doesn't exist
|
|
517
|
+
# The error message automatically suggests similar model names if available
|
|
518
|
+
puts e.message
|
|
519
|
+
# Example output:
|
|
520
|
+
# HTTP 404: Not Found
|
|
521
|
+
#
|
|
522
|
+
# Model 'qwen2.5:7b' not found. Did you mean one of these?
|
|
523
|
+
# - qwen2.5:14b
|
|
524
|
+
# - qwen2.5:32b
|
|
525
|
+
rescue Ollama::HTTPError => e
|
|
526
|
+
# Other HTTP errors (400, 500, etc.)
|
|
527
|
+
# Non-retryable errors (400) are raised immediately
|
|
528
|
+
# Retryable errors (500, 503, 408, 429) are retried
|
|
529
|
+
puts "HTTP #{e.status_code}: #{e.message}"
|
|
530
|
+
rescue Ollama::TimeoutError => e
|
|
531
|
+
puts "Request timed out: #{e.message}"
|
|
532
|
+
rescue Ollama::SchemaViolationError => e
|
|
533
|
+
puts "Output didn't match schema: #{e.message}"
|
|
534
|
+
rescue Ollama::RetryExhaustedError => e
|
|
535
|
+
puts "Failed after retries: #{e.message}"
|
|
536
|
+
rescue Ollama::Error => e
|
|
537
|
+
puts "Error: #{e.message}"
|
|
538
|
+
end
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
## Architecture: Tool Calling Pattern
|
|
542
|
+
|
|
543
|
+
**Important:** This gem includes a tool-calling *loop helper* (`Ollama::Agent::Executor`), but it still does **not** include any domain tools. Tool execution remains **pure Ruby** and **outside the LLM**.
|
|
544
|
+
|
|
545
|
+
### Why Tools Still Don’t “Belong in the LLM”
|
|
546
|
+
|
|
547
|
+
Tool execution is an **orchestration concern**, not an LLM concern. The correct pattern is:
|
|
548
|
+
|
|
549
|
+
```
|
|
550
|
+
┌──────────────────────────┐
|
|
551
|
+
│ Your Agent / App │
|
|
552
|
+
│ │
|
|
553
|
+
│ ┌──────── Tool Router ┐ │
|
|
554
|
+
│ │ │ │
|
|
555
|
+
│ │ ┌─ Ollama Client ┐│ │ ← This gem (reasoning only)
|
|
556
|
+
│ │ │ (outputs intent)││ │
|
|
557
|
+
│ │ └────────────────┘│ │
|
|
558
|
+
│ │ ↓ │ │
|
|
559
|
+
│ │ Tool Registry │ │ ← Your code
|
|
560
|
+
│ │ ↓ │ │
|
|
561
|
+
│ │ Tool Executor │ │ ← Your code
|
|
562
|
+
│ └────────────────────┘ │
|
|
563
|
+
└──────────────────────────┘
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### The Correct Pattern
|
|
567
|
+
|
|
568
|
+
1. **LLM requests a tool call** (via `/api/chat` + tool definitions)
|
|
569
|
+
2. **Your agent executes the tool deterministically** (pure Ruby, no LLM calls)
|
|
570
|
+
3. **Tool result is appended as `role: "tool"`**
|
|
571
|
+
4. **LLM continues** until no more tool calls
|
|
572
|
+
|
|
573
|
+
**Key principle:** LLMs describe intent. Agents execute tools.
|
|
574
|
+
|
|
575
|
+
### Example: Tool-Aware Agent
|
|
576
|
+
|
|
577
|
+
```ruby
|
|
578
|
+
# In your agent code (NOT in this gem)
|
|
579
|
+
class ToolRouter
|
|
580
|
+
def initialize(llm:, registry:)
|
|
581
|
+
@llm = llm # Ollama::Client instance
|
|
582
|
+
@registry = registry
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
def step(prompt:, context:)
|
|
586
|
+
# LLM outputs intent (not execution)
|
|
587
|
+
decision = @llm.generate(
|
|
588
|
+
prompt: prompt,
|
|
589
|
+
schema: {
|
|
590
|
+
"type" => "object",
|
|
591
|
+
"required" => ["action"],
|
|
592
|
+
"properties" => {
|
|
593
|
+
"action" => { "type" => "string" },
|
|
594
|
+
"input" => { "type" => "object" }
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
return { done: true } if decision["action"] == "finish"
|
|
600
|
+
|
|
601
|
+
# Agent executes tool (deterministic)
|
|
602
|
+
tool = @registry.fetch(decision["action"])
|
|
603
|
+
output = tool.call(input: decision["input"], context: context)
|
|
604
|
+
|
|
605
|
+
{ tool: tool.name, output: output }
|
|
606
|
+
end
|
|
607
|
+
end
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
This keeps the `ollama-client` gem **domain-agnostic** and **reusable** across any project.
|
|
611
|
+
|
|
612
|
+
**See `examples/tool_calling_pattern.rb` for a working implementation of this pattern.**
|
|
613
|
+
|
|
614
|
+
## Advanced Examples
|
|
615
|
+
|
|
616
|
+
The `examples/` directory contains advanced examples demonstrating production-grade patterns:
|
|
617
|
+
|
|
618
|
+
### `tool_calling_pattern.rb`
|
|
619
|
+
**Working implementation of the ToolRouter pattern from the Architecture section:**
|
|
620
|
+
- Tool registry and routing
|
|
621
|
+
- LLM outputs intent, agent executes tools
|
|
622
|
+
- Demonstrates the correct separation of concerns
|
|
623
|
+
- Matches the pattern shown in README.md lines 430-500
|
|
624
|
+
|
|
625
|
+
### `dhanhq_trading_agent.rb`
|
|
626
|
+
**Real-world integration: Ollama (reasoning) + DhanHQ (execution):**
|
|
627
|
+
- Ollama analyzes market data and makes trading decisions
|
|
628
|
+
- DhanHQ executes trades (place orders, check positions, etc.)
|
|
629
|
+
- Demonstrates proper separation: LLM = reasoning, DhanHQ = execution
|
|
630
|
+
- Shows risk management with super orders (SL/TP)
|
|
631
|
+
- Perfect example of agent-grade tool calling pattern
|
|
632
|
+
|
|
633
|
+
### `advanced_multi_step_agent.rb`
|
|
634
|
+
Multi-step agent workflow with:
|
|
635
|
+
- Complex nested schemas
|
|
636
|
+
- State management across steps
|
|
637
|
+
- Confidence thresholds
|
|
638
|
+
- Risk assessment
|
|
639
|
+
- Error recovery
|
|
640
|
+
|
|
641
|
+
### `advanced_error_handling.rb`
|
|
642
|
+
Comprehensive error handling patterns:
|
|
643
|
+
- All error types (NotFoundError, HTTPError, TimeoutError, etc.)
|
|
644
|
+
- Retry strategies with exponential backoff
|
|
645
|
+
- Fallback mechanisms
|
|
646
|
+
- Error statistics and observability
|
|
647
|
+
|
|
648
|
+
### `advanced_complex_schemas.rb`
|
|
649
|
+
Real-world complex schemas:
|
|
650
|
+
- Financial analysis (nested metrics, recommendations, risk factors)
|
|
651
|
+
- Code review (issues, suggestions, effort estimation)
|
|
652
|
+
- Research paper analysis (findings, methodology, citations)
|
|
653
|
+
|
|
654
|
+
### `advanced_performance_testing.rb`
|
|
655
|
+
Performance and observability:
|
|
656
|
+
- Latency measurement (min, max, avg, p95, p99)
|
|
657
|
+
- Throughput testing
|
|
658
|
+
- Error rate tracking
|
|
659
|
+
- Metrics export
|
|
660
|
+
|
|
661
|
+
### `advanced_edge_cases.rb`
|
|
662
|
+
Boundary and edge case testing:
|
|
663
|
+
- Empty/long prompts
|
|
664
|
+
- Special characters and unicode
|
|
665
|
+
- Minimal/strict schemas
|
|
666
|
+
- Deeply nested structures
|
|
667
|
+
- Enum constraints
|
|
668
|
+
|
|
669
|
+
Run any example:
|
|
670
|
+
```bash
|
|
671
|
+
ruby examples/advanced_multi_step_agent.rb
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
## Development
|
|
675
|
+
|
|
676
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
677
|
+
|
|
678
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
679
|
+
|
|
680
|
+
## Contributing
|
|
681
|
+
|
|
682
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/shubhamtaywade82/ollama-client. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/shubhamtaywade82/ollama-client/blob/main/CODE_OF_CONDUCT.md).
|
|
683
|
+
|
|
684
|
+
## License
|
|
685
|
+
|
|
686
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
687
|
+
|
|
688
|
+
## Code of Conduct
|
|
689
|
+
|
|
690
|
+
Everyone interacting in the Ollama::Client project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/shubhamtaywade82/ollama-client/blob/main/CODE_OF_CONDUCT.md).
|