ollama-client 0.2.4 → 0.2.6

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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -1
  3. data/README.md +560 -106
  4. data/docs/EXAMPLE_REORGANIZATION.md +412 -0
  5. data/docs/GETTING_STARTED.md +361 -0
  6. data/docs/INTEGRATION_TESTING.md +170 -0
  7. data/docs/NEXT_STEPS_SUMMARY.md +114 -0
  8. data/docs/PERSONAS.md +383 -0
  9. data/docs/QUICK_START.md +195 -0
  10. data/docs/README.md +2 -3
  11. data/docs/RELEASE_GUIDE.md +376 -0
  12. data/docs/TESTING.md +392 -170
  13. data/docs/TEST_CHECKLIST.md +450 -0
  14. data/docs/ruby_guide.md +6232 -0
  15. data/examples/README.md +51 -66
  16. data/examples/basic_chat.rb +33 -0
  17. data/examples/basic_generate.rb +29 -0
  18. data/examples/tool_calling_parsing.rb +59 -0
  19. data/exe/ollama-client +128 -1
  20. data/lib/ollama/agent/planner.rb +7 -2
  21. data/lib/ollama/chat_session.rb +101 -0
  22. data/lib/ollama/client.rb +43 -21
  23. data/lib/ollama/config.rb +4 -1
  24. data/lib/ollama/document_loader.rb +163 -0
  25. data/lib/ollama/embeddings.rb +42 -13
  26. data/lib/ollama/errors.rb +1 -0
  27. data/lib/ollama/personas.rb +287 -0
  28. data/lib/ollama/version.rb +1 -1
  29. data/lib/ollama_client.rb +8 -0
  30. metadata +31 -53
  31. data/docs/GEM_RELEASE_GUIDE.md +0 -794
  32. data/docs/GET_RUBYGEMS_SECRET.md +0 -151
  33. data/docs/QUICK_OTP_SETUP.md +0 -80
  34. data/docs/QUICK_RELEASE.md +0 -106
  35. data/docs/RUBYGEMS_OTP_SETUP.md +0 -199
  36. data/examples/advanced_complex_schemas.rb +0 -366
  37. data/examples/advanced_edge_cases.rb +0 -241
  38. data/examples/advanced_error_handling.rb +0 -200
  39. data/examples/advanced_multi_step_agent.rb +0 -341
  40. data/examples/advanced_performance_testing.rb +0 -186
  41. data/examples/chat_console.rb +0 -143
  42. data/examples/complete_workflow.rb +0 -245
  43. data/examples/dhan_console.rb +0 -843
  44. data/examples/dhanhq/README.md +0 -236
  45. data/examples/dhanhq/agents/base_agent.rb +0 -74
  46. data/examples/dhanhq/agents/data_agent.rb +0 -66
  47. data/examples/dhanhq/agents/orchestrator_agent.rb +0 -120
  48. data/examples/dhanhq/agents/technical_analysis_agent.rb +0 -252
  49. data/examples/dhanhq/agents/trading_agent.rb +0 -81
  50. data/examples/dhanhq/analysis/market_structure.rb +0 -138
  51. data/examples/dhanhq/analysis/pattern_recognizer.rb +0 -192
  52. data/examples/dhanhq/analysis/trend_analyzer.rb +0 -88
  53. data/examples/dhanhq/builders/market_context_builder.rb +0 -67
  54. data/examples/dhanhq/dhanhq_agent.rb +0 -829
  55. data/examples/dhanhq/indicators/technical_indicators.rb +0 -158
  56. data/examples/dhanhq/scanners/intraday_options_scanner.rb +0 -492
  57. data/examples/dhanhq/scanners/swing_scanner.rb +0 -247
  58. data/examples/dhanhq/schemas/agent_schemas.rb +0 -61
  59. data/examples/dhanhq/services/base_service.rb +0 -46
  60. data/examples/dhanhq/services/data_service.rb +0 -118
  61. data/examples/dhanhq/services/trading_service.rb +0 -59
  62. data/examples/dhanhq/technical_analysis_agentic_runner.rb +0 -411
  63. data/examples/dhanhq/technical_analysis_runner.rb +0 -420
  64. data/examples/dhanhq/test_tool_calling.rb +0 -538
  65. data/examples/dhanhq/test_tool_calling_verbose.rb +0 -251
  66. data/examples/dhanhq/utils/instrument_helper.rb +0 -32
  67. data/examples/dhanhq/utils/parameter_cleaner.rb +0 -28
  68. data/examples/dhanhq/utils/parameter_normalizer.rb +0 -45
  69. data/examples/dhanhq/utils/rate_limiter.rb +0 -23
  70. data/examples/dhanhq/utils/trading_parameter_normalizer.rb +0 -72
  71. data/examples/dhanhq_agent.rb +0 -964
  72. data/examples/dhanhq_tools.rb +0 -1663
  73. data/examples/multi_step_agent_with_external_data.rb +0 -368
  74. data/examples/structured_outputs_chat.rb +0 -72
  75. data/examples/structured_tools.rb +0 -89
  76. data/examples/test_dhanhq_tool_calling.rb +0 -375
  77. data/examples/test_tool_calling.rb +0 -160
  78. data/examples/tool_calling_direct.rb +0 -124
  79. data/examples/tool_calling_pattern.rb +0 -269
  80. data/exe/dhan_console +0 -4
