prescient 0.2.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/.rubocop.yml +0 -2
- data/.yardopts +3 -6
- data/CHANGELOG.md +53 -4
- data/INTEGRATION_GUIDE.md +27 -29
- data/LICENSE.txt +1 -1
- data/README.md +217 -74
- data/Rakefile +82 -7
- data/Steepfile +17 -0
- data/VECTOR_SEARCH_GUIDE.md +32 -8
- data/docker-compose.yml +3 -5
- data/examples/README.md +45 -0
- data/examples/basic_usage.rb +2 -2
- data/examples/custom_contexts.rb +6 -6
- data/examples/custom_prompts.rb +5 -5
- data/examples/vector_search.rb +2 -2
- data/exe/prescient +7 -0
- data/lib/prescient/base.rb +81 -26
- data/lib/prescient/cli.rb +287 -0
- data/lib/prescient/client.rb +85 -39
- data/lib/prescient/errors.rb +51 -0
- data/lib/prescient/pgvector.rb +194 -0
- data/lib/prescient/provider/anthropic.rb +57 -56
- data/lib/prescient/provider/huggingface.rb +79 -78
- data/lib/prescient/provider/ollama.rb +48 -37
- data/lib/prescient/provider/openai.rb +43 -35
- data/lib/prescient/version.rb +2 -1
- data/lib/prescient.rb +86 -63
- data/scripts/setup-ollama-models.sh +2 -2
- data/sig/prescient.rbs +233 -1
- metadata +25 -218
- data/CHANGELOG.pdf +0 -0
- data/prescient.gemspec +0 -53
data/VECTOR_SEARCH_GUIDE.md
CHANGED
|
@@ -2,23 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
This guide provides a comprehensive overview of using Prescient with PostgreSQL's pgvector extension for semantic search and similarity matching.
|
|
4
4
|
|
|
5
|
+
The runnable companion is [`examples/vector_search.rb`](examples/vector_search.rb);
|
|
6
|
+
see the [examples guide](examples/README.md) for setup and the [main README](README.md)
|
|
7
|
+
for the provider API.
|
|
8
|
+
|
|
5
9
|
## Quick Start
|
|
6
10
|
|
|
11
|
+
For a reusable embedding store, use the library boundary rather than copying
|
|
12
|
+
the example's application-specific SQL:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
require 'prescient'
|
|
16
|
+
require 'pg'
|
|
17
|
+
|
|
18
|
+
store = Prescient::Pgvector::Store.new(
|
|
19
|
+
connection: PG.connect(dbname: 'my_app'),
|
|
20
|
+
dimensions: 1536,
|
|
21
|
+
)
|
|
22
|
+
store.install!
|
|
23
|
+
store.create_index!(metric: :cosine)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`Store` owns only its `prescient_embeddings` table. It does not manage document
|
|
27
|
+
or chunk tables, connections, migrations outside that table, or the `pg` gem.
|
|
28
|
+
Use `#upsert` and `#search` with embeddings of exactly the configured dimension.
|
|
29
|
+
|
|
7
30
|
### 1. Start Services
|
|
8
31
|
|
|
9
32
|
```bash
|
|
10
33
|
# Start PostgreSQL with pgvector and Ollama
|
|
11
|
-
docker
|
|
34
|
+
docker compose up -d postgres ollama
|
|
12
35
|
|
|
13
36
|
# Wait for services to be ready
|
|
14
|
-
docker
|
|
37
|
+
docker compose logs -f postgres ollama
|
|
15
38
|
```
|
|
16
39
|
|
|
17
40
|
### 2. Initialize Models
|
|
18
41
|
|
|
19
42
|
```bash
|
|
20
43
|
# Pull required Ollama models
|
|
21
|
-
docker
|
|
44
|
+
docker compose run --rm ollama-init
|
|
22
45
|
|
|
23
46
|
# Or manually:
|
|
24
47
|
./scripts/setup-ollama-models.sh
|
|
@@ -117,7 +140,7 @@ vector_str = "[#{embedding.join(',')}]"
|
|
|
117
140
|
|
|
118
141
|
db.exec_params(
|
|
119
142
|
"INSERT INTO document_embeddings (document_id, embedding_provider, embedding_model, embedding_dimensions, embedding, embedding_text) VALUES ($1, $2, $3, $4, $5, $6)",
|
|
120
|
-
[document_id, 'ollama', 'nomic-embed-text',
|
|
143
|
+
[document_id, 'ollama', 'nomic-embed-text', embedding.length, vector_str, content]
|
|
121
144
|
)
|
|
122
145
|
```
|
|
123
146
|
|
|
@@ -211,7 +234,7 @@ chunks.each do |chunk|
|
|
|
211
234
|
# Store chunk embedding
|
|
212
235
|
db.exec_params(
|
|
213
236
|
"INSERT INTO chunk_embeddings (chunk_id, document_id, embedding_provider, embedding_model, embedding_dimensions, embedding) VALUES ($1, $2, $3, $4, $5, $6)",
|
|
214
|
-
[chunk_id, document_id, 'ollama', 'nomic-embed-text',
|
|
237
|
+
[chunk_id, document_id, 'ollama', 'nomic-embed-text', chunk_embedding.length, chunk_vector]
|
|
215
238
|
)
|
|
216
239
|
end
|
|
217
240
|
```
|
|
@@ -280,7 +303,7 @@ db.transaction do
|
|
|
280
303
|
vector_str = "[#{embedding.join(',')}]"
|
|
281
304
|
db.exec_params(
|
|
282
305
|
"INSERT INTO document_embeddings (...) VALUES (...)",
|
|
283
|
-
[documents[index].id, 'ollama', 'nomic-embed-text',
|
|
306
|
+
[documents[index].id, 'ollama', 'nomic-embed-text', embedding.length, vector_str, texts[index]]
|
|
284
307
|
)
|
|
285
308
|
end
|
|
286
309
|
end
|
|
@@ -322,14 +345,15 @@ Store embeddings from multiple providers for comparison:
|
|
|
322
345
|
|
|
323
346
|
```ruby
|
|
324
347
|
providers = [
|
|
325
|
-
{ client: Prescient.client(:ollama), name: 'ollama', model: 'nomic-embed-text'
|
|
326
|
-
{ client: Prescient.client(:openai), name: 'openai', model: 'text-embedding-3-small'
|
|
348
|
+
{ client: Prescient.client(:ollama), name: 'ollama', model: 'nomic-embed-text' },
|
|
349
|
+
{ client: Prescient.client(:openai), name: 'openai', model: 'text-embedding-3-small' }
|
|
327
350
|
]
|
|
328
351
|
|
|
329
352
|
providers.each do |provider|
|
|
330
353
|
next unless provider[:client].available?
|
|
331
354
|
|
|
332
355
|
embedding = provider[:client].generate_embedding(text)
|
|
356
|
+
provider[:dims] = embedding.length
|
|
333
357
|
vector_str = "[#{embedding.join(',')}]"
|
|
334
358
|
|
|
335
359
|
db.exec_params(
|
data/docker-compose.yml
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# Docker Compose configuration for running Ollama with Prescient gem
|
|
2
2
|
# This provides a local AI environment for development and testing
|
|
3
3
|
|
|
4
|
-
version: '3.8'
|
|
5
|
-
|
|
6
4
|
services:
|
|
7
5
|
ollama:
|
|
8
6
|
image: ollama/ollama:latest
|
|
@@ -65,7 +63,7 @@ services:
|
|
|
65
63
|
# Pull chat model
|
|
66
64
|
curl -X POST http://ollama:11434/api/pull \
|
|
67
65
|
-H "Content-Type: application/json" \
|
|
68
|
-
-d "{\"name\": \"llama3.
|
|
66
|
+
-d "{\"name\": \"llama3.2:3b\"}"
|
|
69
67
|
|
|
70
68
|
echo "Models pulled successfully!"
|
|
71
69
|
'
|
|
@@ -122,7 +120,7 @@ services:
|
|
|
122
120
|
# Ollama configuration
|
|
123
121
|
- OLLAMA_URL=http://ollama:11434
|
|
124
122
|
- OLLAMA_EMBEDDING_MODEL=nomic-embed-text
|
|
125
|
-
- OLLAMA_CHAT_MODEL=llama3.
|
|
123
|
+
- OLLAMA_CHAT_MODEL=llama3.2:3b
|
|
126
124
|
|
|
127
125
|
# Optional: Other AI provider configurations
|
|
128
126
|
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
|
@@ -150,4 +148,4 @@ volumes:
|
|
|
150
148
|
|
|
151
149
|
networks:
|
|
152
150
|
default:
|
|
153
|
-
name: prescient-network
|
|
151
|
+
name: prescient-network
|
data/examples/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Prescient examples
|
|
2
|
+
|
|
3
|
+
These scripts demonstrate the supported public API. They use the local
|
|
4
|
+
library checkout, so run them from the repository root after installing the
|
|
5
|
+
development dependencies:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bundle install
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Examples
|
|
12
|
+
|
|
13
|
+
- `basic_usage.rb` — default-provider generation, embeddings, health checks,
|
|
14
|
+
provider selection, and custom configuration.
|
|
15
|
+
- `custom_prompts.rb` — system prompts and no-context/with-context templates.
|
|
16
|
+
- `custom_contexts.rb` — explicit context types, field matching, formatting,
|
|
17
|
+
and embedding field selection.
|
|
18
|
+
- `vector_search.rb` — PostgreSQL/pgvector storage and similarity search.
|
|
19
|
+
|
|
20
|
+
The first three examples use Ollama by default. Start Ollama and pull the
|
|
21
|
+
current local models before running them:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
docker compose up -d ollama
|
|
25
|
+
docker compose run --rm ollama-init
|
|
26
|
+
bundle exec ruby examples/basic_usage.rb
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The vector-search example additionally requires PostgreSQL with pgvector:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
docker compose up -d postgres ollama
|
|
33
|
+
docker compose run --rm ollama-init
|
|
34
|
+
DB_HOST=localhost bundle exec ruby examples/vector_search.rb
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Cloud-provider examples require the corresponding credentials and provider
|
|
38
|
+
configuration. The scripts are demonstrations rather than isolated test
|
|
39
|
+
fixtures; they may make real provider requests when the configured service is
|
|
40
|
+
available.
|
|
41
|
+
|
|
42
|
+
See the [main README](../README.md) for configuration, fallback behavior,
|
|
43
|
+
prompt templates, context exclusions, embeddings, and the public API. Rails
|
|
44
|
+
applications can also use the [integration guide](../INTEGRATION_GUIDE.md),
|
|
45
|
+
and PostgreSQL users should read the [pgvector guide](../VECTOR_SEARCH_GUIDE.md).
|
data/examples/basic_usage.rb
CHANGED
|
@@ -110,7 +110,7 @@ Prescient.configure do |config|
|
|
|
110
110
|
config.add_provider(:custom_ollama, Prescient::Provider::Ollama,
|
|
111
111
|
url: ENV.fetch('OLLAMA_URL', 'http://localhost:11434'),
|
|
112
112
|
embedding_model: 'nomic-embed-text',
|
|
113
|
-
chat_model: 'llama3.
|
|
113
|
+
chat_model: 'llama3.2:3b',
|
|
114
114
|
timeout: 60
|
|
115
115
|
)
|
|
116
116
|
end
|
|
@@ -120,4 +120,4 @@ puts " Timeout: #{Prescient.configuration.timeout}s"
|
|
|
120
120
|
puts " Retry attempts: #{Prescient.configuration.retry_attempts}"
|
|
121
121
|
puts " Providers: #{Prescient.configuration.providers.keys.join(', ')}"
|
|
122
122
|
|
|
123
|
-
puts "\n🎉 Examples completed!"
|
|
123
|
+
puts "\n🎉 Examples completed!"
|
data/examples/custom_contexts.rb
CHANGED
|
@@ -16,7 +16,7 @@ Prescient.configure do |config|
|
|
|
16
16
|
config.add_provider(:ecommerce, Prescient::Provider::Ollama,
|
|
17
17
|
url: ENV.fetch('OLLAMA_URL', 'http://localhost:11434'),
|
|
18
18
|
embedding_model: 'nomic-embed-text',
|
|
19
|
-
chat_model: 'llama3.
|
|
19
|
+
chat_model: 'llama3.2:3b',
|
|
20
20
|
# Define your own context types - no hardcoded assumptions!
|
|
21
21
|
context_configs: {
|
|
22
22
|
'product' => {
|
|
@@ -88,7 +88,7 @@ Prescient.configure do |config|
|
|
|
88
88
|
config.add_provider(:healthcare, Prescient::Provider::Ollama,
|
|
89
89
|
url: ENV.fetch('OLLAMA_URL', 'http://localhost:11434'),
|
|
90
90
|
embedding_model: 'nomic-embed-text',
|
|
91
|
-
chat_model: 'llama3.
|
|
91
|
+
chat_model: 'llama3.2:3b',
|
|
92
92
|
context_configs: {
|
|
93
93
|
'patient' => {
|
|
94
94
|
fields: %w[name age gender medical_conditions medications],
|
|
@@ -158,7 +158,7 @@ Prescient.configure do |config|
|
|
|
158
158
|
config.add_provider(:project_mgmt, Prescient::Provider::Ollama,
|
|
159
159
|
url: ENV.fetch('OLLAMA_URL', 'http://localhost:11434'),
|
|
160
160
|
embedding_model: 'nomic-embed-text',
|
|
161
|
-
chat_model: 'llama3.
|
|
161
|
+
chat_model: 'llama3.2:3b',
|
|
162
162
|
context_configs: {
|
|
163
163
|
'issue' => {
|
|
164
164
|
fields: %w[title description status priority assignee labels created_date],
|
|
@@ -237,7 +237,7 @@ begin
|
|
|
237
237
|
config.add_provider(:embedding_demo, Prescient::Provider::Ollama,
|
|
238
238
|
url: ENV.fetch('OLLAMA_URL', 'http://localhost:11434'),
|
|
239
239
|
embedding_model: 'nomic-embed-text',
|
|
240
|
-
chat_model: 'llama3.
|
|
240
|
+
chat_model: 'llama3.2:3b',
|
|
241
241
|
context_configs: {
|
|
242
242
|
'blog_post' => {
|
|
243
243
|
fields: %w[title content author tags category publish_date],
|
|
@@ -288,7 +288,7 @@ Prescient.configure do |config|
|
|
|
288
288
|
config.add_provider(:no_config, Prescient::Provider::Ollama,
|
|
289
289
|
url: ENV.fetch('OLLAMA_URL', 'http://localhost:11434'),
|
|
290
290
|
embedding_model: 'nomic-embed-text',
|
|
291
|
-
chat_model: 'llama3.
|
|
291
|
+
chat_model: 'llama3.2:3b'
|
|
292
292
|
# No context_configs defined - uses pure default behavior
|
|
293
293
|
)
|
|
294
294
|
end
|
|
@@ -352,4 +352,4 @@ puts "\n🎯 Best Practices:"
|
|
|
352
352
|
puts " - Define context_configs for your specific domain"
|
|
353
353
|
puts " - Use explicit 'type' field when context detection isn't reliable"
|
|
354
354
|
puts " - Exclude sensitive/metadata fields from embedding_fields"
|
|
355
|
-
puts " - Test with and without context configs to see the difference"
|
|
355
|
+
puts " - Test with and without context configs to see the difference"
|
data/examples/custom_prompts.rb
CHANGED
|
@@ -15,7 +15,7 @@ Prescient.configure do |config|
|
|
|
15
15
|
config.add_provider(:customer_service, Prescient::Provider::Ollama,
|
|
16
16
|
url: ENV.fetch('OLLAMA_URL', 'http://localhost:11434'),
|
|
17
17
|
embedding_model: 'nomic-embed-text',
|
|
18
|
-
chat_model: 'llama3.
|
|
18
|
+
chat_model: 'llama3.2:3b',
|
|
19
19
|
prompt_templates: {
|
|
20
20
|
system_prompt: 'You are a friendly customer service representative. Be helpful, empathetic, and professional.',
|
|
21
21
|
no_context_template: <<~TEMPLATE.strip,
|
|
@@ -73,7 +73,7 @@ Prescient.configure do |config|
|
|
|
73
73
|
config.add_provider(:tech_docs, Prescient::Provider::Ollama,
|
|
74
74
|
url: ENV.fetch('OLLAMA_URL', 'http://localhost:11434'),
|
|
75
75
|
embedding_model: 'nomic-embed-text',
|
|
76
|
-
chat_model: 'llama3.
|
|
76
|
+
chat_model: 'llama3.2:3b',
|
|
77
77
|
prompt_templates: {
|
|
78
78
|
system_prompt: 'You are a technical documentation assistant. Provide clear, accurate, and detailed technical explanations with code examples when relevant.',
|
|
79
79
|
no_context_template: <<~TEMPLATE.strip,
|
|
@@ -125,7 +125,7 @@ Prescient.configure do |config|
|
|
|
125
125
|
config.add_provider(:creative, Prescient::Provider::Ollama,
|
|
126
126
|
url: ENV.fetch('OLLAMA_URL', 'http://localhost:11434'),
|
|
127
127
|
embedding_model: 'nomic-embed-text',
|
|
128
|
-
chat_model: 'llama3.
|
|
128
|
+
chat_model: 'llama3.2:3b',
|
|
129
129
|
prompt_templates: {
|
|
130
130
|
system_prompt: 'You are a creative writing assistant. Help with storytelling, character development, and creative inspiration. Be imaginative and encouraging.',
|
|
131
131
|
no_context_template: <<~TEMPLATE.strip,
|
|
@@ -182,7 +182,7 @@ Prescient.configure do |config|
|
|
|
182
182
|
config.add_provider(:custom_default, Prescient::Provider::Ollama,
|
|
183
183
|
url: ENV.fetch('OLLAMA_URL', 'http://localhost:11434'),
|
|
184
184
|
embedding_model: 'nomic-embed-text',
|
|
185
|
-
chat_model: 'llama3.
|
|
185
|
+
chat_model: 'llama3.2:3b',
|
|
186
186
|
prompt_templates: {
|
|
187
187
|
# Only override the system prompt, keep default templates
|
|
188
188
|
system_prompt: 'You are Sherlock Holmes. Approach every question with deductive reasoning and attention to detail.'
|
|
@@ -209,4 +209,4 @@ puts "\n💡 Tips:"
|
|
|
209
209
|
puts " - Use %{system_prompt}, %{query}, and %{context} placeholders in templates"
|
|
210
210
|
puts " - Templates use Ruby's % string formatting"
|
|
211
211
|
puts " - Override any or all template parts (system_prompt, no_context_template, with_context_template)"
|
|
212
|
-
puts " - Each provider can have completely different prompt behavior"
|
|
212
|
+
puts " - Each provider can have completely different prompt behavior"
|
data/examples/vector_search.rb
CHANGED
|
@@ -31,7 +31,7 @@ class VectorSearchExample
|
|
|
31
31
|
|
|
32
32
|
# Check if services are available
|
|
33
33
|
unless check_services_available
|
|
34
|
-
puts "❌ Required services not available. Please start with: docker
|
|
34
|
+
puts "❌ Required services not available. Please start with: docker compose up -d"
|
|
35
35
|
return
|
|
36
36
|
end
|
|
37
37
|
|
|
@@ -327,4 +327,4 @@ puts " - Try different embedding models (OpenAI, HuggingFace)"
|
|
|
327
327
|
puts " - Implement hybrid search (vector + keyword)"
|
|
328
328
|
puts " - Add document chunking for large texts"
|
|
329
329
|
puts " - Experiment with different similarity thresholds"
|
|
330
|
-
puts " - Add result re-ranking and filtering"
|
|
330
|
+
puts " - Add result re-ranking and filtering"
|
data/exe/prescient
ADDED
data/lib/prescient/base.rb
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
|
|
3
6
|
# Base class for all AI provider implementations
|
|
4
7
|
#
|
|
5
8
|
# This abstract base class defines the common interface that all AI providers
|
|
@@ -7,7 +10,8 @@
|
|
|
7
10
|
# formatting, prompt building, and error handling.
|
|
8
11
|
#
|
|
9
12
|
# @abstract Subclass and implement {#generate_embedding}, {#generate_response},
|
|
10
|
-
# {#health_check}, and
|
|
13
|
+
# {#health_check}, and any configuration validation required by
|
|
14
|
+
# {#validate_configuration!}
|
|
11
15
|
#
|
|
12
16
|
# @example Creating a custom provider
|
|
13
17
|
# class MyProvider < Prescient::Base
|
|
@@ -24,11 +28,9 @@
|
|
|
24
28
|
# end
|
|
25
29
|
# end
|
|
26
30
|
#
|
|
27
|
-
# @author Claude Code
|
|
28
|
-
# @since 1.0.0
|
|
29
31
|
class Prescient::Base
|
|
30
32
|
# @return [Hash] Configuration options for this provider instance
|
|
31
|
-
attr_reader :options
|
|
33
|
+
attr_reader :options, :provider_name
|
|
32
34
|
|
|
33
35
|
# Initialize the provider with configuration options
|
|
34
36
|
#
|
|
@@ -38,8 +40,12 @@ class Prescient::Base
|
|
|
38
40
|
# @option options [Integer] :timeout Request timeout in seconds
|
|
39
41
|
# @option options [Hash] :prompt_templates Custom prompt templates
|
|
40
42
|
# @option options [Hash] :context_configs Context formatting configurations
|
|
43
|
+
# @option options [Integer] :embedding_dimensions Expected custom embedding size
|
|
44
|
+
# @option options [Array<Symbol, String>] :context_excluded_fields Additional
|
|
45
|
+
# field names excluded from generic embedding text
|
|
41
46
|
def initialize(**options)
|
|
42
47
|
@options = options
|
|
48
|
+
@provider_name = options.fetch(:provider_name, self.class.to_s.split('::').last).to_s.sub(/\A./, &:upcase)
|
|
43
49
|
validate_configuration!
|
|
44
50
|
end
|
|
45
51
|
|
|
@@ -80,7 +86,8 @@ class Prescient::Base
|
|
|
80
86
|
# This method must be implemented by subclasses to provide health check
|
|
81
87
|
# functionality.
|
|
82
88
|
#
|
|
83
|
-
# @return [Hash] Health status with :status
|
|
89
|
+
# @return [Hash] Health status with at least :status and :provider keys,
|
|
90
|
+
# and typically :reachable and :ready for modern adapters
|
|
84
91
|
# @raise [NotImplementedError] If not implemented by subclass
|
|
85
92
|
# @abstract
|
|
86
93
|
def health_check
|
|
@@ -89,9 +96,14 @@ class Prescient::Base
|
|
|
89
96
|
|
|
90
97
|
# Check if the provider is currently available
|
|
91
98
|
#
|
|
92
|
-
#
|
|
99
|
+
# Returns `true` when the health check reports `reachable: true`.
|
|
100
|
+
# For legacy adapters that only return a status string, `status == "healthy"`
|
|
101
|
+
# is also treated as available.
|
|
102
|
+
#
|
|
103
|
+
# @return [Boolean] true if the provider is currently reachable
|
|
93
104
|
def available?
|
|
94
|
-
|
|
105
|
+
health = health_check
|
|
106
|
+
health.key?(:reachable) ? health[:reachable] == true : health[:status] == 'healthy'
|
|
95
107
|
rescue StandardError
|
|
96
108
|
false
|
|
97
109
|
end
|
|
@@ -135,25 +147,29 @@ class Prescient::Base
|
|
|
135
147
|
raise Prescient::Error, "Unexpected error: #{e.message}"
|
|
136
148
|
end
|
|
137
149
|
|
|
138
|
-
#
|
|
150
|
+
# Validate embedding dimensions against the configured model dimension.
|
|
139
151
|
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
152
|
+
# Embedding dimensions are part of the vector-storage contract. Vectors are
|
|
153
|
+
# never padded or truncated because either operation changes their meaning.
|
|
142
154
|
#
|
|
143
|
-
# @param embedding [Array<Float>] The embedding vector to
|
|
144
|
-
# @param target_dimensions [Integer] The
|
|
145
|
-
# @return [Array<Float
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
155
|
+
# @param embedding [Array<Float>] The embedding vector to validate
|
|
156
|
+
# @param target_dimensions [Integer] The required number of dimensions
|
|
157
|
+
# @return [Array<Float>] The original embedding when dimensions are valid
|
|
158
|
+
# @raise [Prescient::InvalidResponseError] If the vector is malformed or has
|
|
159
|
+
# an unexpected dimension
|
|
160
|
+
def validate_embedding_dimensions(embedding, target_dimensions)
|
|
161
|
+
raise Prescient::InvalidResponseError, 'Embedding response is not an array' unless embedding.is_a?(Array)
|
|
162
|
+
|
|
163
|
+
return embedding if embedding.length == target_dimensions
|
|
164
|
+
|
|
165
|
+
raise Prescient::InvalidResponseError,
|
|
166
|
+
"Invalid embedding dimensions: expected #{target_dimensions}, got #{embedding.length}"
|
|
151
167
|
end
|
|
152
168
|
|
|
153
169
|
# Clean and preprocess text for AI processing
|
|
154
170
|
#
|
|
155
|
-
# Removes excess whitespace, normalizes spacing, and
|
|
156
|
-
#
|
|
171
|
+
# Removes excess whitespace, normalizes spacing, and truncates to the
|
|
172
|
+
# library's current 8,000-character input ceiling.
|
|
157
173
|
#
|
|
158
174
|
# @param text [String, nil] The text to clean
|
|
159
175
|
# @return [String] Cleaned text, empty string if input was nil/empty
|
|
@@ -224,12 +240,15 @@ class Prescient::Base
|
|
|
224
240
|
|
|
225
241
|
# Minimal default context configuration - users should define their own contexts
|
|
226
242
|
def default_context_configs
|
|
243
|
+
embedding_fields = [] # : Array[untyped]
|
|
244
|
+
fields = [] # : Array[untyped]
|
|
245
|
+
|
|
227
246
|
{
|
|
228
247
|
# Generic fallback configuration - works with any hash structure
|
|
229
248
|
'default' => {
|
|
230
|
-
fields:
|
|
249
|
+
fields: fields, # Will be dynamically determined from item keys
|
|
231
250
|
format: nil, # Will use fallback formatting
|
|
232
|
-
embedding_fields:
|
|
251
|
+
embedding_fields: embedding_fields, # Will use all string/text fields
|
|
233
252
|
},
|
|
234
253
|
}
|
|
235
254
|
end
|
|
@@ -245,9 +264,13 @@ class Prescient::Base
|
|
|
245
264
|
|
|
246
265
|
# Extract text values from hash, excluding non-textual fields
|
|
247
266
|
def extract_text_values(item)
|
|
248
|
-
# Common fields to exclude from embedding text
|
|
249
|
-
#
|
|
250
|
-
|
|
267
|
+
# Common fields to exclude from embedding text. Provider-specific fields can
|
|
268
|
+
# be added with the :context_excluded_fields option.
|
|
269
|
+
default_excluded_fields = ['id', '_id', 'uuid', 'created_at', 'updated_at', 'timestamp', 'version', 'status',
|
|
270
|
+
'active']
|
|
271
|
+
configured_fields = Array(@options[:context_excluded_fields]) # : Array[untyped]
|
|
272
|
+
configured_excluded_fields = configured_fields.map { |field| field.to_s.downcase }
|
|
273
|
+
exclude_fields = default_excluded_fields | configured_excluded_fields
|
|
251
274
|
|
|
252
275
|
item.filter_map { |key, value|
|
|
253
276
|
next if exclude_fields.include?(key.to_s.downcase)
|
|
@@ -298,7 +321,7 @@ class Prescient::Base
|
|
|
298
321
|
|
|
299
322
|
# Build format data from item fields
|
|
300
323
|
def build_format_data(item, config)
|
|
301
|
-
format_data = {}
|
|
324
|
+
format_data = {} # : Hash[Symbol, untyped]
|
|
302
325
|
fields_to_check = config[:fields].any? ? config[:fields] : item.keys.map(&:to_s)
|
|
303
326
|
|
|
304
327
|
fields_to_check.each do |field|
|
|
@@ -371,4 +394,36 @@ class Prescient::Base
|
|
|
371
394
|
# Fallback: join key-value pairs
|
|
372
395
|
(format_data || item).map { |k, v| "#{k}: #{v}" }.join(', ')
|
|
373
396
|
end
|
|
397
|
+
|
|
398
|
+
def validate_response!(response, operation)
|
|
399
|
+
return if response.success?
|
|
400
|
+
|
|
401
|
+
resp_message, error_class = case response.code
|
|
402
|
+
when 400
|
|
403
|
+
['Bad Request', Prescient::Error]
|
|
404
|
+
when 401
|
|
405
|
+
['Authentication Failure', Prescient::AuthenticationError]
|
|
406
|
+
when 403
|
|
407
|
+
['Forbidden Access', Prescient::AuthenticationError]
|
|
408
|
+
when 404
|
|
409
|
+
['Model Not Available', Prescient::ModelNotAvailableError]
|
|
410
|
+
when 429
|
|
411
|
+
['Rate Limit Exceeded', Prescient::RateLimitError]
|
|
412
|
+
when 500..599
|
|
413
|
+
["#{provider_name} Server Error", Prescient::ProviderError]
|
|
414
|
+
else
|
|
415
|
+
["#{provider_name} Request Failure", Prescient::Error]
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
raise provider_error(resp_message, response, error_class:, operation:)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def provider_error(message, response, operation:, provider: nil, error_class: Prescient::ProviderError)
|
|
422
|
+
error_class.new(
|
|
423
|
+
message,
|
|
424
|
+
provider: provider || provider_name,
|
|
425
|
+
operation:,
|
|
426
|
+
status: response.code,
|
|
427
|
+
)
|
|
428
|
+
end
|
|
374
429
|
end
|