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.
data/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # Prescient
2
2
 
3
- Prescient provides a unified interface for AI providers including Ollama (local), Anthropic Claude, OpenAI GPT, and HuggingFace models. Built for prescient applications that need AI predictions with provider switching, error handling, and fallback mechanisms.
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.1, nomic-embed-text, etc.)
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 3 (Haiku, Sonnet, Opus)
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**: GPT-3.5, GPT-4, text-embedding-3-small/large
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.1:8b
73
+ OLLAMA_CHAT_MODEL=llama3.2:3b
70
74
 
71
75
  # Anthropic
72
76
  ANTHROPIC_API_KEY=your_api_key
73
- ANTHROPIC_MODEL=claude-3-haiku-20240307
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-3.5-turbo
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=microsoft/DialoGPT-medium
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::Provider,
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.1:8b'
106
+ chat_model: 'llama3.2:3b'
103
107
  )
104
108
 
105
109
  # Add Anthropic
106
- config.add_provider(:anthropic, Prescient::Anthropic::Provider,
110
+ config.add_provider(:anthropic, Prescient::Provider::Anthropic,
107
111
  api_key: ENV['ANTHROPIC_API_KEY'],
108
- model: 'claude-3-haiku-20240307'
112
+ model: 'claude-sonnet-4-20250514'
109
113
  )
110
114
 
111
115
  # Add OpenAI
112
- config.add_provider(:openai, Prescient::OpenAI::Provider,
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-3.5-turbo'
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-3.5-turbo'
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-3-haiku-20240307'
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.1:8b'
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
- - Only available (healthy) providers are tried during fallback
162
- - If no fallback providers are configured, all available providers are tried as fallbacks
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, ...] (768-dimensional vector)
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
- [:ollama, :anthropic, :openai, :huggingface].each do |provider|
253
+ Prescient.configuration.providers.keys.each do |provider|
244
254
  health = Prescient.health_check(provider: provider)
245
255
  puts "#{provider}: #{health[:status]}"
246
- puts "Ready: #{health[:ready]}" if health[:ready]
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-3.5-turbo',
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: '%{ name } by %{ brand }: %{ description } - $%{ price } (%{ category })',
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: '%{ product_name } - %{ rating }/5 stars: "%{ review_text }"',
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(:default)
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 integrates seamlessly with PostgreSQL's pgvector extension for storing and searching embeddings:
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-compose up -d postgres
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', 768, vector_str, 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-compose up -d postgres ollama
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::BaseProvider
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] # => "Prescient::Ollama::Provider"
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`, `list_models`
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.1:8b` | 8GB | 4.7GB | Chat model (8B parameters) |
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.1:8b` and upgrade based on your hardware capabilities and performance needs.
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-compose up -d ollama
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-compose up ollama-init
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.1:8b
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.1:8b"}'
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.1:7b ruby examples/custom_contexts.rb
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.1:8b
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.1:8b", "prompt": "Hello", "stream": false}'
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 rspec
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
- desc "Run tests and linting"
16
- task default: %w[test rubocop]
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 "Run tests with coverage"
19
- task :coverage do
20
- ENV['COVERAGE'] = 'true'
21
- Rake::Task[:test].execute
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
@@ -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-compose up -d postgres ollama
34
+ docker compose up -d postgres ollama
12
35
 
13
36
  # Wait for services to be ready
14
- docker-compose logs -f postgres ollama
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-compose up ollama-init
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', 768, vector_str, content]
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', 768, chunk_vector]
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', 768, vector_str, texts[index]]
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', dims: 768 },
326
- { client: Prescient.client(:openai), name: 'openai', model: 'text-embedding-3-small', dims: 1536 }
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(