data/examples/README.md CHANGED
@@ -1,92 +1,77 @@
1
1
  # Examples
2
2
 
3
- This directory contains working examples demonstrating various features of the ollama-client gem.
3
+ This directory contains **minimal examples** demonstrating `ollama-client` usage. These examples focus on **transport and protocol correctness**, not agent behavior.
4
4
 
5
- ## Quick Start Examples
5
+ ## Minimal Examples
6
6
 
7
- ### Basic Tool Calling
8
- - **[test_tool_calling.rb](test_tool_calling.rb)** - Simple tool calling demo with weather tool
9
- - **[tool_calling_pattern.rb](tool_calling_pattern.rb)** - Recommended patterns for tool calling
10
- - **[tool_calling_direct.rb](tool_calling_direct.rb)** - Direct tool calling without Executor
7
+ ### Basic Client Usage
11
8
 
12
- ### DhanHQ Market Data
13
- - **[test_dhanhq_tool_calling.rb](test_dhanhq_tool_calling.rb)** - DhanHQ tools with updated intraday & indicators
14
- - **[dhanhq_tools.rb](dhanhq_tools.rb)** - DhanHQ API wrapper tools
15
- - **[dhan_console.rb](dhan_console.rb)** - Interactive DhanHQ console with planning
9
+ - **[basic_generate.rb](basic_generate.rb)** - Basic `/generate` usage with schema validation
10
+ - Demonstrates stateless, deterministic JSON output
11
+ - Shows schema enforcement
12
+ - No agent logic
16
13
 
17
- ### Multi-Step Agents
18
- - **[multi_step_agent_e2e.rb](multi_step_agent_e2e.rb)** - End-to-end multi-step agent example
19
- - **[multi_step_agent_with_external_data.rb](multi_step_agent_with_external_data.rb)** - Agent with external data integration
14
+ - **[basic_chat.rb](basic_chat.rb)** - Basic `/chat` usage
15
+ - Demonstrates stateful message handling
16
+ - Shows multi-turn conversation structure
17
+ - No agent loops
20
18
 
21
- ### Structured Data
22
- - **[structured_outputs_chat.rb](structured_outputs_chat.rb)** - Structured outputs with schemas
23
- - **[structured_tools.rb](structured_tools.rb)** - Structured tool definitions
24
- - **[tool_dto_example.rb](tool_dto_example.rb)** - Using DTOs for tool definitions
19
+ - **[tool_calling_parsing.rb](tool_calling_parsing.rb)** - Tool-call parsing (no execution)
20
+ - Demonstrates tool-call detection and extraction
21
+ - Shows how to parse tool calls from LLM response
22
+ - **Does NOT execute tools** - that's agent responsibility
25
23
 
