prescient 0.3.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a54256e23ba55fc2492fae38d687b275dcb2362df73449d3212bd03659150717
4
- data.tar.gz: 73ab805b7a5781a1d03488605ae8d7890feda7344b41b1ac550343484b3b597a
3
+ metadata.gz: 3f12c7d49067ef7917aa1f674f83e4ff8111a5606fa62edcbb7c97b8791dfbee
4
+ data.tar.gz: 4bf6ad2a1a8988ec7eb77cb5a2e278f902ceaa2cfc30d0de3c303c4fa1017168
5
5
  SHA512:
6
- metadata.gz: e918a87b3e09affe9d5a002a3955051e7729ee565f84c42c534c99dc3fc70371fda930e63cc90801da07b4c4f5e48031c41e952088dfc3de24f0f38b579e9567
7
- data.tar.gz: feb707947b7cc0b67d8bff933b4c584a69595b9b5e1dd3f214b4c4aa2d48c0577c996b542c238fbcef337f0f276a1bb995acc9653e8fad32262a6f6fa17caaf4
6
+ metadata.gz: b0fd6efd2a47ce4ffd2766d32706b4b852bfb4e47b62b10ca5e39b50698281048d15be905cfcace617185958194bd1fccde939fd4bac1d83d617cf72707a54c5
7
+ data.tar.gz: dfeb2ac6ed61f430730c589a42853b0d118cb425871784dc481e6ea781fedbfd6fcd3582cdadf89718b01c97a2da234898b8bbd868fc709ce3582c02e0662bee
data/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## [0.4.0] - 2025-08-14
6
+
7
+ ### Added
8
+
9
+ - Added the `prescient` CLI with provider listing, health checks, configuration validation, generation, embeddings, JSON output, stdin input, and exit-status handling.
10
+
11
+ ### Changed
12
+
13
+ - Added CLI model overrides to provider generation and embedding operations.
14
+ - Added secure CLI credential sourcing with `--api-key-env`, alongside direct `--api-key` support for ephemeral automation.
15
+ - Documented all CLI automation overrides, including provider selection, generic and task-specific models, API keys, environment-backed credentials, and JSON output.
16
+
5
17
  ## [0.3.0] - 2025-08-14
6
18
 
7
19
  ### Added
data/INTEGRATION_GUIDE.md CHANGED
@@ -13,7 +13,7 @@ and [examples guide](examples/README.md).
13
13
  # Add to your Gemfile
14
14
  gem 'prescient', path: './prescient_gem' # Local development
15
15
  # OR when published:
16
- # gem 'prescient', '~> 0.3.0'
16
+ # gem 'prescient', '~> 0.4.0'
17
17
  ```
18
18
 
19
19
  ### 2. Replace Existing AI Service
data/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  Prescient is a boring AI provider abstraction for Ruby. Configure your AI providers once, then use the same interface regardless of whether the request is handled by OpenAI, Anthropic, Ollama, or Hugging Face. Prescient handles provider selection, retries, health checks, and fallback.
4
4
 
5
- For focused guidance, see the [examples guide](examples/README.md),
6
- [Rails integration guide](INTEGRATION_GUIDE.md), and
7
- [pgvector guide](VECTOR_SEARCH_GUIDE.md).
5
+ For focused guidance, see the **[examples guide](https://github.com/kanutocd/prescient/tree/main/examples)**,
6
+ **[Rails integration guide](https://github.com/kanutocd/prescient/blob/main/INTEGRATION_GUIDE.md)**, and
7
+ **[pgvector guide](https://github.com/kanutocd/prescient/blob/main/VECTOR_SEARCH_GUIDE.md)**.
8
8
 
9
9
  ## Features
10
10
 
@@ -62,6 +62,90 @@ Or install it yourself as:
62
62
  gem install prescient
63
63
  ```
64
64
 
