prescient 0.2.0 → 0.3.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 +41 -4
- data/INTEGRATION_GUIDE.md +27 -29
- data/LICENSE.txt +1 -1
- data/README.md +133 -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/lib/prescient/base.rb +81 -26
- data/lib/prescient/client.rb +70 -36
- data/lib/prescient/errors.rb +51 -0
- data/lib/prescient/pgvector.rb +194 -0
- data/lib/prescient/provider/anthropic.rb +55 -54
- data/lib/prescient/provider/huggingface.rb +76 -76
- data/lib/prescient/provider/ollama.rb +46 -35
- data/lib/prescient/provider/openai.rb +38 -31
- data/lib/prescient/version.rb +2 -1
- data/lib/prescient.rb +85 -63
- data/scripts/setup-ollama-models.sh +2 -2
- data/sig/prescient.rbs +221 -1
- metadata +21 -217
- data/CHANGELOG.pdf +0 -0
- data/prescient.gemspec +0 -53
data/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# Prescient
|
|
2
2
|
|
|
3
|
-
Prescient
|
|
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
|
+
|
|
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).
|
|
4
8
|
|
|
5
9
|
## Features
|
|
6
10
|
|
|
@@ -16,19 +20,19 @@ Prescient provides a unified interface for AI providers including Ollama (local)
|
|
|
16
20
|
|
|
17
21
|
### Ollama (Local)
|
|
18
22
|
|
|
19
|
-
- **Models**: Any Ollama-compatible model (llama3.
|
|
23
|
+
- **Models**: Any Ollama-compatible model (llama3.2, nomic-embed-text, etc.)
|
|
20
24
|
- **Capabilities**: Embeddings, Text Generation, Model Management
|
|
21
25
|
- **Use Case**: Privacy-focused, local deployments
|
|
22
26
|
|
|
23
27
|
### Anthropic Claude
|
|
24
28
|
|
|
25
|
-
- **Models**: Claude
|
|
29
|
+
- **Models**: Current Claude models selected through the Anthropic Models API
|
|
26
30
|
- **Capabilities**: Text Generation only (no embeddings)
|
|
27
31
|
- **Use Case**: High-quality conversational AI
|
|
28
32
|
|
|
29
33
|
### OpenAI
|
|
30
34
|
|
|
31
|
-
- **Models**:
|
|
35
|
+
- **Models**: Current GPT and text-embedding models selected through the OpenAI Models API
|
|
32
36
|
- **Capabilities**: Embeddings, Text Generation
|
|
33
37
|
- **Use Case**: Proven performance, wide model selection
|
|
34
38
|
|
|
@@ -66,21 +70,21 @@ gem install prescient
|
|
|
66
70
|
# Ollama (Local)
|
|
67
71
|
OLLAMA_URL=http://localhost:11434
|
|
68
72
|
OLLAMA_EMBEDDING_MODEL=nomic-embed-text
|
|
69
|
-
OLLAMA_CHAT_MODEL=llama3.
|
|
73
|
+
OLLAMA_CHAT_MODEL=llama3.2:3b
|
|
70
74
|
|
|
71
75
|
# Anthropic
|
|
72
76
|
ANTHROPIC_API_KEY=your_api_key
|
|
73
|
-
ANTHROPIC_MODEL=claude-
|
|
77
|
+
ANTHROPIC_MODEL=claude-sonnet-4-20250514
|
|
74
78
|
|
|
75
79
|
# OpenAI
|
|
76
80
|
OPENAI_API_KEY=your_api_key
|
|
77
81
|
OPENAI_EMBEDDING_MODEL=text-embedding-3-small
|
|
78
|
-
OPENAI_CHAT_MODEL=gpt-
|
|
82
|
+
OPENAI_CHAT_MODEL=gpt-4.1-mini
|
|
79
83
|
|
|
80
84
|
# HuggingFace
|
|
81
85
|
HUGGINGFACE_API_KEY=your_api_key
|
|
82
86
|
HUGGINGFACE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
|
|
83
|
-
HUGGINGFACE_CHAT_MODEL=
|
|
87
|
+
HUGGINGFACE_CHAT_MODEL=google/gemma-2-2b-it
|
|
84
88
|
```
|
|
85
89
|
|
|
86
90
|
### Programmatic Configuration
|
|
@@ -96,23 +100,23 @@ Prescient.configure do |config|
|
|
|
96
100
|
config.retry_delay = 1.0
|
|
97
101
|
|
|
98
102
|
# Add custom Ollama configuration
|
|
99
|
-
config.add_provider(:ollama, Prescient::Ollama
|
|
103
|
+
config.add_provider(:ollama, Prescient::Provider::Ollama,
|
|
100
104
|
url: 'http://localhost:11434',
|
|
101
105
|
embedding_model: 'nomic-embed-text',
|
|
102
|
-
chat_model: 'llama3.
|
|
106
|
+
chat_model: 'llama3.2:3b'
|
|
103
107
|
)
|
|
104
108
|
|
|
105
109
|
# Add Anthropic
|
|
106
|
-
config.add_provider(:anthropic, Prescient::Anthropic
|
|
110
|
+
config.add_provider(:anthropic, Prescient::Provider::Anthropic,
|
|
107
111
|
api_key: ENV['ANTHROPIC_API_KEY'],
|
|
108
|
-
model: 'claude-
|
|
112
|
+
model: 'claude-sonnet-4-20250514'
|
|
109
113
|
)
|
|
110
114
|
|
|
111
115
|
# Add OpenAI
|
|
112
|
-
config.add_provider(:openai, Prescient::OpenAI
|
|
116
|
+
config.add_provider(:openai, Prescient::Provider::OpenAI,
|
|
113
117
|
api_key: ENV['OPENAI_API_KEY'],
|
|
114
118
|
embedding_model: 'text-embedding-3-small',
|
|
115
|
-
chat_model: 'gpt-
|
|
119
|
+
chat_model: 'gpt-4.1-mini'
|
|
116
120
|
)
|
|
117
121
|
end
|
|
118
122
|
```
|
|
@@ -127,19 +131,19 @@ Prescient.configure do |config|
|
|
|
127
131
|
config.add_provider(:primary, Prescient::Provider::OpenAI,
|
|
128
132
|
api_key: ENV['OPENAI_API_KEY'],
|
|
129
133
|
embedding_model: 'text-embedding-3-small',
|
|
130
|
-
chat_model: 'gpt-
|
|
134
|
+
chat_model: 'gpt-4.1-mini'
|
|
131
135
|
)
|
|
132
136
|
|
|
133
137
|
# Configure backup providers
|
|
134
138
|
config.add_provider(:backup1, Prescient::Provider::Anthropic,
|
|
135
139
|
api_key: ENV['ANTHROPIC_API_KEY'],
|
|
136
|
-
model: 'claude-
|
|
140
|
+
model: 'claude-sonnet-4-20250514'
|
|
137
141
|
)
|
|
138
142
|
|
|
139
143
|
config.add_provider(:backup2, Prescient::Provider::Ollama,
|
|
140
144
|
url: 'http://localhost:11434',
|
|
141
145
|
embedding_model: 'nomic-embed-text',
|
|
142
|
-
chat_model: 'llama3.
|
|
146
|
+
chat_model: 'llama3.2:3b'
|
|
143
147
|
)
|
|
144
148
|
|
|
145
149
|
# Configure fallback order
|
|
@@ -158,9 +162,10 @@ response = Prescient.generate_response("Hello", provider: :primary, enable_fallb
|
|
|
158
162
|
|
|
159
163
|
**Fallback Behavior:**
|
|
160
164
|
- When a provider fails with a persistent error, Prescient automatically tries the next available provider
|
|
161
|
-
-
|
|
162
|
-
- If no fallback providers are configured, all
|
|
165
|
+
- Configured fallback providers are tried in order; the provider operation determines availability
|
|
166
|
+
- If no fallback providers are configured, all configured providers are tried as fallbacks
|
|
163
167
|
- Transient errors (rate limits, timeouts) still use retry logic before fallback
|
|
168
|
+
- Provider-service failures, connection failures, rate limits, and unavailable models may trigger fallback; authentication and invalid-request errors are returned to the caller
|
|
164
169
|
- The fallback process preserves all method arguments and options
|
|
165
170
|
|
|
166
171
|
## Usage
|
|
@@ -175,7 +180,7 @@ client = Prescient.client
|
|
|
175
180
|
|
|
176
181
|
# Generate embeddings
|
|
177
182
|
embedding = client.generate_embedding("Your text here")
|
|
178
|
-
# => [0.1, 0.2, 0.3, ...] (
|
|
183
|
+
# => [0.1, 0.2, 0.3, ...] (model-dependent vector dimensions)
|
|
179
184
|
|
|
180
185
|
# Generate text responses
|
|
181
186
|
response = client.generate_response("What is Ruby?")
|
|
@@ -238,12 +243,20 @@ end
|
|
|
238
243
|
|
|
239
244
|
### Health Monitoring
|
|
240
245
|
|
|
246
|
+
Health results separate transport reachability from configured-model readiness:
|
|
247
|
+
`reachable: true` means the provider answered, while `ready: true` means the
|
|
248
|
+
configured operation models were found or validated. Fallback uses the actual
|
|
249
|
+
operation and does not perform an additional health request.
|
|
250
|
+
|
|
241
251
|
```ruby
|
|
242
252
|
# Check all providers
|
|
243
|
-
|
|
253
|
+
Prescient.configuration.providers.keys.each do |provider|
|
|
244
254
|
health = Prescient.health_check(provider: provider)
|
|
245
255
|
puts "#{provider}: #{health[:status]}"
|
|
246
|
-
puts "
|
|
256
|
+
puts "Reachable: #{health[:reachable]}"
|
|
257
|
+
puts "Ready: #{health[:ready]}"
|
|
258
|
+
rescue Prescient::Error => e
|
|
259
|
+
puts "#{provider}: unavailable (#{e.message})"
|
|
247
260
|
end
|
|
248
261
|
```
|
|
249
262
|
|
|
@@ -256,7 +269,7 @@ Prescient.configure do |config|
|
|
|
256
269
|
config.add_provider(:customer_service, Prescient::Provider::OpenAI,
|
|
257
270
|
api_key: ENV['OPENAI_API_KEY'],
|
|
258
271
|
embedding_model: 'text-embedding-3-small',
|
|
259
|
-
chat_model: 'gpt-
|
|
272
|
+
chat_model: 'gpt-4.1-mini',
|
|
260
273
|
prompt_templates: {
|
|
261
274
|
system_prompt: 'You are a friendly customer service representative.',
|
|
262
275
|
no_context_template: <<~TEMPLATE.strip,
|
|
@@ -330,12 +343,12 @@ Prescient.configure do |config|
|
|
|
330
343
|
context_configs: {
|
|
331
344
|
'product' => {
|
|
332
345
|
fields: %w[name description price category brand],
|
|
333
|
-
format: '%{
|
|
346
|
+
format: '%{name} by %{brand}: %{description} - $%{price} (%{category})',
|
|
334
347
|
embedding_fields: %w[name description category brand]
|
|
335
348
|
},
|
|
336
349
|
'review' => {
|
|
337
350
|
fields: %w[product_name rating review_text reviewer_name],
|
|
338
|
-
format: '%{
|
|
351
|
+
format: '%{product_name} - %{rating}/5 stars: "%{review_text}"',
|
|
339
352
|
embedding_fields: %w[product_name review_text]
|
|
340
353
|
}
|
|
341
354
|
}
|
|
@@ -363,6 +376,14 @@ response = client.generate_response("I need a laptop for work", products)
|
|
|
363
376
|
- **fields** - Array of field names available for this context type
|
|
364
377
|
- **format** - Template string for displaying context items
|
|
365
378
|
- **embedding_fields** - Specific fields to use when generating embeddings
|
|
379
|
+
- **context_excluded_fields** - Additional field names excluded from generic embedding text; built-in metadata exclusions remain active
|
|
380
|
+
|
|
381
|
+
```ruby
|
|
382
|
+
config.add_provider(:openai, Prescient::Provider::OpenAI,
|
|
383
|
+
api_key: ENV['OPENAI_API_KEY'],
|
|
384
|
+
context_excluded_fields: %w[tenant_id internal_notes]
|
|
385
|
+
)
|
|
386
|
+
```
|
|
366
387
|
|
|
367
388
|
### Automatic Context Detection
|
|
368
389
|
|
|
@@ -384,7 +405,7 @@ The system works perfectly without any context configuration - it will:
|
|
|
384
405
|
|
|
385
406
|
```ruby
|
|
386
407
|
# No context_configs needed - works with any data!
|
|
387
|
-
client = Prescient.client
|
|
408
|
+
client = Prescient.client
|
|
388
409
|
response = client.generate_response("Analyze this", [
|
|
389
410
|
{ 'title' => 'Issue', 'content' => 'Server down', 'created_at' => '2024-01-01' },
|
|
390
411
|
{ 'name' => 'Alert', 'message' => 'High CPU usage', 'timestamp' => 1234567 }
|
|
@@ -393,9 +414,44 @@ response = client.generate_response("Analyze this", [
|
|
|
393
414
|
|
|
394
415
|
See `examples/custom_contexts.rb` for complete examples.
|
|
395
416
|
|
|
417
|
+
### Sensitive Provider Options
|
|
418
|
+
|
|
419
|
+
`provider_info` always removes the built-in sensitive keys (`api_key`,
|
|
420
|
+
`password`, `token`, and `secret`). Add project-specific keys globally with
|
|
421
|
+
`sensitive_keys`; nested hashes and arrays are sanitized recursively:
|
|
422
|
+
|
|
423
|
+
```ruby
|
|
424
|
+
Prescient.configure do |config|
|
|
425
|
+
config.sensitive_keys = %w[workspace_secret private_key]
|
|
426
|
+
end
|
|
427
|
+
```
|
|
428
|
+
|
|
396
429
|
## Vector Database Integration (pgvector)
|
|
397
430
|
|
|
398
|
-
Prescient
|
|
431
|
+
`Prescient::Pgvector::Store` is an opt-in PostgreSQL integration for storing
|
|
432
|
+
and searching embeddings. It accepts a PG-compatible connection, so `pg` stays
|
|
433
|
+
an application dependency rather than a required dependency of this gem.
|
|
434
|
+
|
|
435
|
+
```ruby
|
|
436
|
+
require 'pg'
|
|
437
|
+
|
|
438
|
+
connection = PG.connect(dbname: 'my_app')
|
|
439
|
+
store = Prescient::Pgvector::Store.new(connection: connection, dimensions: 1536)
|
|
440
|
+
store.install!
|
|
441
|
+
store.create_index!
|
|
442
|
+
|
|
443
|
+
embedding = Prescient.generate_embedding('Semantic search', provider: :openai)
|
|
444
|
+
store.upsert(
|
|
445
|
+
id: 'document-42', embedding:, provider: 'openai', model: 'text-embedding-3-small',
|
|
446
|
+
content: 'Semantic search', metadata: { category: 'guide' }
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
results = store.search(embedding:, provider: :openai, model: 'text-embedding-3-small')
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Every vector must exactly match the store's configured dimensions; Prescient
|
|
453
|
+
never pads or truncates vectors. The existing application-schema example below
|
|
454
|
+
remains available for projects that need documents, chunks, and custom metadata.
|
|
399
455
|
|
|
400
456
|
### Setup with Docker
|
|
401
457
|
|
|
@@ -403,7 +459,7 @@ The included `docker-compose.yml` provides a complete setup with PostgreSQL + pg
|
|
|
403
459
|
|
|
404
460
|
```bash
|
|
405
461
|
# Start PostgreSQL with pgvector
|
|
406
|
-
docker
|
|
462
|
+
docker compose up -d postgres
|
|
407
463
|
|
|
408
464
|
# The database will automatically:
|
|
409
465
|
# - Install pgvector extension
|
|
@@ -447,7 +503,7 @@ embedding = client.generate_embedding(text)
|
|
|
447
503
|
vector_str = "[#{embedding.join(',')}]"
|
|
448
504
|
db.exec_params(
|
|
449
505
|
"INSERT INTO document_embeddings (document_id, embedding_provider, embedding_model, embedding_dimensions, embedding, embedding_text) VALUES ($1, $2, $3, $4, $5, $6)",
|
|
450
|
-
[doc_id, 'ollama', 'nomic-embed-text',
|
|
506
|
+
[doc_id, 'ollama', 'nomic-embed-text', embedding.length, vector_str, text]
|
|
451
507
|
)
|
|
452
508
|
|
|
453
509
|
# Perform similarity search
|
|
@@ -588,7 +644,7 @@ Run the complete vector search example:
|
|
|
588
644
|
|
|
589
645
|
```bash
|
|
590
646
|
# Start services
|
|
591
|
-
docker
|
|
647
|
+
docker compose up -d postgres ollama
|
|
592
648
|
|
|
593
649
|
# Run example
|
|
594
650
|
DB_HOST=localhost ruby examples/vector_search.rb
|
|
@@ -606,7 +662,7 @@ The example demonstrates:
|
|
|
606
662
|
### Custom Provider Implementation
|
|
607
663
|
|
|
608
664
|
```ruby
|
|
609
|
-
class MyCustomProvider < Prescient::
|
|
665
|
+
class MyCustomProvider < Prescient::Base
|
|
610
666
|
def generate_embedding(text, **options)
|
|
611
667
|
# Your implementation
|
|
612
668
|
end
|
|
@@ -642,7 +698,7 @@ client = Prescient.client(:ollama)
|
|
|
642
698
|
info = client.provider_info
|
|
643
699
|
|
|
644
700
|
puts info[:name] # => :ollama
|
|
645
|
-
puts info[:class] # => "
|
|
701
|
+
puts info[:class] # => "Ollama"
|
|
646
702
|
puts info[:available] # => true
|
|
647
703
|
puts info[:options] # => { ... } (excluding sensitive data)
|
|
648
704
|
```
|
|
@@ -651,7 +707,7 @@ puts info[:options] # => { ... } (excluding sensitive data)
|
|
|
651
707
|
|
|
652
708
|
### Ollama
|
|
653
709
|
|
|
654
|
-
- Model management: `pull_model`, `
|
|
710
|
+
- Model management: `pull_model`, `available_models`
|
|
655
711
|
- Local deployment support
|
|
656
712
|
- No API costs
|
|
657
713
|
|
|
@@ -665,6 +721,7 @@ puts info[:options] # => { ... } (excluding sensitive data)
|
|
|
665
721
|
- Multiple embedding model sizes
|
|
666
722
|
- Latest GPT models
|
|
667
723
|
- Reliable performance
|
|
724
|
+
- Uses the Chat Completions endpoint for the stable normalized response contract; the newer Responses API remains a future compatibility extension.
|
|
668
725
|
|
|
669
726
|
### HuggingFace
|
|
670
727
|
|
|
@@ -692,7 +749,7 @@ Before starting, ensure your system meets the minimum requirements for running O
|
|
|
692
749
|
| Model | RAM Required | Storage | Notes |
|
|
693
750
|
| ------------------ | ------------ | ------- | --------------------------------- |
|
|
694
751
|
| `nomic-embed-text` | 1GB | 274MB | Embedding model |
|
|
695
|
-
| `llama3.
|
|
752
|
+
| `llama3.2:3b` | 2GB | 2.0GB | Chat model (3B parameters) |
|
|
696
753
|
| `llama3.1:70b` | 64GB+ | 40GB | Large chat model (70B parameters) |
|
|
697
754
|
| `codellama:7b` | 8GB | 3.8GB | Code generation model |
|
|
698
755
|
|
|
@@ -710,21 +767,21 @@ Before starting, ensure your system meets the minimum requirements for running O
|
|
|
710
767
|
- **Docker**: NVIDIA Container Toolkit installed
|
|
711
768
|
- **Performance**: 3-10x faster inference with compatible models
|
|
712
769
|
|
|
713
|
-
> **💡 Tip**: Start with smaller models like `llama3.
|
|
770
|
+
> **💡 Tip**: Start with smaller models like `llama3.2:3b` and upgrade based on your hardware capabilities and performance needs.
|
|
714
771
|
|
|
715
772
|
### Quick Start with Docker
|
|
716
773
|
|
|
717
774
|
1. **Start Ollama service:**
|
|
718
775
|
|
|
719
776
|
```bash
|
|
720
|
-
docker
|
|
777
|
+
docker compose up -d ollama
|
|
721
778
|
```
|
|
722
779
|
|
|
723
780
|
2. **Pull required models:**
|
|
724
781
|
|
|
725
782
|
```bash
|
|
726
783
|
# Automatic setup
|
|
727
|
-
docker
|
|
784
|
+
docker compose run --rm ollama-init
|
|
728
785
|
|
|
729
786
|
# Or manual setup
|
|
730
787
|
./scripts/setup-ollama-models.sh
|
|
@@ -786,7 +843,7 @@ services:
|
|
|
786
843
|
# Ollama Configuration
|
|
787
844
|
OLLAMA_URL=http://localhost:11434
|
|
788
845
|
OLLAMA_EMBEDDING_MODEL=nomic-embed-text
|
|
789
|
-
OLLAMA_CHAT_MODEL=llama3.
|
|
846
|
+
OLLAMA_CHAT_MODEL=llama3.2:3b
|
|
790
847
|
|
|
791
848
|
# Optional: Other AI providers
|
|
792
849
|
OPENAI_API_KEY=your_key_here
|
|
@@ -803,7 +860,7 @@ curl http://localhost:11434/api/tags
|
|
|
803
860
|
# Pull a specific model
|
|
804
861
|
curl -X POST http://localhost:11434/api/pull \
|
|
805
862
|
-H "Content-Type: application/json" \
|
|
806
|
-
-d '{ "name": "llama3.
|
|
863
|
+
-d '{ "name": "llama3.2:3b"}'
|
|
807
864
|
|
|
808
865
|
# Health check
|
|
809
866
|
curl http://localhost:11434/api/version
|
|
@@ -833,7 +890,7 @@ free -h
|
|
|
833
890
|
# Settings > Resources > Memory: 8GB+
|
|
834
891
|
|
|
835
892
|
# Use smaller models if hardware limited
|
|
836
|
-
OLLAMA_CHAT_MODEL=llama3.
|
|
893
|
+
OLLAMA_CHAT_MODEL=llama3.2:3b ruby examples/custom_contexts.rb
|
|
837
894
|
```
|
|
838
895
|
|
|
839
896
|
**Slow Model Loading:**
|
|
@@ -853,7 +910,7 @@ iostat -x 1
|
|
|
853
910
|
df -h
|
|
854
911
|
|
|
855
912
|
# Manually pull models with retry
|
|
856
|
-
docker exec prescient-ollama ollama pull llama3.
|
|
913
|
+
docker exec prescient-ollama ollama pull llama3.2:3b
|
|
857
914
|
```
|
|
858
915
|
|
|
859
916
|
**GPU Not Detected:**
|
|
@@ -878,7 +935,7 @@ docker logs prescient-ollama
|
|
|
878
935
|
# Test API response time
|
|
879
936
|
time curl -X POST http://localhost:11434/api/generate \
|
|
880
937
|
-H "Content-Type: application/json" \
|
|
881
|
-
-d '{ "model": "llama3.
|
|
938
|
+
-d '{ "model": "llama3.2:3b", "prompt": "Hello", "stream": false}'
|
|
882
939
|
```
|
|
883
940
|
|
|
884
941
|
## Testing
|
|
@@ -886,11 +943,44 @@ time curl -X POST http://localhost:11434/api/generate \
|
|
|
886
943
|
The gem includes comprehensive test coverage:
|
|
887
944
|
|
|
888
945
|
```bash
|
|
889
|
-
bundle exec
|
|
946
|
+
bundle exec rake test
|
|
890
947
|
```
|
|
891
948
|
|
|
892
949
|
## Development
|
|
893
950
|
|
|
951
|
+
### Opt-in live provider smoke tests
|
|
952
|
+
|
|
953
|
+
The default test suite uses mocked provider interactions. To exercise a live
|
|
954
|
+
provider explicitly, set `PRESCIENT_LIVE_SMOKE=1` and select one or more
|
|
955
|
+
providers with `PRESCIENT_LIVE_PROVIDERS`:
|
|
956
|
+
|
|
957
|
+
```bash
|
|
958
|
+
PRESCIENT_LIVE_SMOKE=1 \
|
|
959
|
+
PRESCIENT_LIVE_PROVIDERS=openai \
|
|
960
|
+
OPENAI_API_KEY=... \
|
|
961
|
+
bundle exec ruby -Itest test/prescient/live_provider_smoke_test.rb
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
Supported provider names are `ollama`, `anthropic`, `openai`, and
|
|
965
|
+
`huggingface`. The corresponding provider environment variables and model
|
|
966
|
+
overrides are honored. These tests are never live unless both opt-in
|
|
967
|
+
variables are set.
|
|
968
|
+
|
|
969
|
+
### RBS and Steep
|
|
970
|
+
|
|
971
|
+
Validate the curated core API signatures with:
|
|
972
|
+
|
|
973
|
+
```bash
|
|
974
|
+
bundle exec rake rbs:validate
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
Generate disposable prototypes for comparison with:
|
|
978
|
+
|
|
979
|
+
```bash
|
|
980
|
+
bundle exec rake rbs:prototype
|
|
981
|
+
bundle exec rake rbs:diff
|
|
982
|
+
```
|
|
983
|
+
|
|
894
984
|
After checking out the repo, run:
|
|
895
985
|
|
|
896
986
|
```bash
|
|
@@ -914,34 +1004,3 @@ bundle exec rake install
|
|
|
914
1004
|
## License
|
|
915
1005
|
|
|
916
1006
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
917
|
-
|
|
918
|
-
## Roadmap
|
|
919
|
-
|
|
920
|
-
### Version 0.2.0 (Planned)
|
|
921
|
-
|
|
922
|
-
- **MariaDB Vector Support**: Integration with MariaDB using external vector databases
|
|
923
|
-
- **Hybrid Database Architecture**: Support for MariaDB + Milvus/Qdrant combinations
|
|
924
|
-
- **Vector Database Adapters**: Pluggable adapters for different vector storage backends
|
|
925
|
-
- **Enhanced Chunking Strategies**: Smart document splitting with multiple algorithms
|
|
926
|
-
- **Search Result Ranking**: Advanced scoring and re-ranking capabilities
|
|
927
|
-
|
|
928
|
-
### Version 0.3.0 (Future)
|
|
929
|
-
|
|
930
|
-
- **Streaming Responses**: Real-time response streaming for chat applications
|
|
931
|
-
- **Multi-Model Ensembles**: Combine responses from multiple AI providers
|
|
932
|
-
- **Advanced Analytics**: Search performance insights and usage analytics
|
|
933
|
-
- **Cloud Provider Integration**: Direct support for Pinecone, Weaviate, etc.
|
|
934
|
-
|
|
935
|
-
## Changelog
|
|
936
|
-
|
|
937
|
-
### Version 0.1.0
|
|
938
|
-
|
|
939
|
-
- Initial release
|
|
940
|
-
- Support for Ollama, Anthropic, OpenAI, and HuggingFace
|
|
941
|
-
- Unified interface for embeddings and text generation
|
|
942
|
-
- Comprehensive error handling and retry logic
|
|
943
|
-
- Health monitoring capabilities
|
|
944
|
-
- PostgreSQL pgvector integration with complete Docker setup
|
|
945
|
-
- Vector similarity search with multiple distance functions
|
|
946
|
-
- Document chunking and metadata filtering
|
|
947
|
-
- Performance optimization guides and troubleshooting
|
data/Rakefile
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "bundler/gem_tasks"
|
|
4
|
+
require "rbconfig"
|
|
4
5
|
require "rake/testtask"
|
|
5
6
|
require "rubocop/rake_task"
|
|
7
|
+
require "yard"
|
|
8
|
+
require "yard/rake/yardoc_task"
|
|
6
9
|
|
|
7
10
|
Rake::TestTask.new(:test) do |t|
|
|
8
11
|
t.libs << "test"
|
|
@@ -10,17 +13,89 @@ Rake::TestTask.new(:test) do |t|
|
|
|
10
13
|
t.test_files = FileList["test/**/*_test.rb"]
|
|
11
14
|
end
|
|
12
15
|
|
|
13
|
-
RuboCop::RakeTask.new
|
|
16
|
+
RuboCop::RakeTask.new(:rubocop) do |task|
|
|
17
|
+
task.options = ["--parallel"]
|
|
18
|
+
end
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
YARD::Rake::YardocTask.new(:yard)
|
|
21
|
+
namespace :yard do
|
|
22
|
+
desc "Validate YARD documentation coverage"
|
|
23
|
+
task :validate do
|
|
24
|
+
require "open3"
|
|
25
|
+
|
|
26
|
+
stdout, stderr, status = Open3.capture3("bundle", "exec", "yard", "stats")
|
|
27
|
+
text = "#{stdout}\n#{stderr}"
|
|
28
|
+
puts text
|
|
29
|
+
abort("yard stats failed") unless status.success?
|
|
30
|
+
|
|
31
|
+
match = text.match(/([0-9]+(?:\.[0-9]+)?)%\s+documented/)
|
|
32
|
+
abort("Unable to determine YARD coverage") unless match
|
|
33
|
+
|
|
34
|
+
coverage = match[1].to_f
|
|
35
|
+
minimum = 99.0
|
|
36
|
+
if coverage < minimum
|
|
37
|
+
message = format(
|
|
38
|
+
"YARD coverage %<coverage>.2f%% is below %<minimum>.2f%%",
|
|
39
|
+
coverage: coverage,
|
|
40
|
+
minimum: minimum
|
|
41
|
+
)
|
|
42
|
+
abort(message)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
puts format("YARD coverage %.2f%%", coverage)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
namespace :rbs do
|
|
50
|
+
desc "Remove generated RBS prototype files"
|
|
51
|
+
task :clobber do
|
|
52
|
+
sh "rm -rf tmp/sig"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
desc "Generate disposable RBS prototypes into tmp/sig"
|
|
56
|
+
task :prototype do
|
|
57
|
+
sh "rm -rf tmp/sig"
|
|
58
|
+
sh "mkdir -p tmp/sig"
|
|
59
|
+
sh "bundle exec rbs prototype rb --out-dir=tmp/sig --base-dir=lib lib"
|
|
60
|
+
end
|
|
17
61
|
|
|
18
|
-
desc "
|
|
19
|
-
task :
|
|
20
|
-
|
|
21
|
-
|
|
62
|
+
desc "Validate curated RBS signatures with Steep"
|
|
63
|
+
task :validate do
|
|
64
|
+
sh "bundle exec steep check"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
desc "Open diff between curated and generated signatures"
|
|
68
|
+
task :diff do
|
|
69
|
+
sh "diff -ru sig tmp/sig || true"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
desc "Generate disposable RBS prototypes and validate curated signatures"
|
|
73
|
+
task check: %i[prototype validate]
|
|
22
74
|
end
|
|
23
75
|
|
|
76
|
+
namespace :examples do
|
|
77
|
+
desc "Validate Ruby example syntax without contacting providers"
|
|
78
|
+
task :syntax do
|
|
79
|
+
Dir["examples/**/*.rb"].sort.each do |file|
|
|
80
|
+
sh RbConfig.ruby, "-c", file
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
desc "Validate GitHub Actions workflows"
|
|
86
|
+
task :actionlint do
|
|
87
|
+
actionlint = if File.executable?(".tools/bin/actionlint")
|
|
88
|
+
".tools/bin/actionlint"
|
|
89
|
+
else
|
|
90
|
+
ENV.fetch("ACTIONLINT", "actionlint")
|
|
91
|
+
end
|
|
92
|
+
sh actionlint
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
desc "Run tests and linting"
|
|
97
|
+
task default: %w[test rubocop yard yard:validate rbs:validate examples:syntax]
|
|
98
|
+
|
|
24
99
|
desc "Console with gem loaded"
|
|
25
100
|
task :console do
|
|
26
101
|
require "bundler/setup"
|
data/Steepfile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
target :prescient do
|
|
4
|
+
signature 'sig'
|
|
5
|
+
library 'json'
|
|
6
|
+
library 'net-http'
|
|
7
|
+
|
|
8
|
+
check 'lib/prescient/version.rb'
|
|
9
|
+
check 'lib/prescient/errors.rb'
|
|
10
|
+
check 'lib/prescient.rb'
|
|
11
|
+
check 'lib/prescient/client.rb'
|
|
12
|
+
check 'lib/prescient/base.rb'
|
|
13
|
+
check 'lib/prescient/provider/openai.rb'
|
|
14
|
+
check 'lib/prescient/provider/ollama.rb'
|
|
15
|
+
check 'lib/prescient/provider/anthropic.rb'
|
|
16
|
+
check 'lib/prescient/provider/huggingface.rb'
|
|
17
|
+
end
|
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(
|