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.
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).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]