65
+ ## Command-Line Interface
66
+
67
+ Prescient includes a thin CLI for provider inspection and common operations:
68
+
69
+ ```bash
70
+ prescient providers
71
+ prescient health
72
+ prescient config validate
73
+ prescient generate "Explain Ruby Ractors"
74
+ prescient embed "Ruby is a programming language"
75
+ ```
76
+
77
+ Supported options include:
78
+
79
+ ```text
80
+ --provider NAME Select a provider
81
+ --model NAME Override the selected operation's model
82
+ --chat-model NAME Override the chat model for generation
83
+ --embedding-model NAME Override the embedding model
84
+ --api-key KEY Use an API key for the operation
85
+ --api-key-env NAME Read the API key from an environment variable
86
+ --format FORMAT Select text or json output
87
+ ```
88
+
89
+ Use `--api-key-env` to source credentials from an environment variable. The
90
+ direct `--api-key` option is available for ephemeral automation but may be
91
+ visible in shell history or process listings. Use `--format json` for
92
+ machine-readable output and stdin for shell pipelines:
93
+
94
+ ```bash
95
+ printf '%s' "Explain PostgreSQL logical replication" | \
96
+ prescient generate --provider openai --format json
97
+ ```
98
+
99
+ Example JSON output:
100
+
101
+ ```json
102
+ {
103
+ "response": "PostgreSQL logical replication is a method of replicating data between PostgreSQL databases at a logical level, allowing fine-grained control over which data is replicated and how. Unlike physical replication, which copies the entire database cluster’s data files at the storage level, logical replication works by sending changes to data (such as INSERT, UPDATE, DELETE operations) based on logical changes in the database.\n\n### Key Features of PostgreSQL Logical Replication\n\n1. **Row-Level Replication:** Logical replication replicates data changes at the row level. It streams changes to individual tables rather than the entire database.\n\n2. **Selective Replication:** You can choose specific tables to replicate rather than the whole database. This makes it useful for replicating subsets of data.\n\n3. **Asynchronous Replication:** Changes are sent asynchronously from the publisher (source) to the subscriber (target). This means there may be a slight delay between when changes are made and when they appear on the subscriber.\n\n4. **Supports Heterogeneous Setups:** Logical replication can be used between different major versions of PostgreSQL, allowing upgrades with minimal downtime. It can also be used for replication between different architectures or operating systems.\n\n5. **Bidirectional Replication:** By configuring multiple publishers and subscribers, logical replication can support multi-master setups, although care must be taken to avoid conflicts.\n\n### How Logical Replication Works\n\n- **Publisher:** The database that sends data changes. It defines one or more publications, which specify which tables and changes (inserts, updates, deletes) to replicate.\n \n- **Subscriber:** The database that receives and applies the changes. It subscribes to one or more publications from the publisher.\n\nWhen a change occurs on the publisher's table, the change is captured and sent to the subscriber, where it is applied to the corresponding table.\n\n### Setting Up Logical Replication (Basic Steps)\n\n1. **Enable required settings:** Ensure the PostgreSQL server has `wal_level` set to `logical`, and configure `max_replication_slots` and `max_wal_senders` appropriately.\n\n2. **Create a publication on the publisher:**\n\n ```sql\n CREATE PUBLICATION my_publication FOR TABLE my_table;\n ```\n\n3. **Create a subscription on the subscriber:**\n\n ```sql\n CREATE SUBSCRIPTION my_subscription\n CONNECTION 'host=publisher_host dbname=publisher_db user=replicator password=secret'\n PUBLICATION my_publication;\n ```\n\nOnce set up, changes to `my_table` on the publisher will be replicated to the subscriber.\n\n### Use Cases\n\n- **Selective data replication:** Replicating only certain tables or rows.\n- **Data integration:** Feeding data from multiple sources into a central database.\n- **Upgrading PostgreSQL versions:** Using logical replication to migrate data with minimal downtime.\n- **Multi-datacenter replication:** Replicating data across geographically distributed systems.\n\n---\n\nIn summary, PostgreSQL logical replication is a flexible, table-level replication mechanism that allows selective, asynchronous replication of data changes between PostgreSQL databases, useful for upgrades, distributed architectures, and data integration scenarios.",
104
+ "model": "gpt-4.1-mini",
105
+ "provider": "openai",
106
+ "processing_time": null,
107
+ "metadata": {
108
+ "usage": {
109
+ "prompt_tokens": 38,
110
+ "completion_tokens": 632,
111
+ "total_tokens": 670,
112
+ "prompt_tokens_details": {
113
+ "cached_tokens": 0,
114
+ "audio_tokens": 0
115
+ },
116
+ "completion_tokens_details": {
117
+ "reasoning_tokens": 0,
118
+ "audio_tokens": 0,
119
+ "accepted_prediction_tokens": 0,
120
+ "rejected_prediction_tokens": 0
121
+ }
122
+ },
123
+ "finish_reason": "stop"
124
+ }
125
+ }
126
+ ```
127
+
128
+ For automated model and credential overrides:
129
+
130
+ ```bash
131
+ prescient generate \
132
+ --provider openai \
133
+ --chat-model gpt-4.1-mini \
134
+ --api-key-env OPENAI_API_KEY \
135
+ --format json \
136
+ "Explain PostgreSQL logical replication"
137
+
138
+ prescient embed \
139
+ --provider openai \
140
+ --embedding-model text-embedding-3-small \
141
+ --api-key-env OPENAI_API_KEY \
142
+ "Ruby is a programming language"
143
+ ```
144
+
145
+ The CLI writes results to stdout, diagnostics to stderr, and returns a
146
+ non-zero status for invalid usage, provider errors, or unreachable health
147
+ checks. It uses the same `Prescient::Client` execution path as Ruby callers.
148
+
65
149
  ## Configuration
