prescient 0.1.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 +6 -4
- data/.yardopts +11 -0
- data/CHANGELOG.md +101 -0
- data/INTEGRATION_GUIDE.md +361 -0
- data/LICENSE.txt +1 -1
- data/README.md +217 -100
- data/Rakefile +84 -8
- data/Steepfile +17 -0
- data/VECTOR_SEARCH_GUIDE.md +74 -47
- 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 +187 -28
- data/lib/prescient/client.rb +169 -31
- 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 +77 -79
- 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 +160 -36
- data/scripts/setup-ollama-models.sh +2 -2
- data/sig/prescient.rbs +221 -1
- metadata +23 -184
- data/prescient.gemspec +0 -51
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,21 +13,94 @@ 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
|
|
17
44
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
ENV['COVERAGE'] = 'true'
|
|
21
|
-
Rake::Task[:test].execute
|
|
45
|
+
puts format("YARD coverage %.2f%%", coverage)
|
|
46
|
+
end
|
|
22
47
|
end
|
|
23
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
|
|
61
|
+
|
|
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]
|
|
74
|
+
end
|
|
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"
|
|
27
102
|
require "prescient"
|
|
28
103
|
require "irb"
|
|
104
|
+
ARGV.clear
|
|
29
105
|
IRB.start
|
|
30
|
-
end
|
|
106
|
+
end
|
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
|
|
|
@@ -130,17 +153,17 @@ query_embedding = client.generate_embedding(query_text)
|
|
|
130
153
|
query_vector = "[#{query_embedding.join(',')}]"
|
|
131
154
|
|
|
132
155
|
results = db.exec_params(
|
|
133
|
-
"SELECT d.title, d.content, de.embedding <=> $1::vector AS distance
|
|
134
|
-
FROM documents d
|
|
135
|
-
JOIN document_embeddings de ON d.id = de.document_id
|
|
136
|
-
ORDER BY de.embedding <=> $1::vector
|
|
156
|
+
"SELECT d.title, d.content, de.embedding <=> $1::vector AS distance
|
|
157
|
+
FROM documents d
|
|
158
|
+
JOIN document_embeddings de ON d.id = de.document_id
|
|
159
|
+
ORDER BY de.embedding <=> $1::vector
|
|
137
160
|
LIMIT 5",
|
|
138
161
|
[query_vector]
|
|
139
162
|
)
|
|
140
163
|
|
|
141
164
|
results.each do |row|
|
|
142
165
|
similarity = 1 - row['distance'].to_f
|
|
143
|
-
puts "#{row['title']} (#{(similarity * 100).round(1)}% similar)"
|
|
166
|
+
puts "#{ row['title']} (#{ (similarity * 100).round(1)}% similar)"
|
|
144
167
|
end
|
|
145
168
|
```
|
|
146
169
|
|
|
@@ -150,11 +173,11 @@ end
|
|
|
150
173
|
# Search with metadata filtering
|
|
151
174
|
results = db.exec_params(
|
|
152
175
|
"SELECT d.title, de.embedding <=> $1::vector as distance
|
|
153
|
-
FROM documents d
|
|
176
|
+
FROM documents d
|
|
154
177
|
JOIN document_embeddings de ON d.id = de.document_id
|
|
155
178
|
WHERE d.metadata->'tags' ? 'programming'
|
|
156
179
|
AND d.metadata->>'difficulty' = 'beginner'
|
|
157
|
-
ORDER BY de.embedding <=> $1::vector
|
|
180
|
+
ORDER BY de.embedding <=> $1::vector
|
|
158
181
|
LIMIT 10",
|
|
159
182
|
[query_vector]
|
|
160
183
|
)
|
|
@@ -168,17 +191,17 @@ For large documents, split into chunks for better search granularity:
|
|
|
168
191
|
def chunk_document(text, chunk_size: 500, overlap: 50)
|
|
169
192
|
chunks = []
|
|
170
193
|
start = 0
|
|
171
|
-
|
|
194
|
+
|
|
172
195
|
while start < text.length
|
|
173
196
|
end_pos = [start + chunk_size, text.length].min
|
|
174
|
-
|
|
197
|
+
|
|
175
198
|
# Find word boundary to avoid cutting words
|
|
176
199
|
if end_pos < text.length
|
|
177
200
|
while end_pos > start && text[end_pos] != ' '
|
|
178
201
|
end_pos -= 1
|
|
179
202
|
end
|
|
180
203
|
end
|
|
181
|
-
|
|
204
|
+
|
|
182
205
|
chunk = text[start...end_pos].strip
|
|
183
206
|
chunks << {
|
|
184
207
|
text: chunk,
|
|
@@ -186,11 +209,11 @@ def chunk_document(text, chunk_size: 500, overlap: 50)
|
|
|
186
209
|
end_pos: end_pos,
|
|
187
210
|
index: chunks.length
|
|
188
211
|
}
|
|
189
|
-
|
|
212
|
+
|
|
190
213
|
start = end_pos - overlap
|
|
191
214
|
break if start >= text.length
|
|
192
215
|
end
|
|
193
|
-
|
|
216
|
+
|
|
194
217
|
chunks
|
|
195
218
|
end
|
|
196
219
|
|
|
@@ -200,18 +223,18 @@ chunks.each do |chunk|
|
|
|
200
223
|
# Insert chunk
|
|
201
224
|
chunk_result = db.exec_params(
|
|
202
225
|
"INSERT INTO document_chunks (document_id, chunk_index, chunk_text, chunk_metadata) VALUES ($1, $2, $3, $4) RETURNING id",
|
|
203
|
-
[document_id, chunk[:index], chunk[:text], {start_pos: chunk[:start_pos], end_pos: chunk[:end_pos]}.to_json]
|
|
226
|
+
[document_id, chunk[:index], chunk[:text], { start_pos: chunk[:start_pos], end_pos: chunk[:end_pos]}.to_json]
|
|
204
227
|
)
|
|
205
228
|
chunk_id = chunk_result[0]['id']
|
|
206
|
-
|
|
229
|
+
|
|
207
230
|
# Generate embedding for chunk
|
|
208
231
|
chunk_embedding = client.generate_embedding(chunk[:text])
|
|
209
232
|
chunk_vector = "[#{chunk_embedding.join(',')}]"
|
|
210
|
-
|
|
233
|
+
|
|
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
|
```
|
|
@@ -224,20 +247,20 @@ For different dataset sizes and performance requirements:
|
|
|
224
247
|
|
|
225
248
|
```sql
|
|
226
249
|
-- Small datasets (< 100K vectors): Fast build, good accuracy
|
|
227
|
-
CREATE INDEX idx_embeddings_small
|
|
228
|
-
ON document_embeddings
|
|
250
|
+
CREATE INDEX idx_embeddings_small
|
|
251
|
+
ON document_embeddings
|
|
229
252
|
USING hnsw (embedding vector_cosine_ops)
|
|
230
253
|
WITH (m = 8, ef_construction = 32);
|
|
231
254
|
|
|
232
255
|
-- Medium datasets (100K - 1M vectors): Balanced
|
|
233
|
-
CREATE INDEX idx_embeddings_medium
|
|
234
|
-
ON document_embeddings
|
|
256
|
+
CREATE INDEX idx_embeddings_medium
|
|
257
|
+
ON document_embeddings
|
|
235
258
|
USING hnsw (embedding vector_cosine_ops)
|
|
236
259
|
WITH (m = 16, ef_construction = 64);
|
|
237
260
|
|
|
238
261
|
-- Large datasets (> 1M vectors): High accuracy
|
|
239
|
-
CREATE INDEX idx_embeddings_large
|
|
240
|
-
ON document_embeddings
|
|
262
|
+
CREATE INDEX idx_embeddings_large
|
|
263
|
+
ON document_embeddings
|
|
241
264
|
USING hnsw (embedding vector_cosine_ops)
|
|
242
265
|
WITH (m = 32, ef_construction = 128);
|
|
243
266
|
```
|
|
@@ -251,9 +274,9 @@ SET hnsw.ef_search = 100; -- Balanced (default)
|
|
|
251
274
|
SET hnsw.ef_search = 200; -- High accuracy, slower
|
|
252
275
|
|
|
253
276
|
-- Monitor query performance
|
|
254
|
-
EXPLAIN (ANALYZE, BUFFERS)
|
|
255
|
-
SELECT * FROM document_embeddings
|
|
256
|
-
ORDER BY embedding <=> '[0.1,0.2,...]'::vector
|
|
277
|
+
EXPLAIN (ANALYZE, BUFFERS)
|
|
278
|
+
SELECT * FROM document_embeddings
|
|
279
|
+
ORDER BY embedding <=> '[0.1,0.2,...]'::vector
|
|
257
280
|
LIMIT 10;
|
|
258
281
|
```
|
|
259
282
|
|
|
@@ -268,7 +291,7 @@ texts.each_slice(10) do |batch|
|
|
|
268
291
|
batch.each do |text|
|
|
269
292
|
embedding = client.generate_embedding(text)
|
|
270
293
|
embeddings << embedding
|
|
271
|
-
|
|
294
|
+
|
|
272
295
|
# Small delay to avoid rate limiting
|
|
273
296
|
sleep(0.1)
|
|
274
297
|
end
|
|
@@ -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
|
|
@@ -295,13 +318,13 @@ Combine vector similarity with traditional text search:
|
|
|
295
318
|
```sql
|
|
296
319
|
WITH vector_results AS (
|
|
297
320
|
SELECT document_id, embedding <=> $1::vector as distance
|
|
298
|
-
FROM document_embeddings
|
|
299
|
-
ORDER BY embedding <=> $1::vector
|
|
321
|
+
FROM document_embeddings
|
|
322
|
+
ORDER BY embedding <=> $1::vector
|
|
300
323
|
LIMIT 20
|
|
301
324
|
),
|
|
302
325
|
text_results AS (
|
|
303
326
|
SELECT id as document_id, ts_rank(to_tsvector(content), plainto_tsquery($2)) as rank
|
|
304
|
-
FROM documents
|
|
327
|
+
FROM documents
|
|
305
328
|
WHERE to_tsvector(content) @@ plainto_tsquery($2)
|
|
306
329
|
)
|
|
307
330
|
SELECT d.title, d.content,
|
|
@@ -322,16 +345,17 @@ 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(
|
|
336
360
|
"INSERT INTO document_embeddings (document_id, embedding_provider, embedding_model, embedding_dimensions, embedding, embedding_text) VALUES ($1, $2, $3, $4, $5, $6)",
|
|
337
361
|
[document_id, provider[:name], provider[:model], provider[:dims], vector_str, text]
|
|
@@ -348,14 +372,14 @@ end
|
|
|
348
372
|
def track_search(query_text, results, provider, model)
|
|
349
373
|
query_embedding = client.generate_embedding(query_text)
|
|
350
374
|
query_vector = "[#{query_embedding.join(',')}]"
|
|
351
|
-
|
|
375
|
+
|
|
352
376
|
# Insert search query
|
|
353
377
|
query_result = db.exec_params(
|
|
354
378
|
"INSERT INTO search_queries (query_text, embedding_provider, embedding_model, query_embedding, result_count) VALUES ($1, $2, $3, $4, $5) RETURNING id",
|
|
355
379
|
[query_text, provider, model, query_vector, results.length]
|
|
356
380
|
)
|
|
357
381
|
query_id = query_result[0]['id']
|
|
358
|
-
|
|
382
|
+
|
|
359
383
|
# Insert query results
|
|
360
384
|
results.each_with_index do |result, index|
|
|
361
385
|
db.exec_params(
|
|
@@ -371,14 +395,14 @@ end
|
|
|
371
395
|
```sql
|
|
372
396
|
-- Popular search terms
|
|
373
397
|
SELECT query_text, COUNT(*) as search_count
|
|
374
|
-
FROM search_queries
|
|
398
|
+
FROM search_queries
|
|
375
399
|
WHERE created_at > NOW() - INTERVAL '7 days'
|
|
376
400
|
GROUP BY query_text
|
|
377
401
|
ORDER BY search_count DESC
|
|
378
402
|
LIMIT 10;
|
|
379
403
|
|
|
380
404
|
-- Average similarity scores
|
|
381
|
-
SELECT embedding_provider, embedding_model,
|
|
405
|
+
SELECT embedding_provider, embedding_model,
|
|
382
406
|
AVG(similarity_score) as avg_similarity,
|
|
383
407
|
COUNT(*) as result_count
|
|
384
408
|
FROM query_results qr
|
|
@@ -400,11 +424,12 @@ ORDER BY hour;
|
|
|
400
424
|
### Common Issues
|
|
401
425
|
|
|
402
426
|
**Slow queries:**
|
|
427
|
+
|
|
403
428
|
```sql
|
|
404
429
|
-- Check if indexes are being used
|
|
405
|
-
EXPLAIN (ANALYZE, BUFFERS)
|
|
406
|
-
SELECT * FROM document_embeddings
|
|
407
|
-
ORDER BY embedding <=> '[...]'::vector
|
|
430
|
+
EXPLAIN (ANALYZE, BUFFERS)
|
|
431
|
+
SELECT * FROM document_embeddings
|
|
432
|
+
ORDER BY embedding <=> '[...]'::vector
|
|
408
433
|
LIMIT 10;
|
|
409
434
|
|
|
410
435
|
-- Rebuild indexes if needed
|
|
@@ -412,10 +437,11 @@ REINDEX INDEX idx_document_embeddings_cosine;
|
|
|
412
437
|
```
|
|
413
438
|
|
|
414
439
|
**Memory issues:**
|
|
440
|
+
|
|
415
441
|
```sql
|
|
416
442
|
-- Check index sizes
|
|
417
443
|
SELECT schemaname, tablename, indexname, pg_size_pretty(pg_relation_size(indexrelid)) as size
|
|
418
|
-
FROM pg_stat_user_indexes
|
|
444
|
+
FROM pg_stat_user_indexes
|
|
419
445
|
WHERE tablename LIKE '%embedding%'
|
|
420
446
|
ORDER BY pg_relation_size(indexrelid) DESC;
|
|
421
447
|
|
|
@@ -424,6 +450,7 @@ SET work_mem = '256MB';
|
|
|
424
450
|
```
|
|
425
451
|
|
|
426
452
|
**Dimension mismatches:**
|
|
453
|
+
|
|
427
454
|
```ruby
|
|
428
455
|
# Validate embedding dimensions before storing
|
|
429
456
|
expected_dims = 768
|
|
@@ -447,4 +474,4 @@ end
|
|
|
447
474
|
- [pgvector Documentation](https://github.com/pgvector/pgvector)
|
|
448
475
|
- [HNSW Algorithm](https://arxiv.org/abs/1603.09320)
|
|
449
476
|
- [Vector Database Concepts](https://www.pinecone.io/learn/vector-database/)
|
|
450
|
-
- [Embedding Best Practices](https://platform.openai.com/docs/guides/embeddings/what-are-embeddings)
|
|
477
|
+
- [Embedding Best Practices](https://platform.openai.com/docs/guides/embeddings/what-are-embeddings)
|
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"
|