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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/README.md +336 -91
  4. data/RELEASE_NOTES_v0.2.6.md +41 -0
  5. data/docs/AREAS_FOR_CONSIDERATION.md +325 -0
  6. data/docs/EXAMPLE_REORGANIZATION.md +412 -0
  7. data/docs/FEATURES_ADDED.md +12 -1
  8. data/docs/GETTING_STARTED.md +361 -0
  9. data/docs/INTEGRATION_TESTING.md +170 -0
  10. data/docs/NEXT_STEPS_SUMMARY.md +114 -0
  11. data/docs/PERSONAS.md +383 -0
  12. data/docs/QUICK_START.md +195 -0
  13. data/docs/TESTING.md +392 -170
  14. data/docs/TEST_CHECKLIST.md +450 -0
  15. data/examples/README.md +62 -63
  16. data/examples/basic_chat.rb +33 -0
  17. data/examples/basic_generate.rb +29 -0
  18. data/examples/mcp_executor.rb +39 -0
  19. data/examples/mcp_http_executor.rb +45 -0
  20. data/examples/tool_calling_parsing.rb +59 -0
  21. data/examples/tool_dto_example.rb +0 -0
  22. data/exe/ollama-client +128 -1
  23. data/lib/ollama/agent/planner.rb +7 -2
  24. data/lib/ollama/chat_session.rb +101 -0
  25. data/lib/ollama/client.rb +41 -35
  26. data/lib/ollama/config.rb +9 -4
  27. data/lib/ollama/document_loader.rb +1 -1
  28. data/lib/ollama/embeddings.rb +61 -28
  29. data/lib/ollama/errors.rb +1 -0
  30. data/lib/ollama/mcp/http_client.rb +149 -0
  31. data/lib/ollama/mcp/stdio_client.rb +146 -0
  32. data/lib/ollama/mcp/tools_bridge.rb +72 -0
  33. data/lib/ollama/mcp.rb +31 -0
  34. data/lib/ollama/options.rb +3 -1
  35. data/lib/ollama/personas.rb +287 -0
  36. data/lib/ollama/version.rb +1 -1
  37. data/lib/ollama_client.rb +17 -5
  38. metadata +22 -48
  39. data/examples/advanced_complex_schemas.rb +0 -366
  40. data/examples/advanced_edge_cases.rb +0 -241
  41. data/examples/advanced_error_handling.rb +0 -200
  42. data/examples/advanced_multi_step_agent.rb +0 -341
  43. data/examples/advanced_performance_testing.rb +0 -186
  44. data/examples/chat_console.rb +0 -143
  45. data/examples/complete_workflow.rb +0 -245
  46. data/examples/dhan_console.rb +0 -843
  47. data/examples/dhanhq/README.md +0 -236
  48. data/examples/dhanhq/agents/base_agent.rb +0 -74
  49. data/examples/dhanhq/agents/data_agent.rb +0 -66
  50. data/examples/dhanhq/agents/orchestrator_agent.rb +0 -120
  51. data/examples/dhanhq/agents/technical_analysis_agent.rb +0 -252
  52. data/examples/dhanhq/agents/trading_agent.rb +0 -81
  53. data/examples/dhanhq/analysis/market_structure.rb +0 -138
  54. data/examples/dhanhq/analysis/pattern_recognizer.rb +0 -192
  55. data/examples/dhanhq/analysis/trend_analyzer.rb +0 -88
  56. data/examples/dhanhq/builders/market_context_builder.rb +0 -67
  57. data/examples/dhanhq/dhanhq_agent.rb +0 -829
  58. data/examples/dhanhq/indicators/technical_indicators.rb +0 -158
  59. data/examples/dhanhq/scanners/intraday_options_scanner.rb +0 -492
  60. data/examples/dhanhq/scanners/swing_scanner.rb +0 -247
  61. data/examples/dhanhq/schemas/agent_schemas.rb +0 -61
  62. data/examples/dhanhq/services/base_service.rb +0 -46
  63. data/examples/dhanhq/services/data_service.rb +0 -118
  64. data/examples/dhanhq/services/trading_service.rb +0 -59
  65. data/examples/dhanhq/technical_analysis_agentic_runner.rb +0 -411
  66. data/examples/dhanhq/technical_analysis_runner.rb +0 -420
  67. data/examples/dhanhq/test_tool_calling.rb +0 -538
  68. data/examples/dhanhq/test_tool_calling_verbose.rb +0 -251
  69. data/examples/dhanhq/utils/instrument_helper.rb +0 -32
  70. data/examples/dhanhq/utils/parameter_cleaner.rb +0 -28
  71. data/examples/dhanhq/utils/parameter_normalizer.rb +0 -45
  72. data/examples/dhanhq/utils/rate_limiter.rb +0 -23
  73. data/examples/dhanhq/utils/trading_parameter_normalizer.rb +0 -72
  74. data/examples/dhanhq_agent.rb +0 -964
  75. data/examples/dhanhq_tools.rb +0 -1663
  76. data/examples/multi_step_agent_with_external_data.rb +0 -368
  77. data/examples/structured_outputs_chat.rb +0 -72
  78. data/examples/structured_tools.rb +0 -89
  79. data/examples/test_dhanhq_tool_calling.rb +0 -375
  80. data/examples/test_tool_calling.rb +0 -160
  81. data/examples/tool_calling_direct.rb +0 -124
  82. data/examples/tool_calling_pattern.rb +0 -269
  83. 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"
@@ -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.generate(prompt: "...", schema: {...}, options: options.to_h)
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