prescient 0.6.0 → 0.8.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 +21 -268
- data/CHANGELOG.md +59 -0
- data/INTEGRATION_GUIDE.md +19 -1
- data/README.md +369 -21
- data/Steepfile +12 -12
- data/db/migrate/001_create_prescient_tables.rb +15 -16
- data/docker-compose.yml +22 -0
- data/examples/README.md +36 -1
- data/examples/custom_contexts.rb +4 -4
- data/examples/web_search.rb +34 -0
- data/exe/prescient +2 -2
- data/exe/prescient-mcp +7 -0
- data/lib/prescient/agent/audit_log.rb +37 -0
- data/lib/prescient/agent/cli_adapter.rb +29 -0
- data/lib/prescient/agent/configuration.rb +57 -0
- data/lib/prescient/agent/context.rb +56 -0
- data/lib/prescient/agent/error_serializer.rb +47 -0
- data/lib/prescient/agent/errors.rb +25 -0
- data/lib/prescient/agent/parser.rb +49 -0
- data/lib/prescient/agent/prompt_builder.rb +31 -0
- data/lib/prescient/agent/result.rb +36 -0
- data/lib/prescient/agent/runtime.rb +175 -0
- data/lib/prescient/agent/schema_validator.rb +215 -0
- data/lib/prescient/agent/tool_registry.rb +89 -0
- data/lib/prescient/agent.rb +22 -0
- data/lib/prescient/api.rb +346 -231
- data/lib/prescient/base.rb +370 -372
- data/lib/prescient/cli.rb +591 -402
- data/lib/prescient/client.rb +31 -4
- data/lib/prescient/configuration_loader.rb +511 -336
- data/lib/prescient/document_source.rb +114 -0
- data/lib/prescient/errors.rb +13 -3
- data/lib/prescient/mcp/authentication.rb +39 -0
- data/lib/prescient/mcp/configuration.rb +38 -0
- data/lib/prescient/mcp/rack.rb +243 -0
- data/lib/prescient/mcp/server.rb +202 -0
- data/lib/prescient/mcp/stdio.rb +42 -0
- data/lib/prescient/mcp.rb +8 -0
- data/lib/prescient/pgvector.rb +193 -189
- data/lib/prescient/provider/anthropic.rb +129 -125
- data/lib/prescient/provider/deepseek.rb +122 -118
- data/lib/prescient/provider/gemini.rb +153 -149
- data/lib/prescient/provider/huggingface.rb +191 -187
- data/lib/prescient/provider/mistral.rb +151 -147
- data/lib/prescient/provider/ollama.rb +168 -165
- data/lib/prescient/provider/openai.rb +174 -171
- data/lib/prescient/provider/xai.rb +122 -118
- data/lib/prescient/tool/search_api.rb +130 -0
- data/lib/prescient/tool/searxng.rb +128 -0
- data/lib/prescient/tool.rb +125 -0
- data/lib/prescient/version.rb +1 -1
- data/lib/prescient.rb +129 -55
- data/schema/prescient.configuration.schema.json +119 -0
- data/searxng/settings.yml +18 -0
- data/sig/prescient.rbs +228 -1
- metadata +33 -5
data/README.md
CHANGED
|
@@ -2,25 +2,116 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://rubygems.org/gems/prescient)
|
|
4
4
|
[](https://www.ruby-lang.org/)
|
|
5
|
+
[](https://github.com/kanutocd/prescient/pkgs/container/prescient)
|
|
5
6
|
[](https://github.com/kanutocd/prescient/actions/workflows/ci.yml)
|
|
6
7
|
[](https://github.com/kanutocd/prescient/actions/workflows/security.yml)
|
|
7
8
|
[](LICENSE.txt)
|
|
8
9
|
|
|
9
|
-
Prescient is a boring AI provider
|
|
10
|
+
Prescient is a boring AI provider gateway implemented in Ruby. Configure your AI providers once, then use them through a consistent Ruby API, CLI, or REST API. Prescient handles provider selection, retries, health checks, and fallback across configured providers, including OpenAI, Anthropic, Ollama, Hugging Face, Google Gemini, Mistral, DeepSeek, and xAI.
|
|
10
11
|
|
|
11
12
|
For focused guidance, see the **[examples guide](https://github.com/kanutocd/prescient/tree/main/examples)**,
|
|
12
13
|
**[Rails integration guide](https://github.com/kanutocd/prescient/blob/main/INTEGRATION_GUIDE.md)**, and
|
|
13
14
|
**[pgvector guide](https://github.com/kanutocd/prescient/blob/main/VECTOR_SEARCH_GUIDE.md)**.
|
|
14
15
|
|
|
16
|
+
Prescient has three primary application entry points:
|
|
17
|
+
|
|
18
|
+
- **Ruby gem** — Use `Prescient` directly from a Ruby application.
|
|
19
|
+
- **CLI** — Run provider operations and configuration checks from scripts or a
|
|
20
|
+
terminal.
|
|
21
|
+
- **Rack-compatible REST API** — Mount `Prescient::API` in a Rack, Rails, or
|
|
22
|
+
other Rack-compatible application.
|
|
23
|
+
|
|
24
|
+
The Ruby entry point does not load the CLI, REST API, or MCP transport. Those
|
|
25
|
+
surfaces are loaded lazily when referenced or explicitly required.
|
|
26
|
+
MCP is an optional protocol integration surface available through explicit
|
|
27
|
+
`prescient/mcp` loading; it does not change the three primary entry points.
|
|
28
|
+
|
|
29
|
+
## Contents
|
|
30
|
+
|
|
31
|
+
- [Quick Start](#quick-start)
|
|
32
|
+
- [Features](#features)
|
|
33
|
+
- [Supported Providers](#supported-providers)
|
|
34
|
+
- [Installation](#installation)
|
|
35
|
+
- [Command-Line Interface](#command-line-interface)
|
|
36
|
+
- [REST API](#rest-api)
|
|
37
|
+
- [Configuration](#configuration)
|
|
38
|
+
- [Ruby API](#ruby-api)
|
|
39
|
+
- [Agent runtime](#agent-runtime)
|
|
40
|
+
- [Custom Prompt Templates](#custom-prompt-templates)
|
|
41
|
+
- [Custom Context Configurations](#custom-context-configurations)
|
|
42
|
+
- [Vector Database Integration](#vector-database-integration-pgvector)
|
|
43
|
+
- [Testing](#testing)
|
|
44
|
+
- [Development](#development)
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
Prescient reads provider credentials from the environment. The following
|
|
49
|
+
example uses OpenAI; replace it with a configured provider when appropriate.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
export OPENAI_API_KEY=your_api_key
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Create `example.rb`:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
require "prescient"
|
|
59
|
+
|
|
60
|
+
Prescient.configure do |config|
|
|
61
|
+
config.default_provider = :openai
|
|
62
|
+
config.add_provider(
|
|
63
|
+
:openai,
|
|
64
|
+
Prescient::Provider::OpenAI,
|
|
65
|
+
api_key: ENV.fetch("OPENAI_API_KEY"),
|
|
66
|
+
chat_model: ENV.fetch("OPENAI_CHAT_MODEL", "gpt-4.1-mini")
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
result = Prescient.generate_response(
|
|
71
|
+
"Draft a concise maintenance notice for a scheduled database upgrade."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
puts result[:response]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Run it with:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
bundle exec ruby example.rb
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`generate_response` returns a normalized hash containing the generated
|
|
84
|
+
response, provider, model, and provider metadata. Use the client directly when
|
|
85
|
+
you need repeated operations or provider-specific fallback control:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
client = Prescient.client(:openai)
|
|
89
|
+
response = client.generate_response("Summarize this release note.")
|
|
90
|
+
embedding = client.generate_embedding("A searchable release note.")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
For local development, configure Ollama instead of a hosted provider:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
docker compose up -d ollama
|
|
97
|
+
docker compose run --rm ollama-init
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
See the [examples guide](examples/README.md) for complete scripts and the
|
|
101
|
+
[Configuration](#configuration) section for YAML, environment references,
|
|
102
|
+
fallbacks, prompt templates, and external tools.
|
|
103
|
+
|
|
15
104
|
## Features
|
|
16
105
|
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
106
|
+
- **Provider abstraction** — One consistent interface across supported AI providers
|
|
107
|
+
- **Multiple interfaces** — Ruby API, CLI, and Rack-compatible REST API
|
|
108
|
+
- **Text and embeddings** — Generate responses and embeddings with provider/model selection
|
|
109
|
+
- **Reliability controls** — Retries, health checks, and fallback across configured providers
|
|
110
|
+
- **Declarative configuration** — Versioned YAML, environment references, and JSON Schema validation
|
|
111
|
+
- **Prompt and context customization** — Configurable prompt templates and context formatting
|
|
112
|
+
- **External tools** — Explicit, normalized web-search integration with SearXNG and SearchApi
|
|
113
|
+
- **Local and cloud support** — Ollama alongside hosted providers
|
|
114
|
+
- **Optional integrations** — Docker deployment and pgvector support without making either mandatory
|
|
24
115
|
|
|
25
116
|
## Supported Providers
|
|
26
117
|
|
|
@@ -103,6 +194,8 @@ prescient config validate
|
|
|
103
194
|
prescient config example
|
|
104
195
|
prescient generate "Explain Ruby Ractors"
|
|
105
196
|
prescient embed "Ruby is a programming language"
|
|
197
|
+
prescient search "Ruby HTTP clients"
|
|
198
|
+
prescient agent "Summarize the account status" --tool accounts
|
|
106
199
|
```
|
|
107
200
|
|
|
108
201
|
Supported options include:
|
|
@@ -122,8 +215,15 @@ Supported options include:
|
|
|
122
215
|
--api-key KEY Use an API key for the operation
|
|
123
216
|
--api-key-env NAME Read the API key from an environment variable
|
|
124
217
|
--format FORMAT Select text or json output
|
|
218
|
+
--json-file PATH Load JSON documents as generation context
|
|
219
|
+
--no-fallback Disable provider fallback
|
|
125
220
|
```
|
|
126
221
|
|
|
222
|
+
Search-specific options are `--tool NAME`, `--generate`, and `--limit COUNT`.
|
|
223
|
+
Agent-specific options are repeatable `--tool NAME` and `--max-loops COUNT`.
|
|
224
|
+
The full command-specific help is available with `prescient search --help` or
|
|
225
|
+
`prescient agent --help`.
|
|
226
|
+
|
|
127
227
|
Use `--api-key-env` to source credentials from an environment variable. The
|
|
128
228
|
direct `--api-key` option is available for ephemeral automation but may be
|
|
129
229
|
visible in shell history or process listings. Use `--format json` for
|
|
@@ -242,6 +342,9 @@ run Prescient::API.new(
|
|
|
242
342
|
Available endpoints include:
|
|
243
343
|
|
|
244
344
|
- **`POST /v1/generate`**
|
|
345
|
+
- **`POST /v1/search`**
|
|
346
|
+
- **`POST /v1/search/generate`**
|
|
347
|
+
- **`POST /v1/agent`**
|
|
245
348
|
- **`POST /v1/embeddings`**
|
|
246
349
|
- **`POST /v1/embeddings/batch`**
|
|
247
350
|
- **`GET /v1/providers`**
|
|
@@ -347,25 +450,139 @@ YAML values, then built-in defaults. Use `prescient config validate` to check a
|
|
|
347
450
|
configuration before running an operation, or `prescient config example` to
|
|
348
451
|
generate an annotated starter file.
|
|
349
452
|
|
|
350
|
-
|
|
351
|
-
|
|
453
|
+
### External Tools
|
|
454
|
+
|
|
455
|
+
External tools are opt-in capability adapters, separate from AI providers.
|
|
456
|
+
Supported web-search adapters are [SearXNG](https://searxng.org/) and
|
|
457
|
+
[SearchApi](https://www.searchapi.io/):
|
|
458
|
+
|
|
459
|
+
#### SearXNG
|
|
460
|
+
|
|
461
|
+
Setting `SEARXNG_URL` registers the default `web_search` tool for CLI and Ruby
|
|
462
|
+
environment-based configuration. YAML or programmatic configuration can be
|
|
463
|
+
used when more control is needed.
|
|
352
464
|
|
|
353
465
|
```yaml
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
type:
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
with_context_template: "%{system_prompt}\n\nContext:\n%{context}\n\nUser: %{query}"
|
|
466
|
+
tools:
|
|
467
|
+
web_search:
|
|
468
|
+
type: searxng
|
|
469
|
+
url_env: SEARXNG_URL
|
|
470
|
+
language: en
|
|
471
|
+
categories: [general, news]
|
|
472
|
+
timeout: 5
|
|
473
|
+
max_results: 5
|
|
363
474
|
```
|
|
364
475
|
|
|
476
|
+
Run the SearXNG-backed tool:
|
|
477
|
+
|
|
365
478
|
```bash
|
|
366
|
-
|
|
479
|
+
docker compose up -d searxng
|
|
480
|
+
SEARXNG_URL=http://localhost:8080 bundle exec prescient search \
|
|
481
|
+
--format json "Ruby HTTP clients"
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
#### SearchApi
|
|
485
|
+
|
|
486
|
+
[SearchApi](https://www.searchapi.io/) uses a hosted Google search engine and
|
|
487
|
+
requires an API key:
|
|
488
|
+
|
|
489
|
+
```yaml
|
|
490
|
+
tools:
|
|
491
|
+
web_search:
|
|
492
|
+
type: searchapi
|
|
493
|
+
api_key_env: SEARCHAPI_API_KEY
|
|
494
|
+
engine: google
|
|
495
|
+
location: New York
|
|
496
|
+
hl: en
|
|
497
|
+
gl: us
|
|
498
|
+
timeout: 10
|
|
499
|
+
max_results: 5
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
The `engine` value can select another SearchApi web or product engine when its
|
|
503
|
+
response uses `organic_results`, such as `bing`, `yahoo`, `yandex`,
|
|
504
|
+
`amazon_search`, or `walmart_search`.
|
|
505
|
+
|
|
506
|
+
Adapters can be grouped under one logical capability for ordered fallback:
|
|
507
|
+
|
|
508
|
+
```yaml
|
|
509
|
+
tools:
|
|
510
|
+
web_search:
|
|
511
|
+
adapters:
|
|
512
|
+
- type: searxng
|
|
513
|
+
url_env: SEARXNG_URL
|
|
514
|
+
- type: searchapi
|
|
515
|
+
api_key_env: SEARCHAPI_API_KEY
|
|
516
|
+
engine: google
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
Adapters are tried in order. Fallback is limited to transient connection and
|
|
520
|
+
rate-limit failures; invalid configuration, authentication failures, and
|
|
521
|
+
malformed responses are not retried with another adapter.
|
|
522
|
+
|
|
523
|
+
Run the SearchApi-backed tool:
|
|
524
|
+
|
|
525
|
+
```bash
|
|
526
|
+
SEARCHAPI_API_KEY=your-key bundle exec prescient search \
|
|
527
|
+
--config prescient.yml --format json "Ruby HTTP clients"
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
Invoke a configured tool explicitly from Ruby or the CLI:
|
|
531
|
+
|
|
532
|
+
```ruby
|
|
533
|
+
result = Prescient.tool(:web_search).search('Ruby HTTP clients', limit: 3)
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
```bash
|
|
537
|
+
bundle exec prescient search --config prescient.yml \
|
|
538
|
+
--tool web_search --format json "Ruby HTTP clients"
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
Results use a normalized envelope containing `tool`, `query`, `source`, and
|
|
542
|
+
`results` entries with `title`, `url`, `snippet`, and `source`. Requests have
|
|
543
|
+
bounded query length, timeout, result count, and response size. Tool execution
|
|
544
|
+
is explicit; Prescient does not autonomously invoke tools, and tool endpoints
|
|
545
|
+
are not exposed as raw tool endpoints through `Prescient::API`; the API exposes
|
|
546
|
+
the explicit combined search-and-generation operation below. Other adapters can
|
|
547
|
+
implement the same contract without changing provider integrations.
|
|
548
|
+
|
|
549
|
+
Search results are not sent to an AI provider by default. Opt in when you want
|
|
550
|
+
the normalized results assembled as generation context:
|
|
551
|
+
|
|
552
|
+
```ruby
|
|
553
|
+
response = Prescient.search_and_generate(
|
|
554
|
+
'Ruby HTTP clients',
|
|
555
|
+
tool: :web_search,
|
|
556
|
+
provider: :openai,
|
|
557
|
+
)
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
The CLI exposes the same opt-in behavior with `--generate`:
|
|
561
|
+
|
|
562
|
+
```bash
|
|
563
|
+
prescient search --generate --provider openai "Ruby HTTP clients"
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
The REST API exposes the same opt-in behavior:
|
|
567
|
+
|
|
568
|
+
Raw normalized search results are available without generation:
|
|
569
|
+
|
|
570
|
+
```bash
|
|
571
|
+
curl -X POST http://localhost:9292/v1/search \
|
|
572
|
+
-H 'Content-Type: application/json' \
|
|
573
|
+
-d '{"query":"Ruby HTTP clients","limit":5}'
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
```bash
|
|
577
|
+
curl -X POST http://localhost:9292/v1/search/generate \
|
|
578
|
+
-H 'Content-Type: application/json' \
|
|
579
|
+
-d '{"query":"Ruby HTTP clients","provider":"openai","limit":5}'
|
|
367
580
|
```
|
|
368
581
|
|
|
582
|
+
Use `fallback: false` to disable provider fallback for the request. The
|
|
583
|
+
response is the normalized AI provider response; omit this endpoint and use
|
|
584
|
+
`POST /v1/generate` when search context is not wanted.
|
|
585
|
+
|
|
369
586
|
### Programmatic Configuration
|
|
370
587
|
|
|
371
588
|
```ruby
|
|
@@ -465,6 +682,117 @@ client_no_fallback = Prescient::Client.new(:primary, enable_fallback: false)
|
|
|
465
682
|
response = Prescient.generate_response("Hello", provider: :primary, enable_fallback: true)
|
|
466
683
|
```
|
|
467
684
|
|
|
685
|
+
### JSON document context
|
|
686
|
+
|
|
687
|
+
For small documentation sets, you can load JSON objects from a file instead of
|
|
688
|
+
creating embeddings:
|
|
689
|
+
|
|
690
|
+
```ruby
|
|
691
|
+
documents = Prescient::DocumentSource::JsonFile.new(path: "docs.json").fetch
|
|
692
|
+
response = Prescient.generate_response("How do I reset my password?", documents)
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
The CLI supports the same flow with bounded document loading:
|
|
696
|
+
|
|
697
|
+
```bash
|
|
698
|
+
prescient generate --json-file docs.json "How do I reset my password?"
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
Redis-backed documents use an injected Redis-compatible client, so Redis remains
|
|
702
|
+
optional for gem consumers:
|
|
703
|
+
|
|
704
|
+
```ruby
|
|
705
|
+
source = Prescient::DocumentSource::RedisJson.new(client: redis, key: "docs:acme")
|
|
706
|
+
response = Prescient.generate_response("Summarize the docs", source.fetch)
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
The REST API accepts the same documents inline as `documents` on
|
|
710
|
+
`POST /v1/generate`. Sources enforce document-count and serialized-byte limits;
|
|
711
|
+
use retrieval or embeddings when the documentation no longer fits comfortably
|
|
712
|
+
in the provider's context window.
|
|
713
|
+
|
|
714
|
+
## Agent runtime
|
|
715
|
+
|
|
716
|
+
Prescient also provides an optional, bounded agent runtime. It is loaded
|
|
717
|
+
explicitly and can execute allowlisted search tools through the existing
|
|
718
|
+
provider routing:
|
|
719
|
+
|
|
720
|
+
```ruby
|
|
721
|
+
require "prescient/agent"
|
|
722
|
+
|
|
723
|
+
agent = Prescient::Agent::Runtime.new(
|
|
724
|
+
provider: :openai,
|
|
725
|
+
tool_names: [:web_search],
|
|
726
|
+
configuration: Prescient::Agent::Configuration.new(max_loops: 5)
|
|
727
|
+
)
|
|
728
|
+
result = agent.run("Find and summarize the latest Ruby release")
|
|
729
|
+
puts result.response
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
The runtime supports one validated JSON action per iteration and stops at its
|
|
733
|
+
loop limit. It does not provide arbitrary Ruby, shell, filesystem, browser, or
|
|
734
|
+
database access. The agent namespace is not loaded by `require "prescient"`.
|
|
735
|
+
|
|
736
|
+
The same bounded runtime is available from the CLI:
|
|
737
|
+
|
|
738
|
+
```bash
|
|
739
|
+
prescient agent "Summarize the account status" --provider openai --tool accounts
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
It is also available through `POST /v1/agent`. Supply an explicit `tools` array;
|
|
743
|
+
the REST API does not grant access to configured tools implicitly:
|
|
744
|
+
|
|
745
|
+
```json
|
|
746
|
+
{"prompt":"Summarize the account status","provider":"openai","tools":[]}
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
Applications can provide `authorization` and `request_context` hooks to enforce
|
|
750
|
+
tenant/principal policy, and a bounded `telemetry` hook receives only event
|
|
751
|
+
metadata such as loop count, action names, and success status. The authorization
|
|
752
|
+
hook receives `tool:`, copied `arguments:`, and copied request-scoped
|
|
753
|
+
`context:`; only an exact `true` result permits invocation. Prescient does not
|
|
754
|
+
guess roles, ownership, or organization boundaries, so the host application
|
|
755
|
+
must make the access decision.
|
|
756
|
+
|
|
757
|
+
For durable, privacy-preserving audit records, pass an opt-in JSONL sink:
|
|
758
|
+
|
|
759
|
+
```ruby
|
|
760
|
+
audit_log = Prescient::Agent::AuditLog.new(path: "tmp/prescient-agent.jsonl")
|
|
761
|
+
Prescient::Agent::Runtime.new(audit_log:).run("Summarize account status")
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
The sink persists event metadata and timestamps, never prompts, documents, or
|
|
765
|
+
raw observations. Audit storage failures are isolated from agent execution;
|
|
766
|
+
configure retention, access controls, and storage protection in the host.
|
|
767
|
+
|
|
768
|
+
For MCP hosts, load the optional dependency-free adapter explicitly with
|
|
769
|
+
`require "prescient/mcp"`, or run `prescient-mcp` for newline-delimited
|
|
770
|
+
JSON-RPC over stdio. MCP exposes only explicitly enabled capabilities and never
|
|
771
|
+
returns credentials or raw provider failure bodies.
|
|
772
|
+
|
|
773
|
+
The optional Rack adapter provides a bounded Streamable HTTP surface with
|
|
774
|
+
bearer-token authentication, Origin allowlisting, initialize-assigned sessions,
|
|
775
|
+
`Mcp-Session-Id` lifecycle management, notifications, and one-shot JSON or
|
|
776
|
+
server-sent-event responses:
|
|
777
|
+
|
|
778
|
+
```ruby
|
|
779
|
+
require "prescient/mcp"
|
|
780
|
+
|
|
781
|
+
app = Prescient::MCP::Rack.new(
|
|
782
|
+
authentication: Prescient::MCP::Authentication::BearerToken.new(
|
|
783
|
+
token: ENV.fetch("MCP_TOKEN"),
|
|
784
|
+
principal: { id: "mcp-client" }
|
|
785
|
+
),
|
|
786
|
+
allowed_origins: ["https://admin.example.com"]
|
|
787
|
+
)
|
|
788
|
+
run app
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
Clients must send `MCP-Protocol-Version: 2025-06-18` and the returned
|
|
792
|
+
`Mcp-Session-Id` on requests after `initialize`. `DELETE` terminates a session.
|
|
793
|
+
The adapter does not implement a long-lived notification queue or replace a
|
|
794
|
+
host application's token provisioning and authorization policy.
|
|
795
|
+
|
|
468
796
|
**Fallback Behavior:**
|
|
469
797
|
- When a provider fails with a persistent error, Prescient automatically tries the next available provider
|
|
470
798
|
- Configured fallback providers are tried in order; the provider operation determines availability
|
|
@@ -473,7 +801,7 @@ response = Prescient.generate_response("Hello", provider: :primary, enable_fallb
|
|
|
473
801
|
- Provider-service failures, connection failures, rate limits, and unavailable models may trigger fallback; authentication and invalid-request errors are returned to the caller
|
|
474
802
|
- The fallback process preserves all method arguments and options
|
|
475
803
|
|
|
476
|
-
##
|
|
804
|
+
## Ruby API
|
|
477
805
|
|
|
478
806
|
### Quick Start
|
|
479
807
|
|
|
@@ -602,6 +930,24 @@ client = Prescient.client(:customer_service)
|
|
|
602
930
|
response = client.generate_response("What's your return policy?")
|
|
603
931
|
```
|
|
604
932
|
|
|
933
|
+
Templates can also be configured in YAML or overridden for one CLI operation:
|
|
934
|
+
|
|
935
|
+
```yaml
|
|
936
|
+
providers:
|
|
937
|
+
openai:
|
|
938
|
+
type: openai
|
|
939
|
+
api_key_env: OPENAI_API_KEY
|
|
940
|
+
chat_model: gpt-4.1-mini
|
|
941
|
+
prompt_templates:
|
|
942
|
+
system_prompt: You are a concise assistant.
|
|
943
|
+
no_context_template: "%{system_prompt}\n\nUser: %{query}"
|
|
944
|
+
with_context_template: "%{system_prompt}\n\nContext:\n%{context}\n\nUser: %{query}"
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
```bash
|
|
948
|
+
prescient generate --prompt-templates-file prompts.yml "Summarize this"
|
|
949
|
+
```
|
|
950
|
+
|
|
605
951
|
### Template Placeholders
|
|
606
952
|
|
|
607
953
|
- `%{system_prompt}` - The system/role instruction
|
|
@@ -1132,6 +1478,8 @@ The included `docker-compose.yml` provides:
|
|
|
1132
1478
|
|
|
1133
1479
|
- **ollama**: Ollama AI service with persistent model storage
|
|
1134
1480
|
- **ollama-init**: Automatically pulls required models on startup
|
|
1481
|
+
- **searxng**: Optional SearXNG web-search service with JSON output enabled
|
|
1482
|
+
- **postgres**: Optional PostgreSQL database with pgvector support
|
|
1135
1483
|
- **redis**: Optional caching layer for embeddings
|
|
1136
1484
|
- **prescient-app**: Example Ruby application container
|
|
1137
1485
|
|
data/Steepfile
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
target :prescient do
|
|
4
|
-
signature
|
|
5
|
-
library
|
|
6
|
-
library
|
|
4
|
+
signature "sig"
|
|
5
|
+
library "json"
|
|
6
|
+
library "net-http"
|
|
7
7
|
|
|
8
|
-
check
|
|
9
|
-
check
|
|
10
|
-
check
|
|
11
|
-
check
|
|
12
|
-
check
|
|
13
|
-
check
|
|
14
|
-
check
|
|
15
|
-
check
|
|
16
|
-
check
|
|
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
17
|
end
|
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
# Rails migration for Prescient gem vector database tables
|
|
4
4
|
# Copy this file to your Rails db/migrate directory and adjust the timestamp
|
|
5
|
-
|
|
6
5
|
class CreatePrescientTables < ActiveRecord::Migration[7.0]
|
|
7
6
|
def up
|
|
8
7
|
# Enable pgvector extension
|
|
9
|
-
enable_extension
|
|
8
|
+
enable_extension "vector"
|
|
10
9
|
|
|
11
10
|
# Documents table to store original content
|
|
12
11
|
create_table :documents do |t|
|
|
@@ -76,14 +75,14 @@ class CreatePrescientTables < ActiveRecord::Migration[7.0]
|
|
|
76
75
|
add_index :documents, :metadata, using: :gin
|
|
77
76
|
|
|
78
77
|
add_index :document_embeddings, :document_id
|
|
79
|
-
add_index :document_embeddings, [
|
|
78
|
+
add_index :document_embeddings, %i[embedding_provider embedding_model], name: "idx_doc_embeddings_provider_model"
|
|
80
79
|
add_index :document_embeddings, :embedding_dimensions
|
|
81
80
|
|
|
82
|
-
add_index :document_chunks, [
|
|
81
|
+
add_index :document_chunks, %i[document_id chunk_index], unique: true
|
|
83
82
|
|
|
84
83
|
add_index :chunk_embeddings, :chunk_id
|
|
85
84
|
add_index :chunk_embeddings, :document_id
|
|
86
|
-
add_index :chunk_embeddings, [
|
|
85
|
+
add_index :chunk_embeddings, %i[embedding_provider embedding_model], name: "idx_chunk_embeddings_provider_model"
|
|
87
86
|
|
|
88
87
|
add_index :search_queries, :created_at
|
|
89
88
|
add_index :query_results, :query_id
|
|
@@ -94,30 +93,30 @@ class CreatePrescientTables < ActiveRecord::Migration[7.0]
|
|
|
94
93
|
|
|
95
94
|
# Vector indexes for document embeddings
|
|
96
95
|
execute <<-SQL
|
|
97
|
-
CREATE INDEX idx_document_embeddings_cosine#{
|
|
98
|
-
ON document_embeddings#{
|
|
96
|
+
CREATE INDEX idx_document_embeddings_cosine#{" "}
|
|
97
|
+
ON document_embeddings#{" "}
|
|
99
98
|
USING hnsw (embedding vector_cosine_ops)
|
|
100
99
|
WITH (m = 16, ef_construction = 64);
|
|
101
100
|
SQL
|
|
102
101
|
|
|
103
102
|
execute <<-SQL
|
|
104
|
-
CREATE INDEX idx_document_embeddings_l2#{
|
|
105
|
-
ON document_embeddings#{
|
|
103
|
+
CREATE INDEX idx_document_embeddings_l2#{" "}
|
|
104
|
+
ON document_embeddings#{" "}
|
|
106
105
|
USING hnsw (embedding vector_l2_ops)
|
|
107
106
|
WITH (m = 16, ef_construction = 64);
|
|
108
107
|
SQL
|
|
109
108
|
|
|
110
109
|
# Vector indexes for chunk embeddings
|
|
111
110
|
execute <<-SQL
|
|
112
|
-
CREATE INDEX idx_chunk_embeddings_cosine#{
|
|
113
|
-
ON chunk_embeddings#{
|
|
111
|
+
CREATE INDEX idx_chunk_embeddings_cosine#{" "}
|
|
112
|
+
ON chunk_embeddings#{" "}
|
|
114
113
|
USING hnsw (embedding vector_cosine_ops)
|
|
115
114
|
WITH (m = 16, ef_construction = 64);
|
|
116
115
|
SQL
|
|
117
116
|
|
|
118
117
|
execute <<-SQL
|
|
119
|
-
CREATE INDEX idx_chunk_embeddings_l2#{
|
|
120
|
-
ON chunk_embeddings#{
|
|
118
|
+
CREATE INDEX idx_chunk_embeddings_l2#{" "}
|
|
119
|
+
ON chunk_embeddings#{" "}
|
|
121
120
|
USING hnsw (embedding vector_l2_ops)
|
|
122
121
|
WITH (m = 16, ef_construction = 64);
|
|
123
122
|
SQL
|
|
@@ -150,9 +149,9 @@ class CreatePrescientTables < ActiveRecord::Migration[7.0]
|
|
|
150
149
|
drop_table :document_embeddings
|
|
151
150
|
drop_table :documents
|
|
152
151
|
|
|
153
|
-
execute
|
|
154
|
-
execute
|
|
152
|
+
execute "DROP FUNCTION IF EXISTS cosine_similarity(vector, vector);"
|
|
153
|
+
execute "DROP FUNCTION IF EXISTS euclidean_distance(vector, vector);"
|
|
155
154
|
|
|
156
|
-
disable_extension
|
|
155
|
+
disable_extension "vector"
|
|
157
156
|
end
|
|
158
157
|
end
|
data/docker-compose.yml
CHANGED
|
@@ -91,6 +91,26 @@ services:
|
|
|
91
91
|
retries: 3
|
|
92
92
|
start_period: 30s
|
|
93
93
|
|
|
94
|
+
# Optional SearXNG instance for the external web-search example.
|
|
95
|
+
searxng:
|
|
96
|
+
image: searxng/searxng:latest
|
|
97
|
+
container_name: prescient-searxng
|
|
98
|
+
ports:
|
|
99
|
+
- "8080:8080"
|
|
100
|
+
environment:
|
|
101
|
+
- SEARXNG_BASE_URL=http://localhost:8080/
|
|
102
|
+
- SEARXNG_SECRET=${SEARXNG_SECRET:-prescient-development-secret}
|
|
103
|
+
volumes:
|
|
104
|
+
- ./searxng/settings.yml:/etc/searxng/settings.yml:ro
|
|
105
|
+
- searxng_cache:/var/cache/searxng
|
|
106
|
+
restart: unless-stopped
|
|
107
|
+
healthcheck:
|
|
108
|
+
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/')"]
|
|
109
|
+
interval: 30s
|
|
110
|
+
timeout: 10s
|
|
111
|
+
retries: 3
|
|
112
|
+
start_period: 30s
|
|
113
|
+
|
|
94
114
|
# Optional: Redis for caching embeddings (useful for development)
|
|
95
115
|
redis:
|
|
96
116
|
image: redis:7-alpine
|
|
@@ -145,6 +165,8 @@ volumes:
|
|
|
145
165
|
driver: local
|
|
146
166
|
redis_data:
|
|
147
167
|
driver: local
|
|
168
|
+
searxng_cache:
|
|
169
|
+
driver: local
|
|
148
170
|
|
|
149
171
|
networks:
|
|
150
172
|
default:
|
data/examples/README.md
CHANGED
|
@@ -18,7 +18,12 @@ bundle install
|
|
|
18
18
|
- `vector_search.rb` — `Prescient::Pgvector::Store` PostgreSQL/pgvector storage
|
|
19
19
|
and similarity search.
|
|
20
20
|
- `rest_api.ru` — a tiny Rack-compatible application that mounts
|
|
21
|
-
`Prescient::API` and lists its endpoints at
|
|
21
|
+
`Prescient::API` and lists its endpoints at `/`, including the bounded agent
|
|
22
|
+
and search-generation routes.
|
|
23
|
+
- `web_search.rb` — explicit SearXNG tool invocation with normalized JSON output.
|
|
24
|
+
|
|
25
|
+
The same `web_search` capability can use SearchApi instead of SearXNG when the
|
|
26
|
+
tool is configured with `type: searchapi` and `SEARCHAPI_API_KEY`.
|
|
22
27
|
|
|
23
28
|
Run the REST API example with a Rack server such as `rackup`:
|
|
24
29
|
|
|
@@ -29,6 +34,17 @@ PRESCIENT_API_TOKEN=change-me BUNDLE_WITH=rack_example \
|
|
|
29
34
|
curl http://localhost:9292/
|
|
30
35
|
```
|
|
31
36
|
|
|
37
|
+
The endpoint catalog includes `POST /v1/search/generate`. With a configured
|
|
38
|
+
SearXNG tool and AI provider, call it explicitly to feed search results into
|
|
39
|
+
generation:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
curl -X POST http://localhost:9292/v1/search/generate \
|
|
43
|
+
-H 'Authorization: Bearer change-me' \
|
|
44
|
+
-H 'Content-Type: application/json' \
|
|
45
|
+
-d '{"query":"Ruby HTTP clients","provider":"openai","limit":5}'
|
|
46
|
+
```
|
|
47
|
+
|
|
32
48
|
Running `bundle exec ruby examples/rest_api.ru` directly prints the same
|
|
33
49
|
endpoint catalog without starting a server.
|
|
34
50
|
|
|
@@ -57,6 +73,25 @@ configuration. The scripts are demonstrations rather than isolated test
|
|
|
57
73
|
fixtures; they may make real provider requests when the configured service is
|
|
58
74
|
available.
|
|
59
75
|
|
|
76
|
+
The web-search example requires a reachable SearXNG instance:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
docker compose up -d searxng
|
|
80
|
+
SEARXNG_URL=http://localhost:8080 bundle exec ruby examples/web_search.rb "Ruby HTTP clients"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The example returns normalized search results directly by default. Opt in to
|
|
84
|
+
feeding those results to the configured AI provider with `--generate`:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
SEARXNG_URL=http://localhost:8080 PRESCIENT_PROVIDER=openai \
|
|
88
|
+
bundle exec ruby examples/web_search.rb --generate "Ruby HTTP clients"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Omit `--generate` to keep the search results direct. `PRESCIENT_PROVIDER` is
|
|
92
|
+
only used with `--generate` and may be omitted when the default provider is
|
|
93
|
+
configured.
|
|
94
|
+
|
|
60
95
|
See the [main README](../README.md) for configuration, fallback behavior,
|
|
61
96
|
prompt templates, context exclusions, embeddings, and the public API. Rails
|
|
62
97
|
applications can also use the [integration guide](../INTEGRATION_GUIDE.md),
|