26
- ### Advanced Features
27
- - **[advanced_multi_step_agent.rb](advanced_multi_step_agent.rb)** - Complex multi-step workflows
28
- - **[advanced_error_handling.rb](advanced_error_handling.rb)** - Error handling patterns
29
- - **[advanced_edge_cases.rb](advanced_edge_cases.rb)** - Edge case handling
30
- - **[advanced_complex_schemas.rb](advanced_complex_schemas.rb)** - Complex schema definitions
31
- - **[advanced_performance_testing.rb](advanced_performance_testing.rb)** - Performance testing
32
-
33
- ### Interactive Consoles
34
- - **[chat_console.rb](chat_console.rb)** - Simple chat console with streaming
35
- - **[dhan_console.rb](dhan_console.rb)** - DhanHQ market data console with formatted tool results
36
-
37
- ### Complete Workflows
38
- - **[complete_workflow.rb](complete_workflow.rb)** - Complete agent workflow example
39
-
40
- ## DhanHQ Examples
41
-
42
- The `dhanhq/` subdirectory contains more specialized DhanHQ examples:
43
- - Technical analysis agents
44
- - Market scanners (intraday options, swing trading)
45
- - Pattern recognition and trend analysis
46
- - Multi-agent orchestration
47
-
48
- See [dhanhq/README.md](dhanhq/README.md) for details.
24
+ - **[tool_dto_example.rb](tool_dto_example.rb)** - Tool DTO serialization
25
+ - Demonstrates Tool class serialization/deserialization
26
+ - Shows DTO functionality
49
27
 
50
28
  ## Running Examples
51
29
 
52
- Most examples are standalone and can be run directly:
30
+ All examples are standalone and can be run directly:
53
31
 
54
32
  ```bash
55
- # Basic tool calling
56
- ruby examples/test_tool_calling.rb
33
+ # Basic generate
34
+ ruby examples/basic_generate.rb
35
+
36
+ # Basic chat
37
+ ruby examples/basic_chat.rb
57
38
 
58
- # DhanHQ with intraday data
59
- ruby examples/test_dhanhq_tool_calling.rb
39
+ # Tool calling parsing
40
+ ruby examples/tool_calling_parsing.rb
60
41
 
61
- # Interactive console
62
- ruby examples/chat_console.rb
42
+ # Tool DTO
43
+ ruby examples/tool_dto_example.rb
63
44
  ```
64
45
 
65
46
  ### Requirements
66
47
 
67
- Some examples require additional setup:
68
-
69
- **DhanHQ Examples:**
70
- - Set `DHANHQ_CLIENT_ID` and `DHANHQ_ACCESS_TOKEN` environment variables
71
- - Or create `.env` file with credentials
72
-
73
- **Ollama:**
74
48
  - Ollama server running (default: `http://localhost:11434`)
75
49
  - Set `OLLAMA_BASE_URL` if using a different URL
76
50
  - Set `OLLAMA_MODEL` if not using default model
77
51
 