66
150
 
67
151
  ### Environment Variables
data/exe/prescient ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'prescient'
5
+ require 'prescient/cli'
6
+
7
+ exit Prescient::CLI.run(ARGV)
@@ -0,0 +1,287 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'optparse'
5
+
6
+ # Command-line interface for common Prescient operations.
7
+ class Prescient::CLI
8
+ # Supported output formats.
9
+ # @return [Array<String>] Output format names
10
+ FORMATS = ['text', 'json'].freeze
11
+
12
+ # Raised when command-line arguments are invalid or incomplete.
13
+ class UsageError < StandardError; end
14
+
15
+ # Run the CLI and return a process exit status.
16
+ #
17
+ # @param arguments [Array<String>] Command-line arguments
18
+ # @param input [IO] Input stream used for stdin prompts
19
+ # @param output [IO] Output stream for command results
20
+ # @param errors [IO] Output stream for diagnostics
21
+ # @return [Integer] Process exit status
22
+ def self.run(arguments, input: $stdin, output: $stdout, errors: $stderr)
23
+ new(arguments, input:, output:, errors:).run
24
+ rescue UsageError, OptionParser::ParseError => e
25
+ errors.puts "prescient: #{e.message}"
26
+ 2
27
+ rescue Prescient::Error => e
28
+ errors.puts "prescient: #{e.message}"
29
+ 1
30
+ end
31
+
32
+ # Initialize a CLI runner with injectable streams.
33
+ #
34
+ # @param arguments [Array<String>] Command-line arguments
35
+ # @param input [IO] Input stream used for stdin prompts
36
+ # @param output [IO] Output stream for command results
37
+ # @param errors [IO] Output stream for diagnostics
38
+ def initialize(arguments, input:, output:, errors:)
39
+ @arguments = arguments.dup
40
+ @input = input
41
+ @output = output
42
+ @errors = errors
43
+ end
44
+
45
+ def run
46
+ command = @arguments.shift
47
+ return print_help(2) unless command
48
+
49
+ case command
50
+ when 'providers' then providers
51
+ when 'health' then health
52
+ when 'generate' then generate
53
+ when 'embed' then embed
54
+ when 'config' then config
55
+ when 'help', '--help', '-h' then print_help(0)
56
+ else
57
+ raise UsageError, "unknown command #{command.inspect}; run 'prescient help'"
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def providers
64
+ options = parse_options('List configured providers')
65
+ return options if options.is_a?(Integer)
66
+
67
+ provider_list = Prescient.configuration.providers.map { |name, registration|
68
+ { name: name.to_s, class: registration[:class].name }
69
+ }
70
+
71
+ if options[:format] == 'json'
72
+ print_json(providers: provider_list)
73
+ else
74
+ provider_list.each { |provider| @output.puts "#{provider[:name]}\t#{provider[:class]}" }
75
+ end
76
+ 0
77
+ end
78
+
79
+ def health
80
+ options = parse_options('Check provider health')
81
+ return options if options.is_a?(Integer)
82
+
83
+ names = options[:provider] ? [options[:provider].to_sym] : Prescient.configuration.providers.keys
84
+ raise UsageError, 'no providers are configured' if names.empty?
85
+
86
+ results = names.to_h { |name| [name.to_s, Prescient.health_check(provider: name)] }
87
+ output_health(results, options[:format])
88
+ results.values.all? { |result| result[:reachable] != false } ? 0 : 1
89
+ end
90
+
91
+ def generate
92
+ options = parse_options('Generate a text response', fallback: true)
93
+ return options if options.is_a?(Integer)
94
+
95
+ prompt = read_text(options[:arguments], 'prompt')
96
+ client = client_for(options)
97
+ response = client.generate_response(prompt, **model_options(options))
98
+
99
+ options[:format] == 'json' ? print_json(response) : @output.puts(response[:response])
100
+ 0
101
+ end
102
+
103
+ def embed
104
+ options = parse_options('Generate an embedding', fallback: true)
105
+ return options if options.is_a?(Integer)
106
+
107
+ text = read_text(options[:arguments], 'text')
108
+ client = client_for(options)
109
+ embedding = client.generate_embedding(text, **model_options(options))
110
+
111
+ if options[:format] == 'json'
112
+ print_json(embedding: embedding, dimensions: embedding.length, provider: client.provider_name.to_s)
113
+ else
114
+ @output.puts JSON.generate(embedding)
115
+ end
116
+ 0
117
+ end
118
+
119
+ def config
120
+ subcommand = @arguments.shift
121
+ raise UsageError, "unknown config command #{subcommand.inspect}" unless subcommand == 'validate'
122
+
123
+ options = parse_options('Validate the current configuration')
124
+ return options if options.is_a?(Integer)
125
+
126
+ validate_configuration
127
+ if options[:format] == 'json'
128
+ print_json(valid: true, providers: Prescient.configuration.providers.keys.map(&:to_s))
129
+ else
130
+ @output.puts 'configuration valid'
131
+ end
132
+ 0
133
+ end
134
+
135
+ def validate_configuration
136
+ configuration = Prescient.configuration
137
+ unless configuration.provider(configuration.default_provider)
138
+ raise Prescient::Error, 'default provider is not configured'
139
+ end
140
+
141
+ configuration.providers.each_key { |name| configuration.provider(name) }
142
+ end
143
+
144
+ def parse_options(description, fallback: false)
145
+ options = { format: 'text', fallback: fallback }
146
+ parser = OptionParser.new do |parser|
147
+ parser.banner = "Usage: prescient #{@arguments.first || 'command'} [options]"
148
+ parser.separator description
149
+ add_common_options(parser, options)
150
+ parser.on('--no-fallback', 'Disable provider fallback') { options[:fallback] = false } if fallback
151
+ parser.on('-h', '--help', 'Show command help') do
152
+ @output.puts parser
153
+ throw :help_shown, 0
154
+ end
155
+ end
156
+
157
+ result = catch(:help_shown) { parse_arguments(parser) }
158
+ return result unless result.nil?
159
+
160
+ options[:arguments] = @arguments
161
+ options
162
+ end
163
+
164
+ def add_common_options(parser, options)
165
+ parser.on('--format FORMAT', FORMATS, "Output format (#{FORMATS.join(', ')})") do |value|
166
+ options[:format] = value
167
+ end
168
+ parser.on('--provider NAME', 'Use a specific provider') do |value|
169
+ options[:provider] = value
170
+ end
171
+ add_model_options(parser, options)
172
+ add_credential_options(parser, options)
173
+ end
174
+
175
+ # Parse command arguments and return nil when parsing completes.
176
+ #
177
+ # @param parser [OptionParser] Configured command option parser
178
+ # @return [nil]
179
+ def parse_arguments(parser)
180
+ parser.parse!(@arguments)
181
+ nil
182
+ end
183
+
184
+ def model_options(options)
185
+ options[:model] ? { model: options[:model] } : {}
186
+ end
187
+
188
+ def client_for(options)
189
+ validate_override_options(options)
190
+ Prescient.client(
191
+ options[:provider]&.to_sym,
192
+ enable_fallback: options[:fallback],
193
+ provider_options: provider_options(options),
194
+ )
195
+ rescue KeyError => e
196
+ raise UsageError, "environment variable not set: #{e.key}"
197
+ end
198
+
199
+ def add_model_options(parser, options)
200
+ parser.on('--model NAME', 'Override the configured model') do |value|
201
+ options[:model] = value
202
+ end
203
+ parser.on('--embedding-model NAME', 'Override the embedding model') do |value|
204
+ options[:embedding_model] = value
205
+ end
206
+ parser.on('--chat-model NAME', 'Override the chat model') do |value|
207
+ options[:chat_model] = value
208
+ end
209
+ end
210
+
211
+ def add_credential_options(parser, options)
212
+ parser.on('--api-key KEY', 'Use an API key for this operation') do |value|
213
+ options[:api_key] = value
214
+ end
215
+ parser.on('--api-key-env NAME', 'Read the API key from this environment variable') do |value|
216
+ options[:api_key_env] = value
217
+ end
218
+ end
219
+
220
+ def validate_override_options(options)
221
+ if options[:model] && (options[:embedding_model] || options[:chat_model])
222
+ raise UsageError, '--model cannot be combined with --embedding-model or --chat-model'
223
+ end
224
+ return unless options[:api_key] && options[:api_key_env]
225
+
226
+ raise UsageError, '--api-key cannot be combined with --api-key-env'
227
+ end
228
+
229
+ def provider_options(options)
230
+ {
231
+ api_key: api_key_override(options),
232
+ embedding_model: options[:embedding_model],
233
+ chat_model: options[:chat_model],
234
+ }.compact
235
+ end
236
+
237
+ def api_key_override(options)
238
+ return options[:api_key] if options[:api_key]
239
+ return ENV.fetch(options[:api_key_env]) if options[:api_key_env]
240
+
241
+ nil
242
+ end
243
+
244
+ def output_health(results, format)
245
+ if format == 'json'
246
+ print_json(results)
247
+ else
248
+ results.each do |name, result|
249
+ @output.puts '%<name>-12s %<status>s' % { name: name, status: result[:status] || 'unknown' }
250
+ end
251
+ end
252
+ end
253
+
254
+ def read_text(arguments, label)
255
+ return arguments.join(' ') unless arguments.empty?
256
+ return @input.read unless @input.tty?
257
+
258
+ raise UsageError, "missing #{label}; provide it as an argument or through stdin"
259
+ end
260
+
261
+ def print_json(value)
262
+ @output.puts JSON.generate(value)
263
+ end
264
+
265
+ def print_help(status)
266
+ @output.puts <<~HELP
267
+ Usage: prescient COMMAND [options]
268
+
269
+ Commands:
270
+ providers List configured providers
271
+ health Check provider health
272
+ generate TEXT Generate a text response
273
+ embed TEXT Generate an embedding
274
+ config validate Validate the current configuration
275
+
276
+ Options:
277
+ --provider NAME Select a provider
278
+ --model NAME Override the selected operation's model
279
+ --chat-model NAME Override the chat model
280
+ --embedding-model NAME Override the embedding model
281
+ --api-key KEY Use an API key for the operation
282
+ --api-key-env NAME Read the API key from an environment variable
283
+ --format FORMAT Use text or json output
284
+ HELP
285
+ status
286
+ end
287
+ end
@@ -27,10 +27,11 @@ module Prescient
27
27
  #
