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 +4 -4
- data/CHANGELOG.md +12 -0
- data/INTEGRATION_GUIDE.md +1 -1
- data/README.md +87 -3
- data/exe/prescient +7 -0
- data/lib/prescient/cli.rb +287 -0
- data/lib/prescient/client.rb +16 -4
- data/lib/prescient/provider/anthropic.rb +2 -2
- data/lib/prescient/provider/huggingface.rb +7 -6
- data/lib/prescient/provider/ollama.rb +3 -3
- data/lib/prescient/provider/openai.rb +7 -6
- data/lib/prescient/version.rb +1 -1
- data/lib/prescient.rb +1 -0
- data/sig/prescient.rbs +14 -2
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3f12c7d49067ef7917aa1f674f83e4ff8111a5606fa62edcbb7c97b8791dfbee
|
|
4
|
+
data.tar.gz: 4bf6ad2a1a8988ec7eb77cb5a2e278f902ceaa2cfc30d0de3c303c4fa1017168
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
|
6
|
-
[Rails integration guide](INTEGRATION_GUIDE.md)
|
|
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,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
|
data/lib/prescient/client.rb
CHANGED
|
@@ -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 =
|
|
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, **
|
|
38
|
+
def generate_embedding(text, **options)
|
|
39
39
|
handle_errors do
|
|
40
40
|
clean_text_input = clean_text(text)
|
|
41
41
|
|
|
42
|
-
|
|
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[
|
|
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 #{
|
|
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, **
|
|
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:
|
|
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[
|
|
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 #{
|
|
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: {
|
data/lib/prescient/version.rb
CHANGED
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.
|
|
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
|