78
- ## Learning Path
52
+ ## Full Agent Examples
53
+
54
+ For complete agent examples (trading agents, coding agents, RAG agents, multi-step workflows, tool execution patterns, etc.), see:
55
+
56
+ **[ollama-agent-examples](https://github.com/shubhamtaywade82/ollama-agent-examples)**
57
+
58
+ This separation keeps `ollama-client` focused on the transport layer while providing comprehensive examples for agent developers.
59
+
60
+ ## What These Examples Demonstrate
61
+
62
+ These minimal examples prove:
63
+
64
+ ✅ **Transport layer** - HTTP requests/responses
65
+ ✅ **Protocol correctness** - Request shaping, response parsing
66
+ ✅ **Schema enforcement** - JSON validation
67
+ ✅ **Tool-call parsing** - Detecting and extracting tool calls
79
68
 
80
- 1. **Start here:** `test_tool_calling.rb` - Learn basic tool calling
81
- 2. **Structured data:** `structured_outputs_chat.rb` - Schema-based outputs
82
- 3. **Multi-step:** `multi_step_agent_e2e.rb` - Complex agent workflows
83
- 4. **Market data:** `test_dhanhq_tool_calling.rb` - Real-world API integration
84
- 5. **Interactive:** `dhan_console.rb` - Full-featured console with planning
69
+ These examples do **NOT** demonstrate:
85
70
 
86
- ## Contributing
71
+ Agent loops
72
+ ❌ Tool execution
73
+ ❌ Convergence logic
74
+ ❌ Policy decisions
75
+ ❌ Domain-specific logic
87
76
 
88
- When adding new examples:
89
- - Include clear comments explaining what the example demonstrates
90
- - Add `#!/usr/bin/env ruby` shebang at the top
91
- - Use `frozen_string_literal: true`
92
- - Update this README with a description
77
+ **Those belong in the separate agent examples repository.**
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example: Basic /chat usage
5
+ # Demonstrates client transport layer - stateful message handling
6
+
7
+ require_relative "../lib/ollama_client"
8
+
9
+ client = Ollama::Client.new
10
+
11
+ # Simple chat message
12
+ response = client.chat_raw(
13
+ messages: [{ role: "user", content: "Say hello." }],
14
+ allow_chat: true
15
+ )
16
+
17
+ puts "Response: #{response.message.content}"
18
+ puts "Role: #{response.message.role}"
19
+
20
+ # Multi-turn conversation
21
+ messages = [
22
+ { role: "user", content: "What is Ruby?" }
23
+ ]
24
+
25
+ response1 = client.chat_raw(messages: messages, allow_chat: true)
26
+ puts "\nFirst response: #{response1.message.content}"
27
+
28
+ # Continue conversation
29
+ messages << { role: "assistant", content: response1.message.content }
30
+ messages << { role: "user", content: "Tell me more about its use cases" }
31
+
32
+ response2 = client.chat_raw(messages: messages, allow_chat: true)
33
+ puts "\nSecond response: #{response2.message.content}"
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example: Basic /generate usage with schema validation
5
+ # Demonstrates client transport layer - stateless, deterministic JSON output
6
+
7
+ require_relative "../lib/ollama_client"
8
+
9
+ client = Ollama::Client.new
10
+
11
+ # Define schema for structured output
12
+ schema = {
13
+ "type" => "object",
14
+ "required" => ["status"],
15
+ "properties" => {
16
+ "status" => { "type" => "string" }
17
+ }
18
+ }
19
+
20
+ # Generate structured JSON response
21
+ result = client.generate(
22
+ prompt: "Output a JSON object with a single key 'status' and value 'ok'.",
23
+ schema: schema
24
+ )
25
+
26
+ puts "Result: #{result.inspect}"
27
+ puts "Status: #{result['status']}" # => "ok"
28
+
29
+ # The result is guaranteed to match your schema!
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example: Tool-call parsing (no execution)
5
+ # Demonstrates client transport layer - tool-call detection and extraction
6
+ # NOTE: This example does NOT execute tools. It only parses tool calls from the LLM response.
7
+
8
+ require "json"
9
+ require_relative "../lib/ollama_client"
10
+
11
+ client = Ollama::Client.new
12
+
13
+ # Define tool using Tool classes
14
+ tool = Ollama::Tool.new(
15
+ type: "function",
16
+ function: Ollama::Tool::Function.new(
17
+ name: "get_weather",
18
+ description: "Get weather for a location",
19
+ parameters: Ollama::Tool::Function::Parameters.new(
20
+ type: "object",
21
+ properties: {
22
+ location: Ollama::Tool::Function::Parameters::Property.new(
23
+ type: "string",
24
+ description: "The city name"
25
+ )
26
+ },
27
+ required: %w[location]
28
+ )
29
+ )
30
+ )
31
+
32
+ # Request tool call from LLM
33
+ response = client.chat_raw(
34
+ messages: [{ role: "user", content: "What's the weather in Paris?" }],
35
+ tools: tool,
36
+ allow_chat: true
37
+ )
38
+
39
+ # Parse tool calls (but do NOT execute)
40
+ tool_calls = response.message&.tool_calls
41
+
42
+ if tool_calls && !tool_calls.empty?
43
+ puts "Tool calls detected:"
44
+ tool_calls.each do |call|
45
+ # Access via method (if available)
46
+ name = call.respond_to?(:name) ? call.name : call["function"]["name"]
47
+ args = call.respond_to?(:arguments) ? call.arguments : JSON.parse(call["function"]["arguments"])
48
+
49
+ puts " Tool: #{name}"
50
+ puts " Arguments: #{args.inspect}"
51
+ puts " (Tool execution would happen here in your agent code)"
52
+ end
53
+ else
54
+ puts "No tool calls in response"
55
+ puts "Response: #{response.message&.content}"
56
+ end
57
+
58
+ # Alternative: Access via hash
59
+ # tool_calls = response.to_h.dig("message", "tool_calls")
data/exe/ollama-client CHANGED
@@ -1,4 +1,131 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative "../examples/chat_console"
4
+ # Chat console using ChatSession with TTY reader for input only
5
+ # Note: dotenv is automatically loaded by lib/ollama_client.rb
6
+ require_relative "../lib/ollama_client"
7
+ require "tty-reader"
8
+
9
+ # Build config from environment or defaults
10
+ config = Ollama::Config.new
11
+ config.base_url = ENV["OLLAMA_BASE_URL"] if ENV["OLLAMA_BASE_URL"]
12
+ config.model = ENV["OLLAMA_MODEL"] || config.model
13
+ config.temperature = ENV["OLLAMA_TEMPERATURE"].to_f if ENV["OLLAMA_TEMPERATURE"]
14
+
15
+ # Enable chat + streaming (required for ChatSession)
16
+ config.allow_chat = true
17
+ config.streaming_enabled = true
18
+
19
+ client = Ollama::Client.new(config: config)
20
+
21
+ # Create streaming observer for real-time token display
22
+ observer = Ollama::StreamingObserver.new do |event|
23
+ case event.type
24
+ when :token
25
+ print event.text
26
+ $stdout.flush
27
+ when :tool_call_detected
28
+ puts "\n[Tool call: #{event.name}]"
29
+ when :final
30
+ puts "\n"
31
+ end
32
+ end
33
+
34
+ # Create chat session with optional system message from env
35
+ system_prompt = ENV.fetch("OLLAMA_SYSTEM", "You are a helpful assistant.")
36
+ chat = Ollama::ChatSession.new(client, system: system_prompt, stream: observer)
37
+
38
+ # Setup TTY reader for input with history
39
+ def build_reader
40
+ TTY::Reader.new
41
+ end
42
+
43
+ def read_input(reader)
44
+ # Pass prompt directly to read_line - this is how TTY::Reader is designed to work
45
+ reader.read_line("You: ")
46
+ end
47
+
48
+ HISTORY_PATH = File.expand_path("~/.ollama_chat_history")
49
+
50
+ def load_history(reader, path)
51
+ return unless File.exist?(path)
52
+
53
+ File.readlines(path, chomp: true).reverse_each do |line|
54
+ reader.add_to_history(line) unless line.strip.empty?
55
+ end
56
+ rescue StandardError
57
+ # Ignore history loading errors
58
+ end
59
+
60
+ def save_history(path, text)
61
+ return if text.strip.empty?
62
+
63
+ history = []
64
+ history = File.readlines(path, chomp: true) if File.exist?(path)
65
+ history.delete(text)
66
+ history.unshift(text)
67
+ history = history.first(200) # Limit history size
68
+
69
+ File.write(path, "#{history.join("\n")}\n")
70
+ rescue StandardError
71
+ # Ignore history saving errors
72
+ end
73
+
74
+ # Print simple banner
75
+ puts "Ollama Chat Console"
76
+ puts "Model: #{config.model}"
77
+ puts "Base URL: #{config.base_url}"
78
+ puts "Type 'quit' or 'exit' to exit, 'clear' to reset history."
79
+ puts
80
+
81
+ # Setup reader and load history
82
+ reader = build_reader
83
+ load_history(reader, HISTORY_PATH)
84
+
85
+ # Main loop
86
+ begin
87
+ loop do
88
+ # Use TTY reader with prompt (supports history, arrow keys, editing)
89
+ input = read_input(reader)
90
+
91
+ break if input.nil?
92
+
93
+ text = input.strip
94
+ next if text.empty?
95
+
96
+ # Handle commands
97
+ case text.downcase
98
+ when "quit", "exit", "/quit", "/exit"
99
+ puts "\nGoodbye!\n"
100
+ break
101
+ when "clear", "/clear"
102
+ chat.clear
103
+ puts "Conversation history cleared.\n\n"
104
+ next
105
+ end
106
+
107
+ # Save to history
108
+ save_history(HISTORY_PATH, text)
109
+
110
+ # Assistant response
111
+ print "Assistant: "
112
+
113
+ begin
114
+ response = chat.say(text)
115
+ # Ensure newline after streaming
116
+ puts "" if response.empty?
117
+ rescue Ollama::ChatNotAllowedError => e
118
+ puts "\n❌ Error: #{e.message}"
119
+ puts "Make sure config.allow_chat = true"
120
+ rescue Ollama::Error => e
121
+ puts "\n❌ Error: #{e.message}"
122
+ end
123
+
124
+ puts "" # Blank line between exchanges
125
+ end
126
+ rescue Interrupt
127
+ puts "\n\nInterrupted. Goodbye!\n"
128
+ rescue StandardError => e
129
+ puts "\nUnexpected error: #{e.message}\n"
130
+ puts "#{e.backtrace.first}\n" if ENV["DEBUG"]
131
+ end
@@ -21,17 +21,22 @@ module Ollama
21
21
  ]
