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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +21 -268
  3. data/CHANGELOG.md +59 -0
  4. data/INTEGRATION_GUIDE.md +19 -1
  5. data/README.md +369 -21
  6. data/Steepfile +12 -12
  7. data/db/migrate/001_create_prescient_tables.rb +15 -16
  8. data/docker-compose.yml +22 -0
  9. data/examples/README.md +36 -1
  10. data/examples/custom_contexts.rb +4 -4
  11. data/examples/web_search.rb +34 -0
  12. data/exe/prescient +2 -2
  13. data/exe/prescient-mcp +7 -0
  14. data/lib/prescient/agent/audit_log.rb +37 -0
  15. data/lib/prescient/agent/cli_adapter.rb +29 -0
  16. data/lib/prescient/agent/configuration.rb +57 -0
  17. data/lib/prescient/agent/context.rb +56 -0
  18. data/lib/prescient/agent/error_serializer.rb +47 -0
  19. data/lib/prescient/agent/errors.rb +25 -0
  20. data/lib/prescient/agent/parser.rb +49 -0
  21. data/lib/prescient/agent/prompt_builder.rb +31 -0
  22. data/lib/prescient/agent/result.rb +36 -0
  23. data/lib/prescient/agent/runtime.rb +175 -0
  24. data/lib/prescient/agent/schema_validator.rb +215 -0
  25. data/lib/prescient/agent/tool_registry.rb +89 -0
  26. data/lib/prescient/agent.rb +22 -0
  27. data/lib/prescient/api.rb +346 -231
  28. data/lib/prescient/base.rb +370 -372
  29. data/lib/prescient/cli.rb +591 -402
  30. data/lib/prescient/client.rb +31 -4
  31. data/lib/prescient/configuration_loader.rb +511 -336
  32. data/lib/prescient/document_source.rb +114 -0
  33. data/lib/prescient/errors.rb +13 -3
  34. data/lib/prescient/mcp/authentication.rb +39 -0
  35. data/lib/prescient/mcp/configuration.rb +38 -0
  36. data/lib/prescient/mcp/rack.rb +243 -0
  37. data/lib/prescient/mcp/server.rb +202 -0
  38. data/lib/prescient/mcp/stdio.rb +42 -0
  39. data/lib/prescient/mcp.rb +8 -0
  40. data/lib/prescient/pgvector.rb +193 -189
  41. data/lib/prescient/provider/anthropic.rb +129 -125
  42. data/lib/prescient/provider/deepseek.rb +122 -118
  43. data/lib/prescient/provider/gemini.rb +153 -149
  44. data/lib/prescient/provider/huggingface.rb +191 -187
  45. data/lib/prescient/provider/mistral.rb +151 -147
  46. data/lib/prescient/provider/ollama.rb +168 -165
  47. data/lib/prescient/provider/openai.rb +174 -171
  48. data/lib/prescient/provider/xai.rb +122 -118
  49. data/lib/prescient/tool/search_api.rb +130 -0
  50. data/lib/prescient/tool/searxng.rb +128 -0
  51. data/lib/prescient/tool.rb +125 -0
  52. data/lib/prescient/version.rb +1 -1
  53. data/lib/prescient.rb +129 -55
  54. data/schema/prescient.configuration.schema.json +119 -0
  55. data/searxng/settings.yml +18 -0
  56. data/sig/prescient.rbs +228 -1
  57. metadata +33 -5
data/README.md CHANGED
@@ -2,25 +2,116 @@
2
2
 
3
3
  [![Gem Version](https://img.shields.io/gem/v/prescient?logo=rubygems&logoColor=white)](https://rubygems.org/gems/prescient)
4
4
  [![Requires Ruby 3.1+](https://img.shields.io/badge/Requires-Ruby%203.1%2B-CC342D?logo=ruby&logoColor=white)](https://www.ruby-lang.org/)
5
+ [![Docker](https://img.shields.io/badge/Docker-GHCR-2496ED?logo=docker&logoColor=white)](https://github.com/kanutocd/prescient/pkgs/container/prescient)
5
6
  [![CI](https://github.com/kanutocd/prescient/actions/workflows/ci.yml/badge.svg)](https://github.com/kanutocd/prescient/actions/workflows/ci.yml)
6
7
  [![Security](https://img.shields.io/github/actions/workflow/status/kanutocd/prescient/security.yml?branch=main&event=push&label=Security)](https://github.com/kanutocd/prescient/actions/workflows/security.yml)
7
8
  [![License](https://img.shields.io/badge/License-MIT-22C55E)](LICENSE.txt)
8
9
 
9
- 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, Hugging Face, Google Gemini, Mistral, DeepSeek, or xAI. Prescient handles provider selection, retries, health checks, and fallback.
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
- - **Unified Interface**: Single API for multiple AI providers
18
- - **Local and Cloud Support**: Ollama for local/private deployments, cloud APIs for scale
19
- - **Embedding Generation**: Vector embeddings for semantic search and AI applications
20
- - **Text Completion**: Chat completions with context support
21
- - **Error Handling**: Robust error handling with automatic retries
22
- - **Health Monitoring**: Built-in health checks for all providers
23
- - **Flexible Configuration**: YAML, environment variable, and programmatic configuration
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
- Prompt templates can also be configured per provider. Use the YAML mapping for
351
- multiline templates, or pass a template file to a single CLI operation:
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
- providers:
355
- openai:
356
- type: openai
357
- api_key_env: OPENAI_API_KEY
358
- chat_model: gpt-4.1-mini
359
- prompt_templates:
360
- system_prompt: You are a concise assistant.
361
- no_context_template: "%{system_prompt}\n\nUser: %{query}"
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
- prescient generate --prompt-templates-file prompts.yml "Summarize this"
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
- ## Usage
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 'sig'
5
- library 'json'
6
- library 'net-http'
4
+ signature "sig"
5
+ library "json"
6
+ library "net-http"
7
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'
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 'vector'
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, [:embedding_provider, :embedding_model], name: 'idx_doc_embeddings_provider_model'
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, [:document_id, :chunk_index], unique: true
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, [:embedding_provider, :embedding_model], name: 'idx_chunk_embeddings_provider_model'
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 'DROP FUNCTION IF EXISTS cosine_similarity(vector, vector);'
154
- execute 'DROP FUNCTION IF EXISTS euclidean_distance(vector, vector);'
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 'vector'
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),