28
28
  # @param provider_name [Symbol, nil] Name of provider to use, or nil for default
29
29
  # @param enable_fallback [Boolean] Whether to enable automatic fallback to other providers
30
+ # @param provider_options [Hash] Temporary options for the selected provider
30
31
  # @raise [Prescient::Error] If the specified provider is not configured
31
- def initialize(provider_name = nil, enable_fallback: true)
32
+ def initialize(provider_name = nil, enable_fallback: true, provider_options: {})
32
33
  @provider_name = provider_name || Prescient.configuration.default_provider
33
- @provider = Prescient.configuration.provider(@provider_name)
34
+ @provider = provider_with_options(@provider_name, provider_options)
34
35
  @enable_fallback = enable_fallback
35
36
 
36
37
  raise Prescient::Error, "Provider not configured: #{@provider_name}" unless @provider
@@ -131,6 +132,16 @@ module Prescient
131
132
  end
132
133
  end
133
134
 
135
+ def provider_with_options(provider_name, provider_options)
136
+ return Prescient.configuration.provider(provider_name) if provider_options.empty?
137
+
138
+ registration = Prescient.configuration.providers[provider_name.to_sym]
139
+ return unless registration
140
+
141
+ registration_options = registration[:options] # : Hash[Symbol, untyped]
142
+ registration[:class].new(**registration_options, **provider_options)
143
+ end
144
+
134
145
  def with_error_handling