22
22
  }.freeze
23
23
 
24
- def initialize(client)
24
+ def initialize(client, system_prompt: nil)
25
25
  @client = client
26
+ @system_prompt = system_prompt
26
27
  end
27
28
 
28
29
  # @param prompt [String]
29
30
  # @param context [Hash, nil]
30
31
  # @param schema [Hash, nil]
32
+ # @param system_prompt [String, nil] Optional system prompt override for this call
31
33
  # @return [Object] Parsed JSON (Hash/Array/String/Number/Boolean/Nil)
32
- def run(prompt:, context: nil, schema: nil)
34
+ def run(prompt:, context: nil, schema: nil, system_prompt: nil)
35
+ effective_system = system_prompt || @system_prompt
33
36
  full_prompt = prompt.to_s
34
37
 
38
+ full_prompt = "#{effective_system}\n\n#{full_prompt}" if effective_system && !effective_system.empty?
39
+
35
40
  if context && !context.empty?
36
41
  full_prompt = "#{full_prompt}\n\nContext (JSON):\n#{JSON.pretty_generate(context)}"
37
42
  end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "streaming_observer"
4
+ require_relative "agent/messages"
5
+
6
+ module Ollama
7
+ # Stateful chat session for human-facing interactions.
8
+ #
9
+ # Chat sessions maintain conversation history and support streaming
10
+ # for presentation purposes. They are isolated from agent internals
11
+ # to preserve deterministic behavior in schema-first workflows.
12
+ #
13
+ # Example:
14
+ # client = Ollama::Client.new(config: Ollama::Config.new.tap { |c| c.allow_chat = true })
15
+ # observer = Ollama::StreamingObserver.new do |event|
16
+ # print event.text if event.type == :token
17
+ # end
18
+ # chat = Ollama::ChatSession.new(client, system: "You are helpful", stream: observer)
19
+ # chat.say("Hello")
20
+ # chat.say("Explain Ruby blocks")
21
+ class ChatSession
22
+ attr_reader :messages
23
+
24
+ def initialize(client, system: nil, stream: nil)
25
+ @client = client
26
+ @messages = []
27
+ @stream = stream
28
+ @messages << Agent::Messages.system(system) if system
29
+ end
30
+
31
+ # Send a user message and get assistant response.
32
+ #
33
+ # @param text [String] User message text
34
+ # @param model [String, nil] Model override (uses client config if nil)
35
+ # @param format [Hash, nil] Optional JSON schema for formatting (best-effort, not guaranteed)
36
+ # @param tools [Tool, Array<Tool>, Array<Hash>, nil] Optional tool definitions
37
+ # @param options [Hash] Additional options (temperature, top_p, etc.)
38
+ # @return [String] Assistant response content
39
+ def say(text, model: nil, format: nil, tools: nil, options: {})
40
+ @messages << Agent::Messages.user(text)
41
+
42
+ response = if @stream
43
+ stream_response(model: model, format: format, tools: tools, options: options)
44
+ else
45
+ non_stream_response(model: model, format: format, tools: tools, options: options)
46
+ end
47
+
48
+ content = response["message"]&.dig("content") || ""
49
+ tool_calls = response["message"]&.dig("tool_calls")
50
+
51
+ @messages << Agent::Messages.assistant(content, tool_calls: tool_calls) if content || tool_calls
52
+
53
+ content
54
+ end
55
+
56
+ # Clear conversation history (keeps system message if present).
57
+ def clear
58
+ system_msg = @messages.find { |m| m["role"] == "system" }
59
+ @messages = system_msg ? [system_msg] : []
60
+ end
61
+
62
+ private
63
+
64
+ def stream_response(model:, format:, tools:, options:)
65
+ @client.chat_raw(
66
+ messages: @messages,
67
+ model: model,
68
+ format: format,
69
+ tools: tools,
70
+ options: options,
71
+ allow_chat: true,
72
+ stream: true
73
+ ) do |chunk|
74
+ delta = chunk.dig("message", "content")
75
+ @stream.emit(:token, text: delta.to_s) if delta && !delta.to_s.empty?
76
+
77
+ calls = chunk.dig("message", "tool_calls")
78
+ if calls.is_a?(Array)
79
+ calls.each do |call|
80
+ name = call.dig("function", "name") || call["name"]
81
+ @stream.emit(:tool_call_detected, name: name, data: call) if name
82
+ end
83
+ end
84
+
85
+ # Emit final event when stream completes
86
+ @stream.emit(:final) if chunk["done"] == true
87
+ end
88
+ end
89
+
90
+ def non_stream_response(model:, format:, tools:, options:)
91
+ @client.chat_raw(
92
+ messages: @messages,
93
+ model: model,
94
+ format: format,
95
+ tools: tools,
96
+ options: options,
97
+ allow_chat: true
98
+ )
99
+ end
100
+ end
101
+ end
data/lib/ollama/client.rb CHANGED
@@ -206,7 +206,9 @@ module Ollama
206
206
  end
