ollama-client 0.2.5 → 0.2.7
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 +22 -0
- data/README.md +336 -91
- data/RELEASE_NOTES_v0.2.6.md +41 -0
- data/docs/AREAS_FOR_CONSIDERATION.md +325 -0
- data/docs/EXAMPLE_REORGANIZATION.md +412 -0
- data/docs/FEATURES_ADDED.md +12 -1
- 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/TESTING.md +392 -170
- data/docs/TEST_CHECKLIST.md +450 -0
- data/examples/README.md +62 -63
- data/examples/basic_chat.rb +33 -0
- data/examples/basic_generate.rb +29 -0
- data/examples/mcp_executor.rb +39 -0
- data/examples/mcp_http_executor.rb +45 -0
- data/examples/tool_calling_parsing.rb +59 -0
- data/examples/tool_dto_example.rb +0 -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 +41 -35
- data/lib/ollama/config.rb +9 -4
- data/lib/ollama/document_loader.rb +1 -1
- data/lib/ollama/embeddings.rb +61 -28
- data/lib/ollama/errors.rb +1 -0
- data/lib/ollama/mcp/http_client.rb +149 -0
- data/lib/ollama/mcp/stdio_client.rb +146 -0
- data/lib/ollama/mcp/tools_bridge.rb +72 -0
- data/lib/ollama/mcp.rb +31 -0
- data/lib/ollama/options.rb +3 -1
- data/lib/ollama/personas.rb +287 -0
- data/lib/ollama/version.rb +1 -1
- data/lib/ollama_client.rb +17 -5
- metadata +22 -48
- 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
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Ollama
|
|
8
|
+
module MCP
|
|
9
|
+
# Connects to a remote MCP server via HTTP(S).
|
|
10
|
+
# Sends JSON-RPC via POST; supports session ID from initialize response.
|
|
11
|
+
# Use for URLs like https://gitmcp.io/owner/repo.
|
|
12
|
+
class HttpClient
|
|
13
|
+
PROTOCOL_VERSION = "2025-11-25"
|
|
14
|
+
|
|
15
|
+
def initialize(url:, timeout_seconds: 30, headers: {})
|
|
16
|
+
@uri = URI(url)
|
|
17
|
+
@uri.path = "/" if @uri.path.nil? || @uri.path.empty?
|
|
18
|
+
@timeout = timeout_seconds
|
|
19
|
+
@extra_headers = headers.transform_keys(&:to_s)
|
|
20
|
+
@request_id = 0
|
|
21
|
+
@session_id = nil
|
|
22
|
+
@initialized = false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def start
|
|
26
|
+
return if @initialized
|
|
27
|
+
|
|
28
|
+
run_initialize
|
|
29
|
+
@initialized = true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def tools
|
|
33
|
+
start
|
|
34
|
+
response = request("tools/list", {})
|
|
35
|
+
list = response.dig("result", "tools")
|
|
36
|
+
return [] unless list.is_a?(Array)
|
|
37
|
+
|
|
38
|
+
list.map do |t|
|
|
39
|
+
{
|
|
40
|
+
name: (t["name"] || t[:name]).to_s,
|
|
41
|
+
description: (t["description"] || t[:description]).to_s,
|
|
42
|
+
input_schema: t["inputSchema"] || t[:input_schema] || { "type" => "object" }
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def call_tool(name:, arguments: {})
|
|
48
|
+
start
|
|
49
|
+
response = request("tools/call", "name" => name.to_s, "arguments" => stringify_keys(arguments))
|
|
50
|
+
result = response["result"]
|
|
51
|
+
raise Ollama::Error, "tools/call failed: #{response["error"]}" if response["error"]
|
|
52
|
+
raise Ollama::Error, "tools/call returned no result" unless result
|
|
53
|
+
|
|
54
|
+
content_to_string(result["content"])
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def close
|
|
58
|
+
@session_id = nil
|
|
59
|
+
@initialized = false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def run_initialize
|
|
65
|
+
init_params = {
|
|
66
|
+
"protocolVersion" => PROTOCOL_VERSION,
|
|
67
|
+
"capabilities" => {},
|
|
68
|
+
"clientInfo" => {
|
|
69
|
+
"name" => "ollama-client",
|
|
70
|
+
"version" => Ollama::VERSION
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
response = request("initialize", init_params)
|
|
74
|
+
raise Ollama::Error, "initialize failed: #{response["error"]}" if response["error"]
|
|
75
|
+
|
|
76
|
+
send_notification("notifications/initialized", {})
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def next_id
|
|
80
|
+
@request_id += 1
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def request(method, params)
|
|
84
|
+
id = next_id
|
|
85
|
+
msg = { "jsonrpc" => "2.0", "id" => id, "method" => method, "params" => params }
|
|
86
|
+
post_request(msg, method: method)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def send_notification(method, params)
|
|
90
|
+
body = { "jsonrpc" => "2.0", "method" => method, "params" => params }
|
|
91
|
+
post_request(body, method: method)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def post_request(body, method: nil)
|
|
95
|
+
req = Net::HTTP::Post.new(@uri)
|
|
96
|
+
req["Content-Type"] = "application/json"
|
|
97
|
+
req["Accept"] = "application/json, text/event-stream"
|
|
98
|
+
req["MCP-Protocol-Version"] = PROTOCOL_VERSION
|
|
99
|
+
req["MCP-Session-Id"] = @session_id if @session_id
|
|
100
|
+
@extra_headers.each { |k, v| req[k] = v }
|
|
101
|
+
req.body = body.is_a?(Hash) ? JSON.generate(body) : body.to_s
|
|
102
|
+
|
|
103
|
+
res = http_request(req)
|
|
104
|
+
|
|
105
|
+
if method == "initialize" && res["MCP-Session-Id"]
|
|
106
|
+
@session_id = res["MCP-Session-Id"].to_s.strip
|
|
107
|
+
@session_id = nil if @session_id.empty?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
return {} if res.code == "202"
|
|
111
|
+
|
|
112
|
+
raise Ollama::Error, "MCP HTTP error: #{res.code} #{res.message}" unless res.is_a?(Net::HTTPSuccess)
|
|
113
|
+
|
|
114
|
+
JSON.parse(res.body)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def http_request(req)
|
|
118
|
+
Net::HTTP.start(
|
|
119
|
+
@uri.hostname,
|
|
120
|
+
@uri.port,
|
|
121
|
+
use_ssl: @uri.scheme == "https",
|
|
122
|
+
read_timeout: @timeout,
|
|
123
|
+
open_timeout: @timeout
|
|
124
|
+
) { |http| http.request(req) }
|
|
125
|
+
rescue Net::ReadTimeout, Net::OpenTimeout
|
|
126
|
+
raise Ollama::TimeoutError, "MCP server did not respond within #{@timeout}s"
|
|
127
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError => e
|
|
128
|
+
raise Ollama::Error, "MCP connection failed: #{e.message}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def stringify_keys(hash)
|
|
132
|
+
return {} if hash.nil?
|
|
133
|
+
|
|
134
|
+
hash.transform_keys(&:to_s)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def content_to_string(content)
|
|
138
|
+
return "" unless content.is_a?(Array)
|
|
139
|
+
|
|
140
|
+
content.filter_map do |item|
|
|
141
|
+
next unless item.is_a?(Hash)
|
|
142
|
+
|
|
143
|
+
text = item["text"] || item[:text]
|
|
144
|
+
text&.to_s
|
|
145
|
+
end.join("\n")
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
module Ollama
|
|
7
|
+
module MCP
|
|
8
|
+
# Connects to a local MCP server via stdio (spawns subprocess).
|
|
9
|
+
# Handles JSON-RPC lifecycle: initialize, tools/list, tools/call.
|
|
10
|
+
class StdioClient
|
|
11
|
+
PROTOCOL_VERSION = "2025-11-25"
|
|
12
|
+
|
|
13
|
+
def initialize(command:, args: [], env: {}, timeout_seconds: 30)
|
|
14
|
+
@command = command
|
|
15
|
+
@args = Array(args)
|
|
16
|
+
@env = env
|
|
17
|
+
@timeout = timeout_seconds
|
|
18
|
+
@request_id = 0
|
|
19
|
+
@reader = nil
|
|
20
|
+
@writer = nil
|
|
21
|
+
@initialized = false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def start
|
|
25
|
+
return if @initialized
|
|
26
|
+
|
|
27
|
+
env_merged = ENV.to_h.merge(@env.transform_keys(&:to_s))
|
|
28
|
+
stdin, stdout = Open3.popen2(env_merged, @command, *@args)
|
|
29
|
+
@writer = stdin
|
|
30
|
+
@reader = stdout
|
|
31
|
+
run_initialize
|
|
32
|
+
@initialized = true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def tools
|
|
36
|
+
start
|
|
37
|
+
response = request("tools/list", {})
|
|
38
|
+
list = response.dig("result", "tools")
|
|
39
|
+
return [] unless list.is_a?(Array)
|
|
40
|
+
|
|
41
|
+
list.map do |t|
|
|
42
|
+
{
|
|
43
|
+
name: (t["name"] || t[:name]).to_s,
|
|
44
|
+
description: (t["description"] || t[:description]).to_s,
|
|
45
|
+
input_schema: t["inputSchema"] || t[:input_schema] || { "type" => "object" }
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def call_tool(name:, arguments: {})
|
|
51
|
+
start
|
|
52
|
+
response = request("tools/call", "name" => name.to_s, "arguments" => stringify_keys(arguments))
|
|
53
|
+
result = response["result"]
|
|
54
|
+
raise Ollama::Error, "tools/call failed: #{response["error"]}" if response["error"]
|
|
55
|
+
raise Ollama::Error, "tools/call returned no result" unless result
|
|
56
|
+
|
|
57
|
+
content_to_string(result["content"])
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def close
|
|
61
|
+
return unless @writer
|
|
62
|
+
|
|
63
|
+
@writer.close
|
|
64
|
+
@writer = nil
|
|
65
|
+
@reader = nil
|
|
66
|
+
@initialized = false
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def run_initialize
|
|
72
|
+
init_params = {
|
|
73
|
+
"protocolVersion" => PROTOCOL_VERSION,
|
|
74
|
+
"capabilities" => {},
|
|
75
|
+
"clientInfo" => {
|
|
76
|
+
"name" => "ollama-client",
|
|
77
|
+
"version" => Ollama::VERSION
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
response = request("initialize", init_params)
|
|
81
|
+
raise Ollama::Error, "initialize failed: #{response["error"]}" if response["error"]
|
|
82
|
+
|
|
83
|
+
send_notification("notifications/initialized", {})
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def next_id
|
|
87
|
+
@request_id += 1
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def request(method, params)
|
|
91
|
+
id = next_id
|
|
92
|
+
msg = { "jsonrpc" => "2.0", "id" => id, "method" => method, "params" => params }
|
|
93
|
+
send_message(msg)
|
|
94
|
+
wait_for_response(id)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def send_notification(method, params)
|
|
98
|
+
send_message("jsonrpc" => "2.0", "method" => method, "params" => params)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def send_message(msg)
|
|
102
|
+
line = "#{JSON.generate(msg)}\n"
|
|
103
|
+
@writer.write(line)
|
|
104
|
+
@writer.flush
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def wait_for_response(expected_id)
|
|
108
|
+
loop do
|
|
109
|
+
line = read_line_with_timeout
|
|
110
|
+
next if line.nil? || line.strip.empty?
|
|
111
|
+
next unless line.strip.start_with?("{")
|
|
112
|
+
|
|
113
|
+
parsed = JSON.parse(line)
|
|
114
|
+
next if parsed["method"] # notification from server
|
|
115
|
+
|
|
116
|
+
return parsed if parsed["id"] == expected_id
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def read_line_with_timeout
|
|
121
|
+
unless @reader.wait_readable(@timeout)
|
|
122
|
+
raise Ollama::TimeoutError, "MCP server did not respond within #{@timeout}s"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
@reader.gets
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def stringify_keys(hash)
|
|
129
|
+
return {} if hash.nil?
|
|
130
|
+
|
|
131
|
+
hash.transform_keys(&:to_s)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def content_to_string(content)
|
|
135
|
+
return "" unless content.is_a?(Array)
|
|
136
|
+
|
|
137
|
+
content.filter_map do |item|
|
|
138
|
+
next unless item.is_a?(Hash)
|
|
139
|
+
|
|
140
|
+
text = item["text"] || item[:text]
|
|
141
|
+
text&.to_s
|
|
142
|
+
end.join("\n")
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../tool"
|
|
4
|
+
|
|
5
|
+
module Ollama
|
|
6
|
+
module MCP
|
|
7
|
+
# Bridges an MCP server's tools to Ollama::Agent::Executor.
|
|
8
|
+
# Fetches tools via tools/list, converts them to Ollama tool format,
|
|
9
|
+
# and provides a callable per tool that invokes tools/call.
|
|
10
|
+
# Accepts either client: (StdioClient or HttpClient) or stdio_client: for backward compatibility.
|
|
11
|
+
class ToolsBridge
|
|
12
|
+
def initialize(stdio_client: nil, client: nil)
|
|
13
|
+
@client = client || stdio_client
|
|
14
|
+
raise ArgumentError, "Provide client: or stdio_client:" unless @client
|
|
15
|
+
|
|
16
|
+
@tools_cache = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Returns a hash suitable for Executor: name => { tool: Ollama::Tool, callable: proc }.
|
|
20
|
+
# Callable receives keyword args and returns a string (tool result for the LLM).
|
|
21
|
+
def tools_for_executor
|
|
22
|
+
fetch_tools unless @tools_cache
|
|
23
|
+
|
|
24
|
+
@tools_cache.transform_values do |entry|
|
|
25
|
+
{
|
|
26
|
+
tool: entry[:tool],
|
|
27
|
+
callable: build_callable(entry[:name])
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns raw MCP tool list (name, description, input_schema).
|
|
33
|
+
def list_tools
|
|
34
|
+
@client.tools
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def fetch_tools
|
|
40
|
+
list = @client.tools
|
|
41
|
+
@tools_cache = list.each_with_object({}) do |mcp_tool, hash|
|
|
42
|
+
name = mcp_tool[:name]
|
|
43
|
+
next if name.nil? || name.to_s.empty?
|
|
44
|
+
|
|
45
|
+
hash[name.to_s] = {
|
|
46
|
+
name: name.to_s,
|
|
47
|
+
tool: mcp_tool_to_ollama(mcp_tool)
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def mcp_tool_to_ollama(mcp_tool)
|
|
53
|
+
schema = mcp_tool[:input_schema] || { "type" => "object" }
|
|
54
|
+
function_hash = {
|
|
55
|
+
"name" => mcp_tool[:name].to_s,
|
|
56
|
+
"description" => (mcp_tool[:description] || "MCP tool: #{mcp_tool[:name]}").to_s,
|
|
57
|
+
"parameters" => schema
|
|
58
|
+
}
|
|
59
|
+
Ollama::Tool.from_hash("type" => "function", "function" => function_hash)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def build_callable(name)
|
|
63
|
+
client = @client
|
|
64
|
+
->(**kwargs) { client.call_tool(name: name, arguments: stringify_keys(kwargs)) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def stringify_keys(hash)
|
|
68
|
+
hash.transform_keys(&:to_s)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
data/lib/ollama/mcp.rb
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# MCP (Model Context Protocol) support for local servers.
|
|
4
|
+
#
|
|
5
|
+
# Connect to MCP servers running via stdio (e.g. npx @modelcontextprotocol/server-filesystem)
|
|
6
|
+
# and use their tools with Ollama::Agent::Executor.
|
|
7
|
+
#
|
|
8
|
+
# Example (remote URL, e.g. GitMCP):
|
|
9
|
+
# mcp_client = Ollama::MCP::HttpClient.new(url: "https://gitmcp.io/owner/repo")
|
|
10
|
+
# bridge = Ollama::MCP::ToolsBridge.new(client: mcp_client)
|
|
11
|
+
# tools = bridge.tools_for_executor
|
|
12
|
+
# executor = Ollama::Agent::Executor.new(ollama_client, tools: tools)
|
|
13
|
+
# executor.run(system: "...", user: "What does this repo do?")
|
|
14
|
+
#
|
|
15
|
+
# Example (local stdio):
|
|
16
|
+
# mcp_client = Ollama::MCP::StdioClient.new(
|
|
17
|
+
# command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
|
|
18
|
+
# )
|
|
19
|
+
# bridge = Ollama::MCP::ToolsBridge.new(stdio_client: mcp_client)
|
|
20
|
+
# tools = bridge.tools_for_executor
|
|
21
|
+
# executor.run(system: "...", user: "List files in /tmp")
|
|
22
|
+
#
|
|
23
|
+
module Ollama
|
|
24
|
+
# Model Context Protocol client and tools bridge for Executor.
|
|
25
|
+
module MCP
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
require_relative "mcp/stdio_client"
|
|
30
|
+
require_relative "mcp/http_client"
|
|
31
|
+
require_relative "mcp/tools_bridge"
|
data/lib/ollama/options.rb
CHANGED
|
@@ -8,7 +8,9 @@ module Ollama
|
|
|
8
8
|
#
|
|
9
9
|
# Example:
|
|
10
10
|
# options = Ollama::Options.new(temperature: 0.7, top_p: 0.95)
|
|
11
|
-
# client.
|
|
11
|
+
# client.chat(messages: [...], format: {...}, options: options.to_h, allow_chat: true)
|
|
12
|
+
#
|
|
13
|
+
# Note: generate() doesn't accept options parameter - set options in config instead
|
|
12
14
|
class Options
|
|
13
15
|
VALID_KEYS = %i[temperature top_p top_k num_ctx repeat_penalty seed].freeze
|
|
14
16
|
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ollama
|
|
4
|
+
# Persona system for explicit, contextual personalization.
|
|
5
|
+
#
|
|
6
|
+
# Personas are NOT baked into models or server config. They are injected
|
|
7
|
+
# explicitly at the system/prompt layer, allowing you to:
|
|
8
|
+
#
|
|
9
|
+
# - Use compressed versions for schema-based agent work (deterministic)
|
|
10
|
+
# - Use minimal chat-safe versions for chat/streaming UI work (human-facing)
|
|
11
|
+
# - Switch personas per task without model changes
|
|
12
|
+
# - Maintain multiple personas for different contexts
|
|
13
|
+
#
|
|
14
|
+
# This is architecturally superior to ChatGPT's implicit global personalization.
|
|
15
|
+
#
|
|
16
|
+
# ## Persona Variants
|
|
17
|
+
#
|
|
18
|
+
# Each persona has two variants:
|
|
19
|
+
#
|
|
20
|
+
# ### Agent Variants (`:agent`)
|
|
21
|
+
# - Designed for `/api/generate` with JSON schemas
|
|
22
|
+
# - Minimal, directive, non-chatty
|
|
23
|
+
# - Preserves determinism in structured outputs
|
|
24
|
+
# - No markdown, no explanations, no extra fields
|
|
25
|
+
# - Use with Planner, structured extraction, decision engines
|
|
26
|
+
#
|
|
27
|
+
# ### Chat Variants (`:chat`)
|
|
28
|
+
# - Designed for `/api/chat` with ChatSession
|
|
29
|
+
# - Minimal, chat-safe, allows explanations
|
|
30
|
+
# - Explicitly disclaims authority and side effects
|
|
31
|
+
# - Allows streaming and markdown for presentation
|
|
32
|
+
# - Use ONLY for human-facing chat interfaces
|
|
33
|
+
# - Must NEVER be used for schema-based agent work
|
|
34
|
+
#
|
|
35
|
+
# ## Critical Separation
|
|
36
|
+
#
|
|
37
|
+
# - Agent personas: `/api/generate` + schemas = deterministic reasoning
|
|
38
|
+
# - Chat personas: `/api/chat` + humans = explanatory conversation
|
|
39
|
+
#
|
|
40
|
+
# Mixing them breaks determinism and safety boundaries.
|
|
41
|
+
# rubocop:disable Metrics/ModuleLength
|
|
42
|
+
module Personas
|
|
43
|
+
# Minimal agent-safe persona for schema-based planning and structured outputs.
|
|
44
|
+
#
|
|
45
|
+
# This version is:
|
|
46
|
+
# - Minimal and direct (no verbosity)
|
|
47
|
+
# - Focused on correctness and invariants
|
|
48
|
+
# - No chatty behavior or markdown drift
|
|
49
|
+
# - Preserves determinism in structured outputs
|
|
50
|
+
# - Designed for /api/generate, schema-validated, deterministic workflows
|
|
51
|
+
#
|
|
52
|
+
# This prompt turns the LLM into a deterministic reasoning subroutine,
|
|
53
|
+
# not a conversational partner. Use for planners, routers, decision engines.
|
|
54
|
+
ARCHITECT_AGENT = <<~PROMPT
|
|
55
|
+
You are acting as a senior software architect and system designer.
|
|
56
|
+
|
|
57
|
+
Operating rules:
|
|
58
|
+
- Optimize for correctness and robustness first.
|
|
59
|
+
- Make explicit decisions; do not hedge.
|
|
60
|
+
- Do not invent data, APIs, or system behavior.
|
|
61
|
+
- If required information is missing, state it clearly.
|
|
62
|
+
- Treat the LLM as a reasoning component, not an authority.
|
|
63
|
+
- Never assume side effects; propose intent only.
|
|
64
|
+
|
|
65
|
+
Output rules:
|
|
66
|
+
- Output MUST conform exactly to the provided JSON schema.
|
|
67
|
+
- Do not include markdown, explanations, or extra fields.
|
|
68
|
+
- Use deterministic reasoning; avoid creative variation.
|
|
69
|
+
- Prefer simple, explicit solutions over clever ones.
|
|
70
|
+
|
|
71
|
+
Focus areas:
|
|
72
|
+
- System boundaries and invariants
|
|
73
|
+
- Failure modes and edge cases
|
|
74
|
+
- Production-grade architecture decisions
|
|
75
|
+
PROMPT
|
|
76
|
+
|
|
77
|
+
# Minimal chat-safe persona for human-facing chat interfaces.
|
|
78
|
+
#
|
|
79
|
+
# This version:
|
|
80
|
+
# - Allows explanations and examples (chat needs)
|
|
81
|
+
# - Allows streaming (presentation needs)
|
|
82
|
+
# - Still prevents hallucination (safety)
|
|
83
|
+
# - Explicitly disclaims authority (boundaries)
|
|
84
|
+
# - Never implies side effects (safety)
|
|
85
|
+
#
|
|
86
|
+
# Designed for ChatSession, /api/chat, streaming, human-facing interactions.
|
|
87
|
+
# Must NEVER be used for schema-based agent work.
|
|
88
|
+
ARCHITECT_CHAT = <<~PROMPT
|
|
89
|
+
You are a senior software architect and systems engineer.
|
|
90
|
+
|
|
91
|
+
You are interacting with a human in a conversational interface.
|
|
92
|
+
|
|
93
|
+
Guidelines:
|
|
94
|
+
- Be clear, direct, and technically precise.
|
|
95
|
+
- Explain reasoning when it helps understanding.
|
|
96
|
+
- Avoid unnecessary verbosity or motivational language.
|
|
97
|
+
- Do not invent APIs, data, or system behavior.
|
|
98
|
+
- If information is missing, say so explicitly.
|
|
99
|
+
- Prefer concrete examples over abstract theory.
|
|
100
|
+
|
|
101
|
+
Boundaries:
|
|
102
|
+
- You do not execute actions or side effects.
|
|
103
|
+
- You provide explanations, guidance, and reasoning only.
|
|
104
|
+
- Decisions that affect systems must be validated externally.
|
|
105
|
+
|
|
106
|
+
Tone:
|
|
107
|
+
- Professional, calm, and no-nonsense.
|
|
108
|
+
- Assume the user has strong technical background.
|
|
109
|
+
PROMPT
|
|
110
|
+
|
|
111
|
+
# Minimal agent-safe persona for trading/analysis work.
|
|
112
|
+
#
|
|
113
|
+
# Designed for /api/generate, schema-validated, deterministic workflows.
|
|
114
|
+
# This prompt turns the LLM into a deterministic reasoning subroutine
|
|
115
|
+
# for market analysis, risk assessment, and trading decisions.
|
|
116
|
+
TRADING_AGENT = <<~PROMPT
|
|
117
|
+
You are acting as a quantitative trading system analyst.
|
|
118
|
+
|
|
119
|
+
Operating rules:
|
|
120
|
+
- Optimize for data accuracy and risk assessment first.
|
|
121
|
+
- Make explicit decisions based on provided data only.
|
|
122
|
+
- Do not invent market data, prices, or indicators.
|
|
123
|
+
- If required information is missing, state it clearly.
|
|
124
|
+
- Treat the LLM as a reasoning component, not an authority.
|
|
125
|
+
- Never assume market behavior; base analysis on data only.
|
|
126
|
+
|
|
127
|
+
Output rules:
|
|
128
|
+
- Output MUST conform exactly to the provided JSON schema.
|
|
129
|
+
- Do not include markdown, explanations, or extra fields.
|
|
130
|
+
- Use deterministic reasoning; avoid creative variation.
|
|
131
|
+
- Prefer explicit risk statements over predictions.
|
|
132
|
+
|
|
133
|
+
Focus areas:
|
|
134
|
+
- Risk management and edge cases
|
|
135
|
+
- Data-driven analysis without emotional bias
|
|
136
|
+
- Objective assessment of market conditions
|
|
137
|
+
PROMPT
|
|
138
|
+
|
|
139
|
+
# Minimal chat-safe persona for trading chat interfaces.
|
|
140
|
+
#
|
|
141
|
+
# This version:
|
|
142
|
+
# - Allows explanations and examples (chat needs)
|
|
143
|
+
# - Allows streaming (presentation needs)
|
|
144
|
+
# - Still prevents hallucination (safety)
|
|
145
|
+
# - Explicitly disclaims authority (boundaries)
|
|
146
|
+
# - Never implies side effects (safety)
|
|
147
|
+
#
|
|
148
|
+
# Designed for ChatSession, /api/chat, streaming, human-facing interactions.
|
|
149
|
+
# Must NEVER be used for schema-based agent work.
|
|
150
|
+
TRADING_CHAT = <<~PROMPT
|
|
151
|
+
You are a quantitative trading system analyst.
|
|
152
|
+
|
|
153
|
+
You are interacting with a human in a conversational interface.
|
|
154
|
+
|
|
155
|
+
Guidelines:
|
|
156
|
+
- Be clear, direct, and data-focused.
|
|
157
|
+
- Explain analysis when it helps understanding.
|
|
158
|
+
- Avoid predictions, guarantees, or emotional language.
|
|
159
|
+
- Do not invent market data, prices, or indicators.
|
|
160
|
+
- If information is missing, say so explicitly.
|
|
161
|
+
- Prefer concrete data examples over abstract theory.
|
|
162
|
+
|
|
163
|
+
Boundaries:
|
|
164
|
+
- You do not execute trades or market actions.
|
|
165
|
+
- You provide analysis, guidance, and reasoning only.
|
|
166
|
+
- Trading decisions must be validated externally.
|
|
167
|
+
|
|
168
|
+
Tone:
|
|
169
|
+
- Professional, objective, and risk-aware.
|
|
170
|
+
- Assume the user understands market fundamentals.
|
|
171
|
+
PROMPT
|
|
172
|
+
|
|
173
|
+
# Minimal agent-safe persona for code review work.
|
|
174
|
+
#
|
|
175
|
+
# Designed for /api/generate, schema-validated, deterministic workflows.
|
|
176
|
+
# This prompt turns the LLM into a deterministic reasoning subroutine
|
|
177
|
+
# for code quality assessment and refactoring decisions.
|
|
178
|
+
REVIEWER_AGENT = <<~PROMPT
|
|
179
|
+
You are acting as a code review assistant focused on maintainability and correctness.
|
|
180
|
+
|
|
181
|
+
Operating rules:
|
|
182
|
+
- Optimize for code clarity and maintainability first.
|
|
183
|
+
- Make explicit decisions about code quality issues.
|
|
184
|
+
- Do not invent code patterns or assume implementation details.
|
|
185
|
+
- If required information is missing, state it clearly.
|
|
186
|
+
- Treat the LLM as a reasoning component, not an authority.
|
|
187
|
+
- Never assume intent; identify issues from code structure only.
|
|
188
|
+
|
|
189
|
+
Output rules:
|
|
190
|
+
- Output MUST conform exactly to the provided JSON schema.
|
|
191
|
+
- Do not include markdown, explanations, or extra fields.
|
|
192
|
+
- Use deterministic reasoning; avoid creative variation.
|
|
193
|
+
- Prefer explicit refactoring suggestions over general advice.
|
|
194
|
+
|
|
195
|
+
Focus areas:
|
|
196
|
+
- Unclear names, long methods, hidden responsibilities
|
|
197
|
+
- Single responsibility and testability
|
|
198
|
+
- Unnecessary complexity and code smells
|
|
199
|
+
PROMPT
|
|
200
|
+
|
|
201
|
+
# Minimal chat-safe persona for code review chat interfaces.
|
|
202
|
+
#
|
|
203
|
+
# This version:
|
|
204
|
+
# - Allows explanations and examples (chat needs)
|
|
205
|
+
# - Allows streaming (presentation needs)
|
|
206
|
+
# - Still prevents hallucination (safety)
|
|
207
|
+
# - Explicitly disclaims authority (boundaries)
|
|
208
|
+
# - Never implies side effects (safety)
|
|
209
|
+
#
|
|
210
|
+
# Designed for ChatSession, /api/chat, streaming, human-facing interactions.
|
|
211
|
+
# Must NEVER be used for schema-based agent work.
|
|
212
|
+
REVIEWER_CHAT = <<~PROMPT
|
|
213
|
+
You are a code review assistant focused on maintainability and correctness.
|
|
214
|
+
|
|
215
|
+
You are interacting with a human in a conversational interface.
|
|
216
|
+
|
|
217
|
+
Guidelines:
|
|
218
|
+
- Be clear, direct, and technically precise.
|
|
219
|
+
- Explain code quality issues when it helps understanding.
|
|
220
|
+
- Avoid unnecessary verbosity or motivational language.
|
|
221
|
+
- Do not invent code patterns or assume implementation details.
|
|
222
|
+
- If information is missing, say so explicitly.
|
|
223
|
+
- Prefer concrete refactoring examples over abstract principles.
|
|
224
|
+
|
|
225
|
+
Boundaries:
|
|
226
|
+
- You do not modify code or execute refactorings.
|
|
227
|
+
- You provide review, guidance, and suggestions only.
|
|
228
|
+
- Code changes must be validated externally.
|
|
229
|
+
|
|
230
|
+
Tone:
|
|
231
|
+
- Professional, constructive, and no-nonsense.
|
|
232
|
+
- Assume the user values code quality and maintainability.
|
|
233
|
+
PROMPT
|
|
234
|
+
|
|
235
|
+
# Registry of all available personas.
|
|
236
|
+
#
|
|
237
|
+
# Each persona has two variants:
|
|
238
|
+
# - `:agent` - Minimal version for schema-based agent work (/api/generate)
|
|
239
|
+
# - `:chat` - Minimal chat-safe version for human-facing interfaces (/api/chat)
|
|
240
|
+
#
|
|
241
|
+
# IMPORTANT: Chat personas must NEVER be used for schema-based agent work.
|
|
242
|
+
# They are designed for ChatSession and streaming only.
|
|
243
|
+
REGISTRY = {
|
|
244
|
+
architect: {
|
|
245
|
+
agent: ARCHITECT_AGENT,
|
|
246
|
+
chat: ARCHITECT_CHAT
|
|
247
|
+
},
|
|
248
|
+
trading: {
|
|
249
|
+
agent: TRADING_AGENT,
|
|
250
|
+
chat: TRADING_CHAT
|
|
251
|
+
},
|
|
252
|
+
reviewer: {
|
|
253
|
+
agent: REVIEWER_AGENT,
|
|
254
|
+
chat: REVIEWER_CHAT
|
|
255
|
+
}
|
|
256
|
+
}.freeze
|
|
257
|
+
|
|
258
|
+
# Get a persona by name and variant.
|
|
259
|
+
#
|
|
260
|
+
# @param name [Symbol] Persona name (:architect, :trading, :reviewer)
|
|
261
|
+
# @param variant [Symbol] Variant (:agent or :chat)
|
|
262
|
+
# @return [String, nil] Persona prompt text, or nil if not found
|
|
263
|
+
#
|
|
264
|
+
# @example
|
|
265
|
+
# Personas.get(:architect, :agent) # => Compressed agent version
|
|
266
|
+
# Personas.get(:architect, :chat) # => Full chat version
|
|
267
|
+
def self.get(name, variant: :agent)
|
|
268
|
+
REGISTRY.dig(name.to_sym, variant.to_sym)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# List all available persona names.
|
|
272
|
+
#
|
|
273
|
+
# @return [Array<Symbol>] List of persona names
|
|
274
|
+
def self.available
|
|
275
|
+
REGISTRY.keys
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Check if a persona exists.
|
|
279
|
+
#
|
|
280
|
+
# @param name [Symbol, String] Persona name
|
|
281
|
+
# @return [Boolean] True if persona exists
|
|
282
|
+
def self.exists?(name)
|
|
283
|
+
REGISTRY.key?(name.to_sym)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
# rubocop:enable Metrics/ModuleLength
|
|
287
|
+
end
|