135
146
  retries = 0
136
147
  begin
@@ -208,9 +219,10 @@ module Prescient
208
219
  #
209
220
  # @param provider_name [Symbol, nil] Provider to use, or the configured default
210
221
  # @param enable_fallback [Boolean] Whether provider fallback is enabled
222
+ # @param provider_options [Hash] Temporary options for the selected provider
211
223
  # @return [Client] A configured client instance
212
- def self.client(provider_name = nil, enable_fallback: true)
213
- Client.new(provider_name, enable_fallback: enable_fallback)
224
+ def self.client(provider_name = nil, enable_fallback: true, provider_options: {})
225
+ Client.new(provider_name, enable_fallback: enable_fallback, provider_options: provider_options)
214
226
  end
215
227
 
216
228
  # Generate an embedding through a configured provider.
@@ -37,7 +37,7 @@ class Prescient::Provider::Anthropic < Prescient::Base
37
37
  'anthropic-version' => '2023-06-01',
38
38
  },
39
39
  body: {
40
- model: @options[:model],
40
+ model: options[:model] || @options[:model],
41
41
  max_tokens: options[:max_tokens] || 2000,
42
42
  temperature: options[:temperature] || 0.7,
43
43
  messages: [
@@ -55,7 +55,7 @@ class Prescient::Provider::Anthropic < Prescient::Base
55
55
 
56
56
  {
57
57
  response: content.strip,
58
- model: @options[:model],
58
+ model: options[:model] || @options[:model],
59
59
  provider: 'anthropic',
60
60
  processing_time: nil,
61
61
  metadata: {
@@ -35,11 +35,12 @@ class Prescient::Provider::HuggingFace < Prescient::Base
35
35
  # Generate an embedding through Hugging Face feature extraction.
36
36
  # @param text [String] Text to embed
37
37
  # @return [Array<Float>] Embedding vector
38
- def generate_embedding(text, **_options)
38
+ def generate_embedding(text, **options)
39
39
  handle_errors do
40
40
  clean_text_input = clean_text(text)
41
41
 
42
- response = self.class.post(FEATURE_EXTRACTION_PATH % { model: @options[:embedding_model] },
42
+ embedding_model = options[:model] || @options[:embedding_model]
43
+ response = self.class.post(FEATURE_EXTRACTION_PATH % { model: embedding_model },
43
44
  headers: {
44
45
  'Content-Type' => 'application/json',
45
46
  'Authorization' => "Bearer #{@options[:api_key]}",
@@ -54,10 +55,10 @@ class Prescient::Provider::HuggingFace < Prescient::Base
54
55
 
55
56
  raise Prescient::InvalidResponseError, 'No embedding returned' unless embedding_data.is_a?(Array)
56
57
 
57
- expected_dimensions = EMBEDDING_DIMENSIONS[@options[:embedding_model]] || @options[:embedding_dimensions]
58
+ expected_dimensions = EMBEDDING_DIMENSIONS[embedding_model] || @options[:embedding_dimensions]
58
59
  unless expected_dimensions
59
60
  raise Prescient::Error,
60
- "Embedding dimensions are required for model #{@options[:embedding_model]}"
61
+ "Embedding dimensions are required for model #{embedding_model}"
61
62
  end
62
63
 
63
64
  validate_embedding_dimensions(embedding_data, expected_dimensions)
@@ -78,7 +79,7 @@ class Prescient::Provider::HuggingFace < Prescient::Base
78
79
  'Authorization' => "Bearer #{@options[:api_key]}",
79
80
  },
80
81
  body: {
81
- model: @options[:chat_model],
82
+ model: options[:model] || @options[:chat_model],
82
83
  messages: [{ role: 'user', content: formatted_prompt }],
83
84
  max_tokens: options[:max_tokens] || 2000,
84
85
  temperature: options[:temperature] || 0.7,
@@ -93,7 +94,7 @@ class Prescient::Provider::HuggingFace < Prescient::Base
93
94
 
94
95
  {
95
96
  response: generated_text.strip,
96
- model: @options[:chat_model],
97
+ model: options[:model] || @options[:chat_model],
97
98
  provider: 'huggingface',
98
99
  processing_time: nil,
99
100
  metadata: {
@@ -21,7 +21,7 @@ class Prescient::Provider::Ollama < Prescient::Base
21
21
  root_key: 'embeddings',
22
22
  headers: { 'Content-Type' => 'application/json' },
23
23
  body: {
24
- model: @options[:embedding_model],
24
+ model: options[:model] || @options[:embedding_model],
25
25
  input: clean_text(text),
26
26
  }.to_json)
27
27
 
@@ -55,7 +55,7 @@ class Prescient::Provider::Ollama < Prescient::Base
55
55
 
56
56
  {
57
57
  response: generated_text.strip,
58
- model: @options[:chat_model],
58
+ model: options[:model] || @options[:chat_model],
59
59
  provider: 'ollama',
60
60
  processing_time: response.parsed_response['total_duration']&./(1_000_000_000.0),
61
61
  metadata: {
@@ -158,7 +158,7 @@ class Prescient::Provider::Ollama < Prescient::Base
158
158
  { root_key: 'response',
159
159
  headers: { 'Content-Type' => 'application/json' },
160
160
  body: {
161
- model: @options[:chat_model],
161
+ model: options[:model] || @options[:chat_model],
162
162
  prompt: formatted_prompt,
163
163
  stream: false,
164
164
  options: {
@@ -23,17 +23,18 @@ class Prescient::Provider::OpenAI < Prescient::Base
23
23
  # Generate an embedding through the OpenAI embeddings API.
24
24
  # @param text [String] Text to embed
25
25
  # @return [Array<Float>] Embedding vector
26
- def generate_embedding(text, **_options)
26
+ def generate_embedding(text, **options)
27
27
  handle_errors do
28
28
  clean_text_input = clean_text(text)
29
29
 
30
+ embedding_model = options[:model] || @options[:embedding_model]
30
31
  response = self.class.post('/v1/embeddings',
31
32
  headers: {
32
33
  'Content-Type' => 'application/json',
33
34
  'Authorization' => "Bearer #{@options[:api_key]}",
34
35
  },
35
36
  body: {
36
- model: @options[:embedding_model],
37
+ model: embedding_model,
37
38
  input: clean_text_input,
38
39
  encoding_format: 'float',
39
40
  }.to_json)
@@ -43,10 +44,10 @@ class Prescient::Provider::OpenAI < Prescient::Base
43
44
  embedding_data = response.parsed_response.dig('data', 0, 'embedding')
44
45
  raise Prescient::InvalidResponseError, 'No embedding returned' unless embedding_data
45
46
 
46
- expected_dimensions = EMBEDDING_DIMENSIONS[@options[:embedding_model]] || @options[:embedding_dimensions]
47
+ expected_dimensions = EMBEDDING_DIMENSIONS[embedding_model] || @options[:embedding_dimensions]
47
48
  unless expected_dimensions
48
49
  raise Prescient::Error,
49
- "Embedding dimensions are required for model #{@options[:embedding_model]}"
50
+ "Embedding dimensions are required for model #{embedding_model}"
50
51
  end
51
52
 
52
53
  validate_embedding_dimensions(embedding_data, expected_dimensions)
@@ -67,7 +68,7 @@ class Prescient::Provider::OpenAI < Prescient::Base
67
68
  'Authorization' => "Bearer #{@options[:api_key]}",
68
69
  },
69
70
  body: {
70
- model: @options[:chat_model],
71
+ model: options[:model] || @options[:chat_model],
71
72
  messages: [
72
73
  {
73
74
  role: 'user',
@@ -86,7 +87,7 @@ class Prescient::Provider::OpenAI < Prescient::Base
86
87
 
87
88
  {
88
89
  response: content.strip,
89
- model: @options[:chat_model],
90
+ model: options[:model] || @options[:chat_model],
90
91
  provider: 'openai',
91
92
  processing_time: nil,
92
93
  metadata: {
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Prescient
4
4
  # Current Prescient gem version.
5
- VERSION = '0.3.0'
5
+ VERSION = '0.4.0'
6
6
  end
data/lib/prescient.rb CHANGED
@@ -9,6 +9,7 @@ require_relative 'prescient/provider/anthropic'
9
9
  require_relative 'prescient/provider/openai'
10
10
  require_relative 'prescient/provider/huggingface'
11
11
  require_relative 'prescient/client'
12
+ require_relative 'prescient/cli'
12
13
 
13
14
  # Main Prescient module for AI provider abstraction
14
15
  #
data/sig/prescient.rbs CHANGED
@@ -193,7 +193,7 @@ module Prescient
193
193
  attr_reader provider_name: Symbol
194
194
  attr_reader provider: untyped
195
195
 
196
- def initialize: (?Symbol, ?enable_fallback: bool) -> void
196
+ def initialize: (?Symbol, ?enable_fallback: bool, ?provider_options: Hash[Symbol, untyped]) -> void
197
197
  def generate_embedding: (String, **untyped) -> Array[Float]
198
198
  def generate_response: (String, ?Array[untyped], **untyped) -> Hash[Symbol, untyped]
199
199
  def health_check: () -> Hash[Symbol, untyped]
@@ -203,6 +203,7 @@ module Prescient
203
203
  private
204
204
 
205
205
  def sanitize_options: (Hash[untyped, untyped]) -> Hash[untyped, untyped]
206
+ def provider_with_options: (Symbol, Hash[Symbol, untyped]) -> untyped
206
207
  def with_error_handling: () { () -> untyped } -> untyped
207
208
  def with_fallback_handling: (Symbol, *untyped, **untyped) -> untyped
208
209
  def provider_for: (Symbol, Integer) -> untyped
@@ -210,10 +211,21 @@ module Prescient
210
211
  def fallback_eligible?: (Error) -> bool
211
212
  end
212
213
 
214
+ class CLI
215
+ FORMATS: Array[String]
216
+
217
+ class UsageError < StandardError
218
+ end
219
+
220
+ def self.run: (Array[String], ?input: IO, ?output: IO, ?errors: IO) -> Integer
221
+ def initialize: (Array[String], input: IO, output: IO, errors: IO) -> void
222
+ def run: () -> Integer
223
+ end
224
+
213
225
  def self.configure: () { (Configuration) -> void } -> void
214
226
  def self.configuration: () -> Configuration
215
227
  def self.reset_configuration!: () -> Configuration
216
- def self.client: (?Symbol, ?enable_fallback: bool) -> Client
228
+ def self.client: (?Symbol, ?enable_fallback: bool, ?provider_options: Hash[Symbol, untyped]) -> Client
217
229
  def self.generate_embedding: (String, ?provider: Symbol, ?enable_fallback: bool, **untyped) -> Array[Float]
218
230
  def self.generate_response: (String, ?Array[untyped], ?provider: Symbol, ?enable_fallback: bool, **untyped) -> Hash[Symbol, untyped]
219
231
  def self.health_check: (?provider: Symbol) -> Hash[Symbol, untyped]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prescient
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ken C. Demanawa
@@ -28,7 +28,8 @@ description: "Prescient provides a consistent interface for AI providers includi
28
28
  health checks, and automatic fallback.\n"
29
29
  email:
30
30
  - kenneth.c.demanawa@gmail.com
31
- executables: []
31
+ executables:
32
+ - prescient
32
33
  extensions: []
33
34
  extra_rdoc_files: []
34
35
  files:
@@ -55,8 +56,10 @@ files:
55
56
  - examples/custom_contexts.rb
56
57
  - examples/custom_prompts.rb
57
58
  - examples/vector_search.rb
59
+ - exe/prescient
58
60
  - lib/prescient.rb
59
61
  - lib/prescient/base.rb
62
+ - lib/prescient/cli.rb
60
63
  - lib/prescient/client.rb
61
64
  - lib/prescient/errors.rb
62
65
  - lib/prescient/pgvector.rb