prescient 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +21 -268
- data/CHANGELOG.md +59 -0
- data/INTEGRATION_GUIDE.md +19 -1
- data/README.md +369 -21
- data/Steepfile +12 -12
- data/db/migrate/001_create_prescient_tables.rb +15 -16
- data/docker-compose.yml +22 -0
- data/examples/README.md +36 -1
- data/examples/custom_contexts.rb +4 -4
- data/examples/web_search.rb +34 -0
- data/exe/prescient +2 -2
- data/exe/prescient-mcp +7 -0
- data/lib/prescient/agent/audit_log.rb +37 -0
- data/lib/prescient/agent/cli_adapter.rb +29 -0
- data/lib/prescient/agent/configuration.rb +57 -0
- data/lib/prescient/agent/context.rb +56 -0
- data/lib/prescient/agent/error_serializer.rb +47 -0
- data/lib/prescient/agent/errors.rb +25 -0
- data/lib/prescient/agent/parser.rb +49 -0
- data/lib/prescient/agent/prompt_builder.rb +31 -0
- data/lib/prescient/agent/result.rb +36 -0
- data/lib/prescient/agent/runtime.rb +175 -0
- data/lib/prescient/agent/schema_validator.rb +215 -0
- data/lib/prescient/agent/tool_registry.rb +89 -0
- data/lib/prescient/agent.rb +22 -0
- data/lib/prescient/api.rb +346 -231
- data/lib/prescient/base.rb +370 -372
- data/lib/prescient/cli.rb +591 -402
- data/lib/prescient/client.rb +31 -4
- data/lib/prescient/configuration_loader.rb +511 -336
- data/lib/prescient/document_source.rb +114 -0
- data/lib/prescient/errors.rb +13 -3
- data/lib/prescient/mcp/authentication.rb +39 -0
- data/lib/prescient/mcp/configuration.rb +38 -0
- data/lib/prescient/mcp/rack.rb +243 -0
- data/lib/prescient/mcp/server.rb +202 -0
- data/lib/prescient/mcp/stdio.rb +42 -0
- data/lib/prescient/mcp.rb +8 -0
- data/lib/prescient/pgvector.rb +193 -189
- data/lib/prescient/provider/anthropic.rb +129 -125
- data/lib/prescient/provider/deepseek.rb +122 -118
- data/lib/prescient/provider/gemini.rb +153 -149
- data/lib/prescient/provider/huggingface.rb +191 -187
- data/lib/prescient/provider/mistral.rb +151 -147
- data/lib/prescient/provider/ollama.rb +168 -165
- data/lib/prescient/provider/openai.rb +174 -171
- data/lib/prescient/provider/xai.rb +122 -118
- data/lib/prescient/tool/search_api.rb +130 -0
- data/lib/prescient/tool/searxng.rb +128 -0
- data/lib/prescient/tool.rb +125 -0
- data/lib/prescient/version.rb +1 -1
- data/lib/prescient.rb +129 -55
- data/schema/prescient.configuration.schema.json +119 -0
- data/searxng/settings.yml +18 -0
- data/sig/prescient.rbs +228 -1
- metadata +33 -5
data/lib/prescient/cli.rb
CHANGED
|
@@ -1,480 +1,669 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require_relative
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
require "yaml"
|
|
6
|
+
require_relative "../prescient"
|
|
7
|
+
|
|
8
|
+
module Prescient
|
|
9
|
+
# Command-line interface for common Prescient operations.
|
|
10
|
+
# rubocop:disable Metrics/ClassLength
|
|
11
|
+
class CLI
|
|
12
|
+
# @return [Hash<String, Symbol>] CLI command dispatch table
|
|
13
|
+
COMMAND_HANDLERS = {
|
|
14
|
+
"providers" => :providers,
|
|
15
|
+
"health" => :health,
|
|
16
|
+
"generate" => :generate,
|
|
17
|
+
"embed" => :embed,
|
|
18
|
+
"search" => :search,
|
|
19
|
+
"agent" => :agent,
|
|
20
|
+
"config" => :config
|
|
21
|
+
}.freeze
|
|
22
|
+
# Supported output formats.
|
|
23
|
+
# @return [Array<String>] Output format names
|
|
24
|
+
FORMATS = %w[text json].freeze
|
|
25
|
+
|
|
26
|
+
# Schema URL and annotated starter configuration for `config example`.
|
|
27
|
+
CONFIGURATION_EXAMPLE = <<~YAML
|
|
28
|
+
# yaml-language-server: $schema=https://raw.githubusercontent.com/kanutocd/prescient/refs/heads/main/schema/prescient.configuration.schema.json
|
|
29
|
+
#
|
|
30
|
+
# Prescient configuration example.
|
|
31
|
+
#
|
|
32
|
+
# Precedence, from lowest to highest:
|
|
33
|
+
# 1. Built-in defaults and provider environment variables.
|
|
34
|
+
# 2. Values in this YAML file.
|
|
35
|
+
# 3. Per-operation CLI overrides such as --provider and --chat-model.
|
|
36
|
+
#
|
|
37
|
+
# Use `prescient config validate` after editing this file.
|
|
38
|
+
# Keep credentials out of source control; use *_env references instead.
|
|
39
|
+
version: 1
|
|
40
|
+
|
|
41
|
+
# Global behavior.
|
|
42
|
+
default_provider: ollama
|
|
43
|
+
timeout: 30
|
|
44
|
+
retry_attempts: 3
|
|
45
|
+
retry_delay: 1.0
|
|
46
|
+
fallback_providers: []
|
|
47
|
+
sensitive_keys:
|
|
48
|
+
- api_key
|
|
49
|
+
- password
|
|
50
|
+
- token
|
|
51
|
+
- secret
|
|
52
|
+
|
|
53
|
+
providers:
|
|
54
|
+
# Local Ollama requires no API key.
|
|
55
|
+
ollama:
|
|
56
|
+
type: ollama
|
|
57
|
+
url: http://localhost:11434
|
|
58
|
+
embedding_model: nomic-embed-text
|
|
59
|
+
chat_model: llama3.2:3b
|
|
60
|
+
# prompt_templates:
|
|
61
|
+
# system_prompt: You are a concise assistant.
|
|
62
|
+
# no_context_template: "%<system_prompt>s\x5Cn\x5CnUser: %<query>s"
|
|
63
|
+
# with_context_template: "%<system_prompt>s\x5Cn\x5CnContext:\x5Cn%<context>s\x5Cn\x5CnUser: %<query>s"
|
|
64
|
+
|
|
65
|
+
# Uncomment a cloud provider and set its credential in the environment.
|
|
66
|
+
# openai:
|
|
67
|
+
# type: openai
|
|
68
|
+
# api_key_env: OPENAI_API_KEY
|
|
69
|
+
# embedding_model: text-embedding-3-small
|
|
70
|
+
# chat_model: gpt-4.1-mini
|
|
71
|
+
# prompt_templates:
|
|
72
|
+
# system_prompt: You are a concise assistant.
|
|
73
|
+
# no_context_template: "%<system_prompt>s\x5Cn\x5CnUser: %<query>s"
|
|
74
|
+
|
|
75
|
+
# anthropic:
|
|
76
|
+
# type: anthropic
|
|
77
|
+
# api_key_env: ANTHROPIC_API_KEY
|
|
78
|
+
# model: claude-sonnet-4-20250514
|
|
79
|
+
|
|
80
|
+
# gemini:
|
|
81
|
+
# type: gemini
|
|
82
|
+
# api_key_env: GEMINI_API_KEY
|
|
83
|
+
# embedding_model: gemini-embedding-001
|
|
84
|
+
# chat_model: gemini-2.5-flash
|
|
85
|
+
|
|
86
|
+
# mistral:
|
|
87
|
+
# type: mistral
|
|
88
|
+
# api_key_env: MISTRAL_API_KEY
|
|
89
|
+
# embedding_model: mistral-embed
|
|
90
|
+
# chat_model: mistral-large-latest
|
|
91
|
+
|
|
92
|
+
# DeepSeek supports text generation, but not embeddings.
|
|
93
|
+
# deepseek:
|
|
94
|
+
# type: deepseek
|
|
95
|
+
# api_key_env: DEEPSEEK_API_KEY
|
|
96
|
+
# chat_model: deepseek-v4-flash
|
|
97
|
+
|
|
98
|
+
# xai:
|
|
99
|
+
# type: xai
|
|
100
|
+
# api_key_env: XAI_API_KEY
|
|
101
|
+
# chat_model: grok-4.5
|
|
102
|
+
|
|
103
|
+
# huggingface:
|
|
104
|
+
# type: huggingface
|
|
105
|
+
# api_key_env: HUGGINGFACE_API_KEY
|
|
106
|
+
# embedding_model: sentence-transformers/all-MiniLM-L6-v2
|
|
107
|
+
# chat_model: google/gemma-2-2b-it
|
|
108
|
+
|
|
109
|
+
# External tools are opt-in and separate from AI providers. They can be
|
|
110
|
+
# used directly with `prescient search`, or as context with
|
|
111
|
+
# `prescient search --generate`.
|
|
112
|
+
#
|
|
113
|
+
# The CLI also registers `web_search` automatically when SEARXNG_URL is
|
|
114
|
+
# set and no YAML tool configuration is provided.
|
|
115
|
+
tools:
|
|
116
|
+
# Local SearXNG example. Uncomment this block to configure a tool in YAML.
|
|
117
|
+
# web_search:
|
|
118
|
+
# type: searxng
|
|
119
|
+
# url: http://localhost:8080
|
|
120
|
+
# timeout: 5
|
|
121
|
+
# max_results: 5
|
|
122
|
+
# language: en
|
|
123
|
+
# categories:
|
|
124
|
+
# - general
|
|
125
|
+
# - science
|
|
126
|
+
# max_response_bytes: 1048576
|
|
127
|
+
|
|
128
|
+
# SearchApi example. It uses SearchApi's Google engine by default and
|
|
129
|
+
# authenticates with a Bearer token from the environment.
|
|
130
|
+
# searchapi_web:
|
|
131
|
+
# type: searchapi
|
|
132
|
+
# api_key_env: SEARCHAPI_API_KEY
|
|
133
|
+
# engine: google
|
|
134
|
+
# location: New York
|
|
135
|
+
# hl: en
|
|
136
|
+
# gl: us
|
|
137
|
+
# timeout: 10
|
|
138
|
+
# max_results: 5
|
|
139
|
+
|
|
140
|
+
# Capability fallback. Adapters are tried in order, and fallback occurs
|
|
141
|
+
# only for transient connection or rate-limit failures.
|
|
142
|
+
# resilient_search:
|
|
143
|
+
# adapters:
|
|
144
|
+
# - type: searxng
|
|
145
|
+
# url_env: SEARXNG_URL
|
|
146
|
+
# - type: searchapi
|
|
147
|
+
# api_key_env: SEARCHAPI_API_KEY
|
|
148
|
+
# engine: google
|
|
149
|
+
|
|
150
|
+
# Prefer an environment reference when the URL differs by environment
|
|
151
|
+
# or should not be committed. Use `--tool research_search` to select a
|
|
152
|
+
# tool with a custom name.
|
|
153
|
+
# research_search:
|
|
154
|
+
# type: searxng
|
|
155
|
+
# url_env: SEARXNG_URL
|
|
156
|
+
# timeout_env: SEARXNG_TIMEOUT
|
|
157
|
+
# max_results_env: SEARXNG_MAX_RESULTS
|
|
158
|
+
# language_env: SEARXNG_LANGUAGE
|
|
159
|
+
# categories_env: SEARXNG_CATEGORIES
|
|
160
|
+
|
|
161
|
+
# Search results are returned directly by default. Add `--generate` to
|
|
162
|
+
# feed normalized results to the selected AI provider. Omit `--generate`
|
|
163
|
+
# when the caller should handle the search results itself.
|
|
164
|
+
YAML
|
|
165
|
+
|
|
166
|
+
# Raised when command-line arguments are invalid or incomplete.
|
|
167
|
+
class UsageError < StandardError; end
|
|
168
|
+
|
|
169
|
+
# Run the CLI and return a process exit status.
|
|
17
170
|
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
|
|
171
|
+
# @param arguments [Array<String>] Command-line arguments
|
|
172
|
+
# @param input [IO] Input stream used for stdin prompts
|
|
173
|
+
# @param output [IO] Output stream for command results
|
|
174
|
+
# @param errors [IO] Output stream for diagnostics
|
|
175
|
+
# @return [Integer] Process exit status
|
|
176
|
+
def self.run(arguments, input: $stdin, output: $stdout, errors: $stderr)
|
|
177
|
+
new(arguments, input:, output:, errors:).run
|
|
178
|
+
rescue UsageError, OptionParser::ParseError => e
|
|
179
|
+
errors.puts "prescient: #{e.message}"
|
|
180
|
+
2
|
|
181
|
+
rescue Prescient::Error => e
|
|
182
|
+
errors.puts "prescient: #{e.message}"
|
|
183
|
+
1
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Initialize a CLI runner with injectable streams.
|
|
24
187
|
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
sensitive_keys:
|
|
36
|
-
- api_key
|
|
37
|
-
- password
|
|
38
|
-
- token
|
|
39
|
-
- secret
|
|
40
|
-
|
|
41
|
-
providers:
|
|
42
|
-
# Local Ollama requires no API key.
|
|
43
|
-
ollama:
|
|
44
|
-
type: ollama
|
|
45
|
-
url: http://localhost:11434
|
|
46
|
-
embedding_model: nomic-embed-text
|
|
47
|
-
chat_model: llama3.2:3b
|
|
48
|
-
# prompt_templates:
|
|
49
|
-
# system_prompt: You are a concise assistant.
|
|
50
|
-
# no_context_template: "%<system_prompt>s\\n\\nUser: %<query>s"
|
|
51
|
-
# with_context_template: "%<system_prompt>s\\n\\nContext:\\n%<context>s\\n\\nUser: %<query>s"
|
|
52
|
-
|
|
53
|
-
# Uncomment a cloud provider and set its credential in the environment.
|
|
54
|
-
# openai:
|
|
55
|
-
# type: openai
|
|
56
|
-
# api_key_env: OPENAI_API_KEY
|
|
57
|
-
# embedding_model: text-embedding-3-small
|
|
58
|
-
# chat_model: gpt-4.1-mini
|
|
59
|
-
# prompt_templates:
|
|
60
|
-
# system_prompt: You are a concise assistant.
|
|
61
|
-
# no_context_template: "%<system_prompt>s\n\nUser: %<query>s"
|
|
62
|
-
|
|
63
|
-
# anthropic:
|
|
64
|
-
# type: anthropic
|
|
65
|
-
# api_key_env: ANTHROPIC_API_KEY
|
|
66
|
-
# model: claude-sonnet-4-20250514
|
|
67
|
-
|
|
68
|
-
# gemini:
|
|
69
|
-
# type: gemini
|
|
70
|
-
# api_key_env: GEMINI_API_KEY
|
|
71
|
-
# embedding_model: gemini-embedding-001
|
|
72
|
-
# chat_model: gemini-2.5-flash
|
|
73
|
-
|
|
74
|
-
# mistral:
|
|
75
|
-
# type: mistral
|
|
76
|
-
# api_key_env: MISTRAL_API_KEY
|
|
77
|
-
# embedding_model: mistral-embed
|
|
78
|
-
# chat_model: mistral-large-latest
|
|
79
|
-
|
|
80
|
-
# DeepSeek supports text generation, but not embeddings.
|
|
81
|
-
# deepseek:
|
|
82
|
-
# type: deepseek
|
|
83
|
-
# api_key_env: DEEPSEEK_API_KEY
|
|
84
|
-
# chat_model: deepseek-v4-flash
|
|
85
|
-
|
|
86
|
-
# xai:
|
|
87
|
-
# type: xai
|
|
88
|
-
# api_key_env: XAI_API_KEY
|
|
89
|
-
# chat_model: grok-4.5
|
|
90
|
-
|
|
91
|
-
# huggingface:
|
|
92
|
-
# type: huggingface
|
|
93
|
-
# api_key_env: HUGGINGFACE_API_KEY
|
|
94
|
-
# embedding_model: sentence-transformers/all-MiniLM-L6-v2
|
|
95
|
-
# chat_model: google/gemma-2-2b-it
|
|
96
|
-
YAML
|
|
97
|
-
|
|
98
|
-
# Raised when command-line arguments are invalid or incomplete.
|
|
99
|
-
class UsageError < StandardError; end
|
|
100
|
-
|
|
101
|
-
# Run the CLI and return a process exit status.
|
|
102
|
-
#
|
|
103
|
-
# @param arguments [Array<String>] Command-line arguments
|
|
104
|
-
# @param input [IO] Input stream used for stdin prompts
|
|
105
|
-
# @param output [IO] Output stream for command results
|
|
106
|
-
# @param errors [IO] Output stream for diagnostics
|
|
107
|
-
# @return [Integer] Process exit status
|
|
108
|
-
def self.run(arguments, input: $stdin, output: $stdout, errors: $stderr)
|
|
109
|
-
new(arguments, input:, output:, errors:).run
|
|
110
|
-
rescue UsageError, OptionParser::ParseError => e
|
|
111
|
-
errors.puts "prescient: #{e.message}"
|
|
112
|
-
2
|
|
113
|
-
rescue Prescient::Error => e
|
|
114
|
-
errors.puts "prescient: #{e.message}"
|
|
115
|
-
1
|
|
116
|
-
end
|
|
188
|
+
# @param arguments [Array<String>] Command-line arguments
|
|
189
|
+
# @param input [IO] Input stream used for stdin prompts
|
|
190
|
+
# @param output [IO] Output stream for command results
|
|
191
|
+
# @param errors [IO] Output stream for diagnostics
|
|
192
|
+
def initialize(arguments, input:, output:, errors:)
|
|
193
|
+
@arguments = arguments.dup
|
|
194
|
+
@input = input
|
|
195
|
+
@output = output
|
|
196
|
+
@errors = errors
|
|
197
|
+
end
|
|
117
198
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
# @param errors [IO] Output stream for diagnostics
|
|
124
|
-
def initialize(arguments, input:, output:, errors:)
|
|
125
|
-
@arguments = arguments.dup
|
|
126
|
-
@input = input
|
|
127
|
-
@output = output
|
|
128
|
-
@errors = errors
|
|
129
|
-
end
|
|
199
|
+
# Execute the CLI command and return its process status.
|
|
200
|
+
# @return [Integer] Process exit status
|
|
201
|
+
def run
|
|
202
|
+
config_path = extract_global_config_path
|
|
203
|
+
Prescient.load_configuration(config_path) if config_path || ENV["PRESCIENT_CONFIG"]
|
|
130
204
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def run
|
|
134
|
-
config_path = extract_global_config_path
|
|
135
|
-
Prescient.load_configuration(config_path) if config_path || ENV['PRESCIENT_CONFIG']
|
|
205
|
+
command = @arguments.shift
|
|
206
|
+
return print_help(2) unless command
|
|
136
207
|
|
|
137
|
-
|
|
138
|
-
|
|
208
|
+
run_command(command)
|
|
209
|
+
end
|
|
139
210
|
|
|
140
|
-
|
|
141
|
-
|
|
211
|
+
# Dispatch a parsed command to its handler.
|
|
212
|
+
# @param command [String] Command name
|
|
213
|
+
# @return [Integer] Process exit status
|
|
214
|
+
def run_command(command)
|
|
215
|
+
return print_help(0) if ["help", "--help", "-h"].include?(command)
|
|
216
|
+
|
|
217
|
+
handler = COMMAND_HANDLERS[command]
|
|
218
|
+
raise UsageError, "unknown command #{command.inspect}; run 'prescient help'" unless handler
|
|
142
219
|
|
|
143
|
-
|
|
144
|
-
# @param command [String] Command name
|
|
145
|
-
# @return [Integer] Process exit status
|
|
146
|
-
def run_command(command)
|
|
147
|
-
case command
|
|
148
|
-
when 'providers' then providers
|
|
149
|
-
when 'health' then health
|
|
150
|
-
when 'generate' then generate
|
|
151
|
-
when 'embed' then embed
|
|
152
|
-
when 'config' then config
|
|
153
|
-
when 'help', '--help', '-h' then print_help(0)
|
|
154
|
-
else
|
|
155
|
-
raise UsageError, "unknown command #{command.inspect}; run 'prescient help'"
|
|
220
|
+
send(handler)
|
|
156
221
|
end
|
|
157
|
-
end
|
|
158
222
|
|
|
159
|
-
|
|
223
|
+
private
|
|
160
224
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
225
|
+
def providers
|
|
226
|
+
options = parse_options("List configured providers")
|
|
227
|
+
return options if options.is_a?(Integer)
|
|
164
228
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
229
|
+
provider_list = Prescient.configuration.providers.map do |name, registration|
|
|
230
|
+
{ name: name.to_s, class: registration[:class].name }
|
|
231
|
+
end
|
|
168
232
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
233
|
+
if options[:format] == "json"
|
|
234
|
+
print_json(providers: provider_list)
|
|
235
|
+
else
|
|
236
|
+
provider_list.each { |provider| @output.puts "#{provider[:name]}\t#{provider[:class]}" }
|
|
237
|
+
end
|
|
238
|
+
0
|
|
173
239
|
end
|
|
174
|
-
0
|
|
175
|
-
end
|
|
176
240
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
241
|
+
def health
|
|
242
|
+
options = parse_options("Check provider health")
|
|
243
|
+
return options if options.is_a?(Integer)
|
|
180
244
|
|
|
181
|
-
|
|
182
|
-
|
|
245
|
+
names = options[:provider] ? [options[:provider].to_sym] : Prescient.configuration.providers.keys
|
|
246
|
+
raise UsageError, "no providers are configured" if names.empty?
|
|
183
247
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
248
|
+
results = names.to_h { |name| [name.to_s, Prescient.health_check(provider: name)] }
|
|
249
|
+
output_health(results, options[:format])
|
|
250
|
+
results.values.all? { |result| result[:reachable] != false } ? 0 : 1
|
|
251
|
+
end
|
|
188
252
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
253
|
+
def generate
|
|
254
|
+
options = parse_options("Generate a text response", fallback: true, documents: true)
|
|
255
|
+
return options if options.is_a?(Integer)
|
|
192
256
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
257
|
+
prompt = read_text(options[:arguments], "prompt")
|
|
258
|
+
client = client_for(options)
|
|
259
|
+
context = if options[:json_file]
|
|
260
|
+
Prescient::DocumentSource::JsonFile.new(path: options[:json_file]).fetch
|
|
261
|
+
else
|
|
262
|
+
[]
|
|
263
|
+
end
|
|
264
|
+
response = client.generate_response(prompt, context, **model_options(options))
|
|
196
265
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
266
|
+
options[:format] == "json" ? print_json(response) : @output.puts(response[:response])
|
|
267
|
+
0
|
|
268
|
+
end
|
|
200
269
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
270
|
+
def embed
|
|
271
|
+
options = parse_options("Generate an embedding", fallback: true)
|
|
272
|
+
return options if options.is_a?(Integer)
|
|
204
273
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
274
|
+
text = read_text(options[:arguments], "text")
|
|
275
|
+
client = client_for(options)
|
|
276
|
+
embedding = client.generate_embedding(text, **model_options(options))
|
|
208
277
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
278
|
+
if options[:format] == "json"
|
|
279
|
+
print_json(embedding: embedding, dimensions: embedding.length, provider: client.provider_name.to_s)
|
|
280
|
+
else
|
|
281
|
+
@output.puts JSON.generate(embedding)
|
|
282
|
+
end
|
|
283
|
+
0
|
|
213
284
|
end
|
|
214
|
-
0
|
|
215
|
-
end
|
|
216
285
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
286
|
+
def search
|
|
287
|
+
options = parse_options(
|
|
288
|
+
"Search with a configured external tool",
|
|
289
|
+
fallback: true, tool: true, limit: true, generate: true
|
|
290
|
+
)
|
|
291
|
+
return options if options.is_a?(Integer)
|
|
292
|
+
|
|
293
|
+
query = read_text(options[:arguments], "query")
|
|
294
|
+
tool_name = (options[:tool] || "web_search").to_sym
|
|
295
|
+
return generate_search_response(query, tool_name, options) if options[:generate]
|
|
296
|
+
|
|
297
|
+
tool = Prescient.tool(tool_name)
|
|
298
|
+
raise UsageError, "tool not configured: #{tool_name}" unless tool
|
|
299
|
+
|
|
300
|
+
result = tool.search(query, limit: options[:limit])
|
|
301
|
+
if options[:format] == "json"
|
|
302
|
+
print_json(result)
|
|
303
|
+
else
|
|
304
|
+
print_search_results(result[:results])
|
|
305
|
+
end
|
|
306
|
+
0
|
|
224
307
|
end
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
def validate_config_command
|
|
228
|
-
options = parse_options('Validate the current configuration')
|
|
229
|
-
return options if options.is_a?(Integer)
|
|
230
308
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
309
|
+
def agent
|
|
310
|
+
require "prescient/agent"
|
|
311
|
+
options = parse_options("Run a bounded agent task", agent: true)
|
|
312
|
+
return options if options.is_a?(Integer)
|
|
313
|
+
|
|
314
|
+
task = read_text(options[:arguments], "task")
|
|
315
|
+
Prescient::Agent::CLIAdapter.new(output: @output, errors: @errors).run(
|
|
316
|
+
task:,
|
|
317
|
+
provider: options[:provider]&.to_sym,
|
|
318
|
+
tool_names: options.fetch(:tools, []),
|
|
319
|
+
max_loops: options[:max_loops] || Prescient::Agent::Configuration::DEFAULT_MAX_LOOPS,
|
|
320
|
+
format: options[:format],
|
|
321
|
+
provider_options: provider_options(options),
|
|
322
|
+
generation_options: model_options(options)
|
|
323
|
+
)
|
|
236
324
|
end
|
|
237
|
-
0
|
|
238
|
-
end
|
|
239
325
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
326
|
+
def generate_search_response(query, tool_name, options)
|
|
327
|
+
response = Prescient.search_and_generate(
|
|
328
|
+
query,
|
|
329
|
+
tool: tool_name,
|
|
330
|
+
provider: options[:provider]&.to_sym,
|
|
331
|
+
limit: options[:limit],
|
|
332
|
+
enable_fallback: options[:fallback],
|
|
333
|
+
provider_options: provider_options(options),
|
|
334
|
+
**model_options(options)
|
|
335
|
+
)
|
|
336
|
+
options[:format] == "json" ? print_json(response) : @output.puts(response[:response])
|
|
337
|
+
0
|
|
338
|
+
end
|
|
243
339
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
340
|
+
def print_search_results(results)
|
|
341
|
+
results.each do |item|
|
|
342
|
+
@output.puts item[:title]
|
|
343
|
+
@output.puts item[:url]
|
|
344
|
+
@output.puts item[:snippet] unless item[:snippet].empty?
|
|
345
|
+
@output.puts
|
|
346
|
+
end
|
|
347
|
+
end
|
|
247
348
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
349
|
+
def config
|
|
350
|
+
subcommand = @arguments.shift
|
|
351
|
+
case subcommand
|
|
352
|
+
when "validate" then validate_config_command
|
|
353
|
+
when "example" then configuration_example_command
|
|
354
|
+
else
|
|
355
|
+
raise UsageError, "unknown config command #{subcommand.inspect}"
|
|
356
|
+
end
|
|
252
357
|
end
|
|
253
358
|
|
|
254
|
-
|
|
255
|
-
|
|
359
|
+
def validate_config_command
|
|
360
|
+
options = parse_options("Validate the current configuration")
|
|
361
|
+
return options if options.is_a?(Integer)
|
|
256
362
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
add_common_options(parser, options)
|
|
263
|
-
parser.on('--no-fallback', 'Disable provider fallback') { options[:fallback] = false } if fallback
|
|
264
|
-
parser.on('-h', '--help', 'Show command help') do
|
|
265
|
-
@output.puts parser
|
|
266
|
-
throw :help_shown, 0
|
|
363
|
+
validate_configuration
|
|
364
|
+
if options[:format] == "json"
|
|
365
|
+
print_json(valid: true, providers: Prescient.configuration.providers.keys.map(&:to_s))
|
|
366
|
+
else
|
|
367
|
+
@output.puts "configuration valid"
|
|
267
368
|
end
|
|
369
|
+
0
|
|
268
370
|
end
|
|
269
371
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
options[:arguments] = @arguments
|
|
274
|
-
options
|
|
275
|
-
end
|
|
372
|
+
def configuration_example_command
|
|
373
|
+
options = parse_options("Generate an annotated YAML configuration example")
|
|
374
|
+
return options if options.is_a?(Integer)
|
|
276
375
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
options[:config] = value
|
|
376
|
+
@output.write(CONFIGURATION_EXAMPLE)
|
|
377
|
+
0
|
|
280
378
|
end
|
|
281
|
-
parser.on('--format FORMAT', FORMATS, "Output format (#{FORMATS.join(', ')})") do |value|
|
|
282
|
-
options[:format] = value
|
|
283
|
-
end
|
|
284
|
-
parser.on('--provider NAME', 'Use a specific provider') do |value|
|
|
285
|
-
options[:provider] = value
|
|
286
|
-
end
|
|
287
|
-
add_model_options(parser, options)
|
|
288
|
-
add_credential_options(parser, options)
|
|
289
|
-
end
|
|
290
379
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
parser.parse!(@arguments)
|
|
297
|
-
nil
|
|
298
|
-
end
|
|
380
|
+
def validate_configuration
|
|
381
|
+
configuration = Prescient.configuration
|
|
382
|
+
unless configuration.provider(configuration.default_provider)
|
|
383
|
+
raise Prescient::Error, "default provider is not configured"
|
|
384
|
+
end
|
|
299
385
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
386
|
+
configuration.providers.each_key do |name|
|
|
387
|
+
configuration.provider(name)
|
|
388
|
+
end
|
|
389
|
+
configuration.tools.each_key do |name|
|
|
390
|
+
configuration.tool(name)
|
|
391
|
+
end
|
|
392
|
+
end
|
|
303
393
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
394
|
+
def parse_options(description, fallback: false, tool: false, limit: false, generate: false,
|
|
395
|
+
documents: false, agent: false)
|
|
396
|
+
options = { format: "text", fallback: fallback }
|
|
397
|
+
parser = OptionParser.new do |parser|
|
|
398
|
+
parser.banner = "Usage: prescient #{@arguments.first || "command"} [options]"
|
|
399
|
+
parser.separator description
|
|
400
|
+
parser.separator ""
|
|
401
|
+
parser.separator "Global options:"
|
|
402
|
+
add_common_options(parser, options)
|
|
403
|
+
add_optional_options(parser, options, fallback:, tool:, limit:, generate:, documents:, agent:)
|
|
404
|
+
parser.on("-h", "--help", "Show command help") do
|
|
405
|
+
@output.puts parser
|
|
406
|
+
throw :help_shown, 0
|
|
407
|
+
end
|
|
408
|
+
end
|
|
314
409
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
options[:embedding_model] = value
|
|
410
|
+
result = catch(:help_shown) { parse_arguments(parser) }
|
|
411
|
+
return result unless result.nil?
|
|
412
|
+
|
|
413
|
+
options[:arguments] = @arguments
|
|
414
|
+
options
|
|
321
415
|
end
|
|
322
|
-
|
|
323
|
-
|
|
416
|
+
|
|
417
|
+
def add_optional_options(parser, options, fallback:, tool:, limit:, generate:, documents:, agent:)
|
|
418
|
+
add_document_options(parser, options) if documents
|
|
419
|
+
parser.on("--no-fallback", "Disable provider fallback") { options[:fallback] = false } if fallback
|
|
420
|
+
add_tool_options(parser, options, tool:, limit:, generate:)
|
|
421
|
+
add_agent_options(parser, options) if agent
|
|
324
422
|
end
|
|
325
|
-
|
|
326
|
-
|
|
423
|
+
|
|
424
|
+
def add_agent_options(parser, options)
|
|
425
|
+
parser.separator ""
|
|
426
|
+
parser.separator "Agent options:"
|
|
427
|
+
options[:tools] = []
|
|
428
|
+
parser.on("--tool NAME", "Allow a configured tool (repeatable)") do |value|
|
|
429
|
+
options[:tools] << value.to_sym
|
|
430
|
+
end
|
|
431
|
+
parser.on("--max-loops COUNT", Integer, "Maximum agent iterations") { |value| options[:max_loops] = value }
|
|
327
432
|
end
|
|
328
|
-
|
|
329
|
-
|
|
433
|
+
|
|
434
|
+
def add_tool_options(parser, options, tool:, limit:, generate:)
|
|
435
|
+
return unless tool || limit || generate
|
|
436
|
+
|
|
437
|
+
parser.separator ""
|
|
438
|
+
parser.separator "Search options:"
|
|
439
|
+
parser.on("--tool NAME", "Use a configured external tool") { |value| options[:tool] = value } if tool
|
|
440
|
+
parser.on("--generate", "Use search results as AI provider context") { options[:generate] = true } if generate
|
|
441
|
+
return unless limit
|
|
442
|
+
|
|
443
|
+
parser.on("--limit COUNT", Integer, "Limit the number of results") { |value| options[:limit] = value }
|
|
330
444
|
end
|
|
331
|
-
|
|
332
|
-
|
|
445
|
+
|
|
446
|
+
def add_common_options(parser, options)
|
|
447
|
+
parser.on("--config PATH", "Load configuration from a YAML file") do |value|
|
|
448
|
+
options[:config] = value
|
|
449
|
+
end
|
|
450
|
+
parser.on("--format FORMAT", FORMATS, "Output format (#{FORMATS.join(", ")})") do |value|
|
|
451
|
+
options[:format] = value
|
|
452
|
+
end
|
|
453
|
+
parser.on("--provider NAME", "Use a specific provider") do |value|
|
|
454
|
+
options[:provider] = value
|
|
455
|
+
end
|
|
456
|
+
add_model_options(parser, options)
|
|
457
|
+
add_credential_options(parser, options)
|
|
333
458
|
end
|
|
334
|
-
|
|
335
|
-
|
|
459
|
+
|
|
460
|
+
# Parse command arguments and return nil when parsing completes.
|
|
461
|
+
#
|
|
462
|
+
# @param parser [OptionParser] Configured command option parser
|
|
463
|
+
# @return [nil]
|
|
464
|
+
def parse_arguments(parser)
|
|
465
|
+
parser.parse!(@arguments)
|
|
466
|
+
nil
|
|
336
467
|
end
|
|
337
|
-
end
|
|
338
468
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
options[:api_key] = value
|
|
469
|
+
def model_options(options)
|
|
470
|
+
options[:model] ? { model: options[:model] } : {}
|
|
342
471
|
end
|
|
343
|
-
|
|
344
|
-
|
|
472
|
+
|
|
473
|
+
def client_for(options)
|
|
474
|
+
validate_override_options(options)
|
|
475
|
+
Prescient.client(
|
|
476
|
+
options[:provider]&.to_sym,
|
|
477
|
+
enable_fallback: options[:fallback],
|
|
478
|
+
provider_options: provider_options(options)
|
|
479
|
+
)
|
|
480
|
+
rescue KeyError => e
|
|
481
|
+
raise UsageError, "environment variable not set: #{e.key}"
|
|
345
482
|
end
|
|
346
|
-
end
|
|
347
483
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
484
|
+
def add_model_options(parser, options)
|
|
485
|
+
parser.on("--model NAME", "Override the configured model") do |value|
|
|
486
|
+
options[:model] = value
|
|
487
|
+
end
|
|
488
|
+
parser.on("--embedding-model NAME", "Override the embedding model") do |value|
|
|
489
|
+
options[:embedding_model] = value
|
|
490
|
+
end
|
|
491
|
+
parser.on("--chat-model NAME", "Override the chat model") do |value|
|
|
492
|
+
options[:chat_model] = value
|
|
493
|
+
end
|
|
494
|
+
parser.on("--system-prompt TEXT", "Override the system prompt") do |value|
|
|
495
|
+
options[:system_prompt] = value
|
|
496
|
+
end
|
|
497
|
+
parser.on("--no-context-template TEXT", "Override the no-context prompt template") do |value|
|
|
498
|
+
options[:no_context_template] = value
|
|
499
|
+
end
|
|
500
|
+
parser.on("--with-context-template TEXT", "Override the with-context prompt template") do |value|
|
|
501
|
+
options[:with_context_template] = value
|
|
502
|
+
end
|
|
503
|
+
parser.on("--prompt-templates-file PATH", "Load prompt templates from a YAML file") do |value|
|
|
504
|
+
options[:prompt_templates_file] = value
|
|
505
|
+
end
|
|
351
506
|
end
|
|
352
|
-
return unless options[:api_key] && options[:api_key_env]
|
|
353
507
|
|
|
354
|
-
|
|
355
|
-
|
|
508
|
+
def add_credential_options(parser, options)
|
|
509
|
+
parser.on("--api-key KEY", "Use an API key for this operation") do |value|
|
|
510
|
+
options[:api_key] = value
|
|
511
|
+
end
|
|
512
|
+
parser.on("--api-key-env NAME", "Read the API key from this environment variable") do |value|
|
|
513
|
+
options[:api_key_env] = value
|
|
514
|
+
end
|
|
515
|
+
end
|
|
356
516
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
prompt_templates: prompt_templates(options),
|
|
363
|
-
}.compact
|
|
364
|
-
end
|
|
517
|
+
def add_document_options(parser, options)
|
|
518
|
+
parser.on("--json-file PATH", "Load JSON documents as generation context") do |value|
|
|
519
|
+
options[:json_file] = value
|
|
520
|
+
end
|
|
521
|
+
end
|
|
365
522
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
permitted_symbols: [],
|
|
372
|
-
aliases: true,
|
|
373
|
-
)
|
|
374
|
-
raise UsageError, 'prompt templates file must contain a mapping' unless data.is_a?(Hash)
|
|
375
|
-
|
|
376
|
-
data.transform_keys(&:to_sym)
|
|
377
|
-
else
|
|
378
|
-
{}
|
|
379
|
-
end
|
|
523
|
+
def validate_override_options(options)
|
|
524
|
+
if options[:model] && (options[:embedding_model] || options[:chat_model])
|
|
525
|
+
raise UsageError, "--model cannot be combined with --embedding-model or --chat-model"
|
|
526
|
+
end
|
|
527
|
+
return unless options[:api_key] && options[:api_key_env]
|
|
380
528
|
|
|
381
|
-
|
|
382
|
-
templates[key] = options[key] if options[key]
|
|
529
|
+
raise UsageError, "--api-key cannot be combined with --api-key-env"
|
|
383
530
|
end
|
|
384
|
-
templates.empty? ? nil : templates
|
|
385
|
-
rescue Errno::ENOENT
|
|
386
|
-
raise UsageError, "prompt templates file not found: #{options[:prompt_templates_file]}"
|
|
387
|
-
rescue Psych::SyntaxError => e
|
|
388
|
-
raise UsageError, "invalid prompt templates YAML: #{e.message}"
|
|
389
|
-
end
|
|
390
|
-
|
|
391
|
-
def api_key_override(options)
|
|
392
|
-
return options[:api_key] if options[:api_key]
|
|
393
|
-
return ENV.fetch(options[:api_key_env]) if options[:api_key_env]
|
|
394
531
|
|
|
395
|
-
|
|
396
|
-
|
|
532
|
+
def provider_options(options)
|
|
533
|
+
{
|
|
534
|
+
api_key: api_key_override(options),
|
|
535
|
+
embedding_model: options[:embedding_model],
|
|
536
|
+
chat_model: options[:chat_model],
|
|
537
|
+
prompt_templates: prompt_templates(options)
|
|
538
|
+
}.compact
|
|
539
|
+
end
|
|
397
540
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
541
|
+
def prompt_templates(options)
|
|
542
|
+
templates = if options[:prompt_templates_file]
|
|
543
|
+
data = YAML.safe_load_file(
|
|
544
|
+
options[:prompt_templates_file],
|
|
545
|
+
permitted_classes: [],
|
|
546
|
+
permitted_symbols: [],
|
|
547
|
+
aliases: true
|
|
548
|
+
)
|
|
549
|
+
raise UsageError, "prompt templates file must contain a mapping" unless data.is_a?(Hash)
|
|
550
|
+
|
|
551
|
+
data.transform_keys(&:to_sym)
|
|
552
|
+
else
|
|
553
|
+
{}
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
%i[system_prompt no_context_template with_context_template].each do |key|
|
|
557
|
+
templates[key] = options[key] if options[key]
|
|
404
558
|
end
|
|
559
|
+
templates.empty? ? nil : templates
|
|
560
|
+
rescue Errno::ENOENT
|
|
561
|
+
raise UsageError, "prompt templates file not found: #{options[:prompt_templates_file]}"
|
|
562
|
+
rescue Psych::SyntaxError => e
|
|
563
|
+
raise UsageError, "invalid prompt templates YAML: #{e.message}"
|
|
405
564
|
end
|
|
406
|
-
end
|
|
407
565
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
566
|
+
def api_key_override(options)
|
|
567
|
+
return options[:api_key] if options[:api_key]
|
|
568
|
+
return ENV.fetch(options[:api_key_env]) if options[:api_key_env]
|
|
411
569
|
|
|
412
|
-
|
|
413
|
-
|
|
570
|
+
nil
|
|
571
|
+
end
|
|
414
572
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
573
|
+
def output_health(results, format)
|
|
574
|
+
if format == "json"
|
|
575
|
+
print_json(results)
|
|
576
|
+
else
|
|
577
|
+
results.each do |name, result|
|
|
578
|
+
@output.puts format("%<name>-12s %<status>s", name: name, status: result[:status] || "unknown")
|
|
579
|
+
end
|
|
580
|
+
end
|
|
581
|
+
end
|
|
418
582
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
Commands:
|
|
424
|
-
providers List configured providers
|
|
425
|
-
health Check provider health
|
|
426
|
-
generate TEXT Generate a text response
|
|
427
|
-
embed TEXT Generate an embedding
|
|
428
|
-
config validate Validate the current configuration
|
|
429
|
-
config example Generate an annotated YAML configuration example
|
|
430
|
-
|
|
431
|
-
Options:
|
|
432
|
-
--config PATH Load configuration from a YAML file
|
|
433
|
-
--provider NAME Select a provider
|
|
434
|
-
--model NAME Override the selected operation's model
|
|
435
|
-
--chat-model NAME Override the chat model
|
|
436
|
-
--embedding-model NAME Override the embedding model
|
|
437
|
-
--system-prompt TEXT Override the system prompt
|
|
438
|
-
--no-context-template TEXT
|
|
439
|
-
Override the no-context prompt template
|
|
440
|
-
--with-context-template TEXT
|
|
441
|
-
Override the with-context prompt template
|
|
442
|
-
--prompt-templates-file PATH
|
|
443
|
-
Load prompt templates from a YAML file
|
|
444
|
-
--api-key KEY Use an API key for the operation
|
|
445
|
-
--api-key-env NAME Read the API key from an environment variable
|
|
446
|
-
--format FORMAT Use text or json output
|
|
447
|
-
HELP
|
|
448
|
-
status
|
|
449
|
-
end
|
|
583
|
+
def read_text(arguments, label)
|
|
584
|
+
return arguments.join(" ") unless arguments.empty?
|
|
585
|
+
return @input.read unless @input.tty?
|
|
450
586
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
filtered_arguments = []
|
|
454
|
-
index = 0
|
|
587
|
+
raise UsageError, "missing #{label}; provide it as an argument or through stdin"
|
|
588
|
+
end
|
|
455
589
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
value = @arguments[index + 1]
|
|
460
|
-
raise UsageError, '--config requires a path' unless value
|
|
590
|
+
def print_json(value)
|
|
591
|
+
@output.puts JSON.generate(value)
|
|
592
|
+
end
|
|
461
593
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
594
|
+
def print_help(status)
|
|
595
|
+
@output.puts <<~HELP
|
|
596
|
+
Usage: prescient COMMAND [options]
|
|
597
|
+
|
|
598
|
+
Commands:
|
|
599
|
+
providers List configured providers
|
|
600
|
+
health Check provider health
|
|
601
|
+
generate TEXT Generate a text response
|
|
602
|
+
embed TEXT Generate an embedding
|
|
603
|
+
search TEXT Search with a configured external tool
|
|
604
|
+
agent TEXT Run a bounded agent task
|
|
605
|
+
config validate Validate the current configuration
|
|
606
|
+
config example Generate an annotated YAML configuration example
|
|
607
|
+
|
|
608
|
+
Global options:
|
|
609
|
+
--config PATH Load configuration from a YAML file
|
|
610
|
+
--provider NAME Select a provider
|
|
611
|
+
--model NAME Override the selected operation's model
|
|
612
|
+
--chat-model NAME Override the chat model
|
|
613
|
+
--embedding-model NAME Override the embedding model
|
|
614
|
+
--system-prompt TEXT Override the system prompt
|
|
615
|
+
--no-context-template TEXT
|
|
616
|
+
Override the no-context prompt template
|
|
617
|
+
--with-context-template TEXT
|
|
618
|
+
Override the with-context prompt template
|
|
619
|
+
--prompt-templates-file PATH
|
|
620
|
+
Load prompt templates from a YAML file
|
|
621
|
+
--api-key KEY Use an API key for the operation
|
|
622
|
+
--api-key-env NAME Read the API key from an environment variable
|
|
623
|
+
--format FORMAT Use text or json output
|
|
624
|
+
--json-file PATH Load JSON documents as generation context
|
|
625
|
+
|
|
626
|
+
Search options:
|
|
627
|
+
--tool NAME Select an external tool for search
|
|
628
|
+
--generate Use search results as AI provider context
|
|
629
|
+
--limit COUNT Limit search results
|
|
630
|
+
|
|
631
|
+
Agent options:
|
|
632
|
+
--tool NAME Allow a configured tool (repeatable)
|
|
633
|
+
--max-loops COUNT Maximum agent iterations
|
|
634
|
+
HELP
|
|
635
|
+
status
|
|
636
|
+
end
|
|
466
637
|
|
|
467
|
-
|
|
468
|
-
|
|
638
|
+
def extract_global_config_path
|
|
639
|
+
config_path = nil
|
|
640
|
+
filtered_arguments = []
|
|
641
|
+
index = 0
|
|
642
|
+
|
|
643
|
+
while index < @arguments.length
|
|
644
|
+
argument = @arguments[index]
|
|
645
|
+
if argument == "--config"
|
|
646
|
+
value = @arguments[index + 1]
|
|
647
|
+
raise UsageError, "--config requires a path" unless value
|
|
648
|
+
|
|
649
|
+
config_path = value
|
|
650
|
+
index += 2
|
|
651
|
+
next
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
if argument.start_with?("--config=")
|
|
655
|
+
config_path = argument.split("=", 2).last
|
|
656
|
+
index += 1
|
|
657
|
+
next
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
filtered_arguments << argument
|
|
469
661
|
index += 1
|
|
470
|
-
next
|
|
471
662
|
end
|
|
472
663
|
|
|
473
|
-
|
|
474
|
-
|
|
664
|
+
@arguments = filtered_arguments
|
|
665
|
+
config_path
|
|
475
666
|
end
|
|
476
|
-
|
|
477
|
-
@arguments = filtered_arguments
|
|
478
|
-
config_path
|
|
479
667
|
end
|
|
668
|
+
# rubocop:enable Metrics/ClassLength
|
|
480
669
|
end
|