207
207
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/ParameterLists
208
208
 
209
- def generate(prompt:, schema:, model: nil, strict: false, return_meta: false)
209
+ def generate(prompt:, schema: nil, model: nil, strict: false, return_meta: false)
210
+ validate_generate_params!(prompt, schema)
211
+
210
212
  attempts = 0
211
213
  @current_schema = schema # Store for prompt enhancement
212
214
  started_at = monotonic_time
@@ -227,23 +229,12 @@ module Ollama
227
229
  }
228
230
  )
229
231
 
230
- parsed = parse_json_response(raw)
231
-
232
- # CRITICAL: If schema is provided, free-text output is forbidden
233
- raise SchemaViolationError, "Empty or nil response when schema is required" if parsed.nil? || parsed.empty?
234
-
235
- SchemaValidator.validate!(parsed, schema)
236
- return parsed unless return_meta
237
-
238
- {
239
- "data" => parsed,
240
- "meta" => {
241
- "endpoint" => "/api/generate",
242
- "model" => model || @config.model,
243
- "attempts" => attempts,
244
- "latency_ms" => elapsed_ms(started_at)
245
- }
232
+ meta = {
233
+ model: model || @config.model,
234
+ attempts: attempts,
235
+ started_at: started_at
246
236
  }
237
+ process_generate_response(raw: raw, schema: schema, meta: meta, return_meta: return_meta)
247
238
  rescue NotFoundError => e
248
239
  # 404 errors are never retried, but we can suggest models
249
240
  enhanced_error = enhance_not_found_error(e)
@@ -267,7 +258,7 @@ module Ollama
267
258
  end
268
259
  # rubocop:enable Metrics/MethodLength
269
260
 
270
- def generate_strict!(prompt:, schema:, model: nil, return_meta: false)
261
+ def generate_strict!(prompt:, schema: nil, model: nil, return_meta: false)
271
262
  generate(prompt: prompt, schema: schema, model: model, strict: true, return_meta: return_meta)
272
263
  end
273
264
 
@@ -348,12 +339,43 @@ module Ollama
348
339
 
349
340
  private
350
341
 
342
+ def validate_generate_params!(prompt, _schema)
343
+ raise ArgumentError, "prompt is required" if prompt.nil?
344
+ # schema is optional - nil means plain text/markdown response
345
+ end
346
+
347
+ def process_generate_response(raw:, schema:, meta:, return_meta:)
348
+ response_data = schema ? parse_and_validate_schema_response(raw, schema) : raw
349
+ return response_data unless return_meta
350
+
351
+ {
352
+ "data" => response_data,
353
+ "meta" => {
354
+ "endpoint" => "/api/generate",
355
+ "model" => meta[:model],
356
+ "attempts" => meta[:attempts],
357
+ "latency_ms" => elapsed_ms(meta[:started_at])
358
+ }
359
+ }
360
+ end
361
+
362
+ def parse_and_validate_schema_response(raw, schema)
363
+ parsed = parse_json_response(raw)
364
+ raise SchemaViolationError, "Empty or nil response when schema is required" if parsed.nil? || parsed.empty?
365
+
366
+ SchemaValidator.validate!(parsed, schema)
367
+ parsed
368
+ end
369
+
351
370
  def ensure_chat_allowed!(allow_chat:, strict:, method_name:)
352
- return if allow_chat || strict
371
+ return if allow_chat || strict || @config.allow_chat
353
372
 
354
- raise Error,
373
+ raise ChatNotAllowedError,
355
374
  "#{method_name}() is intentionally gated because it is easy to misuse inside agents. " \
356
- "Prefer generate(). If you really want #{method_name}(), pass allow_chat: true (or strict: true)."
375
+ "Prefer generate() for deterministic, schema-first workflows. " \
376
+ "To use #{method_name}(), either: " \
377
+ "1) Pass allow_chat: true as a parameter, or " \
378
+ "2) Enable chat in config: config.allow_chat = true"
357
379
  end
358
380
 
359
381
  # Normalize tools to array of hashes for API
data/lib/ollama/config.rb CHANGED
@@ -15,7 +15,8 @@ module Ollama
15
15
  # client = Ollama::Client.new(config: config)
16
16
  #
17
17
  class Config
18
- attr_accessor :base_url, :model, :timeout, :retries, :temperature, :top_p, :num_ctx, :on_response
18
+ attr_accessor :base_url, :model, :timeout, :retries, :temperature, :top_p, :num_ctx, :on_response, :allow_chat,
19
+ :streaming_enabled
19
20
 
20
21
  def initialize
21
22
  @base_url = "http://localhost:11434"
@@ -26,6 +27,8 @@ module Ollama
26
27
  @top_p = 0.9
27
28
  @num_ctx = 8192
28
29
  @on_response = nil
30
+ @allow_chat = false
31
+ @streaming_enabled = false
29
32
  end
30
33
 
31
34
  # Load configuration from JSON file (useful for production deployments)