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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -1
- data/README.md +560 -106
- data/docs/EXAMPLE_REORGANIZATION.md +412 -0
- data/docs/GETTING_STARTED.md +361 -0
- data/docs/INTEGRATION_TESTING.md +170 -0
- data/docs/NEXT_STEPS_SUMMARY.md +114 -0
- data/docs/PERSONAS.md +383 -0
- data/docs/QUICK_START.md +195 -0
- data/docs/README.md +2 -3
- data/docs/RELEASE_GUIDE.md +376 -0
- data/docs/TESTING.md +392 -170
- data/docs/TEST_CHECKLIST.md +450 -0
- data/docs/ruby_guide.md +6232 -0
- data/examples/README.md +51 -66
- data/examples/basic_chat.rb +33 -0
- data/examples/basic_generate.rb +29 -0
- data/examples/tool_calling_parsing.rb +59 -0
- data/exe/ollama-client +128 -1
- data/lib/ollama/agent/planner.rb +7 -2
- data/lib/ollama/chat_session.rb +101 -0
- data/lib/ollama/client.rb +43 -21
- data/lib/ollama/config.rb +4 -1
- data/lib/ollama/document_loader.rb +163 -0
- data/lib/ollama/embeddings.rb +42 -13
- data/lib/ollama/errors.rb +1 -0
- data/lib/ollama/personas.rb +287 -0
- data/lib/ollama/version.rb +1 -1
- data/lib/ollama_client.rb +8 -0
- metadata +31 -53
- data/docs/GEM_RELEASE_GUIDE.md +0 -794
- data/docs/GET_RUBYGEMS_SECRET.md +0 -151
- data/docs/QUICK_OTP_SETUP.md +0 -80
- data/docs/QUICK_RELEASE.md +0 -106
- data/docs/RUBYGEMS_OTP_SETUP.md +0 -199
- data/examples/advanced_complex_schemas.rb +0 -366
- data/examples/advanced_edge_cases.rb +0 -241
- data/examples/advanced_error_handling.rb +0 -200
- data/examples/advanced_multi_step_agent.rb +0 -341
- data/examples/advanced_performance_testing.rb +0 -186
- data/examples/chat_console.rb +0 -143
- data/examples/complete_workflow.rb +0 -245
- data/examples/dhan_console.rb +0 -843
- data/examples/dhanhq/README.md +0 -236
- data/examples/dhanhq/agents/base_agent.rb +0 -74
- data/examples/dhanhq/agents/data_agent.rb +0 -66
- data/examples/dhanhq/agents/orchestrator_agent.rb +0 -120
- data/examples/dhanhq/agents/technical_analysis_agent.rb +0 -252
- data/examples/dhanhq/agents/trading_agent.rb +0 -81
- data/examples/dhanhq/analysis/market_structure.rb +0 -138
- data/examples/dhanhq/analysis/pattern_recognizer.rb +0 -192
- data/examples/dhanhq/analysis/trend_analyzer.rb +0 -88
- data/examples/dhanhq/builders/market_context_builder.rb +0 -67
- data/examples/dhanhq/dhanhq_agent.rb +0 -829
- data/examples/dhanhq/indicators/technical_indicators.rb +0 -158
- data/examples/dhanhq/scanners/intraday_options_scanner.rb +0 -492
- data/examples/dhanhq/scanners/swing_scanner.rb +0 -247
- data/examples/dhanhq/schemas/agent_schemas.rb +0 -61
- data/examples/dhanhq/services/base_service.rb +0 -46
- data/examples/dhanhq/services/data_service.rb +0 -118
- data/examples/dhanhq/services/trading_service.rb +0 -59
- data/examples/dhanhq/technical_analysis_agentic_runner.rb +0 -411
- data/examples/dhanhq/technical_analysis_runner.rb +0 -420
- data/examples/dhanhq/test_tool_calling.rb +0 -538
- data/examples/dhanhq/test_tool_calling_verbose.rb +0 -251
- data/examples/dhanhq/utils/instrument_helper.rb +0 -32
- data/examples/dhanhq/utils/parameter_cleaner.rb +0 -28
- data/examples/dhanhq/utils/parameter_normalizer.rb +0 -45
- data/examples/dhanhq/utils/rate_limiter.rb +0 -23
- data/examples/dhanhq/utils/trading_parameter_normalizer.rb +0 -72
- data/examples/dhanhq_agent.rb +0 -964
- data/examples/dhanhq_tools.rb +0 -1663
- data/examples/multi_step_agent_with_external_data.rb +0 -368
- data/examples/structured_outputs_chat.rb +0 -72
- data/examples/structured_tools.rb +0 -89
- data/examples/test_dhanhq_tool_calling.rb +0 -375
- data/examples/test_tool_calling.rb +0 -160
- data/examples/tool_calling_direct.rb +0 -124
- data/examples/tool_calling_pattern.rb +0 -269
- data/exe/dhan_console +0 -4
data/examples/README.md
CHANGED
|
@@ -1,92 +1,77 @@
|
|
|
1
1
|
# Examples
|
|
2
2
|
|
|
3
|
-
This directory contains
|
|
3
|
+
This directory contains **minimal examples** demonstrating `ollama-client` usage. These examples focus on **transport and protocol correctness**, not agent behavior.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Minimal Examples
|
|
6
6
|
|
|
7
|
-
### Basic
|
|
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
|
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
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
|
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
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
|
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
- **
|
|
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
|
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
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
|
-
|
|
30
|
+
All examples are standalone and can be run directly:
|
|
53
31
|
|
|
54
32
|
```bash
|
|
55
|
-
# Basic
|
|
56
|
-
ruby examples/
|
|
33
|
+
# Basic generate
|
|
34
|
+
ruby examples/basic_generate.rb
|
|
35
|
+
|
|
36
|
+
# Basic chat
|
|
37
|
+
ruby examples/basic_chat.rb
|
|
57
38
|
|
|
58
|
-
#
|
|
59
|
-
ruby examples/
|
|
39
|
+
# Tool calling parsing
|
|
40
|
+
ruby examples/tool_calling_parsing.rb
|
|
60
41
|
|
|
61
|
-
#
|
|
62
|
-
ruby examples/
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
71
|
+
❌ Agent loops
|
|
72
|
+
❌ Tool execution
|
|
73
|
+
❌ Convergence logic
|
|
74
|
+
❌ Policy decisions
|
|
75
|
+
❌ Domain-specific logic
|
|
87
76
|
|
|
88
|
-
|
|
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
|
-
|
|
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
|
data/lib/ollama/agent/planner.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
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
|
|
373
|
+
raise ChatNotAllowedError,
|
|
355
374
|
"#{method_name}() is intentionally gated because it is easy to misuse inside agents. " \
|
|
356
|
-
"Prefer generate()
|
|
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)
|