prescient 0.6.0 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 167f50351b78b35d1ae63d3487285db48c07799ad015b5d49f46dd3c451afa49
4
- data.tar.gz: 91e1895635015120c75b0b461fea017d8ec7c4b5ebd81f1d2b0c86ba37690e1d
3
+ metadata.gz: 176552789656cbda6e78951ab04eb0ecdfa9ffa53dcf88159bec5cdbf403cdef
4
+ data.tar.gz: 5173a6bdd607dc6aca121079ee6e81243a550575f4702c57133f637a31aac798
5
5
  SHA512:
6
- metadata.gz: 6cdf4ed9b05525692898cdd88153f092c23f5caac1d422962bdc768055bb225e960e45a9fb877adb17a7c686e8bf608db09dea1b3e24ebc15767071255910285
7
- data.tar.gz: ecec27b8c215dfe5fb92e00f68fd6a5ae66c8284342525c9ae635214318032fc4970a72d2944679e455915f2ba8de8425a07706530ca6c309bedc5d5d4ff114a
6
+ metadata.gz: fb309c19aa67f200e806b227f392ff5181e5ef7bc1ea00a6f9a0d4cf5ebbce989c0d9b472885a1c2795efaa7789a8ed61f912453fd20594fba029f8c7bc126d4
7
+ data.tar.gz: 70187123148e3c0aad70f88fa469c91e3c55200c1c36b9452697109e2746cfe0fe70ef8358b766b3f99b967884aba4b846f3a8cb634c76fe9803ad45925d0e50
data/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## [0.7.0] - 2025-08-17
6
+
7
+ ### Added
8
+
9
+ - Added an explicit external-tool contract with a configurable SearXNG web-search adapter.
10
+ - Added YAML/schema configuration for tool registration, environment references, bounded requests, and normalized search results.
11
+ - Added `prescient search` and an annotated `web_search` configuration example.
12
+ - Added an optional development SearXNG service to `docker-compose.yml` for runnable web-search examples.
13
+ - Added opt-in search-to-provider context assembly through `Prescient.search_and_generate` and `prescient search --generate`.
14
+ - Added `POST /v1/search/generate` for REST API consumers to opt in to search-result context.
15
+ - Added `POST /v1/search` for normalized raw external-tool results without AI generation.
16
+ - Added SearchApi as a hosted web-search adapter with engine, location, language,
17
+ country, API-key, timeout, and result-limit configuration.
18
+ - Added capability groups for ordered tool-adapter fallback on transient
19
+ connection and rate-limit failures.
20
+ - Documented SearXNG and SearchApi setup and CLI usage side by side.
21
+ - Added explicit `--generate` opt-in behavior to the web-search example.
22
+ - Added `SEARXNG_URL` environment defaults for automatic `web_search` registration.
23
+ - Expanded `prescient config example` with documented SearXNG settings,
24
+ environment references, custom tool names, and direct-versus-generated usage.
25
+ - Organized CLI help into global and search-specific option sections.
26
+
5
27
  ## [0.6.0] - 2025-08-15
6
28
 
7
29
  ### Added
data/INTEGRATION_GUIDE.md CHANGED
@@ -11,7 +11,7 @@ and [examples guide](examples/README.md).
11
11
 
12
12
  ```ruby
13
13
  # Add to your Gemfile
14
- gem 'prescient', '~> 0.6.0'
14
+ gem 'prescient', '~> 0.7.0'
15
15
  ```
16
16
 
17
17
  ### 2. Replace Existing AI Service
@@ -190,6 +190,8 @@ This makes the following routes available under `/prescient`:
190
190
  | `GET` | `/v1/capabilities` | Provider capabilities |
191
191
  | `GET` | `/v1/health` | Provider health |
192
192
  | `POST` | `/v1/generate` | Text generation |
193
+ | `POST` | `/v1/search` | Normalized external-tool search |
194
+ | `POST` | `/v1/search/generate` | Search with opt-in AI generation |
193
195
  | `POST` | `/v1/embeddings` | Single embedding |
194
196
  | `POST` | `/v1/embeddings/batch` | Bounded batch embeddings |
195
197
 
@@ -202,6 +204,16 @@ curl -X POST http://localhost:3000/prescient/v1/generate \
202
204
  -H "Authorization: Bearer ${PRESCIENT_API_TOKEN}" \
203
205
  -H 'Content-Type: application/json' \
204
206
  -d '{"prompt":"Explain Ruby fibers"}'
207
+
208
+ curl -X POST http://localhost:3000/prescient/v1/search/generate \
209
+ -H "Authorization: Bearer ${PRESCIENT_API_TOKEN}" \
210
+ -H 'Content-Type: application/json' \
211
+ -d '{"query":"Ruby HTTP clients","provider":"openai","limit":5}'
212
+
213
+ curl -X POST http://localhost:3000/prescient/v1/search \
214
+ -H "Authorization: Bearer ${PRESCIENT_API_TOKEN}" \
215
+ -H 'Content-Type: application/json' \
216
+ -d '{"query":"Ruby HTTP clients","limit":5}'
205
217
  ```
206
218
 
207
219
  Responses include a request ID. Request bodies are size-limited, batch inputs
data/README.md CHANGED
@@ -2,11 +2,12 @@
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
@@ -14,13 +15,15 @@ For focused guidance, see the **[examples guide](https://github.com/kanutocd/pre
14
15
 
15
16
  ## Features
16
17
 
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
18
+ - **Provider abstraction** One consistent interface across supported AI providers
19
+ - **Multiple interfaces** Ruby API, CLI, and Rack-compatible REST API
20
+ - **Text and embeddings** Generate responses and embeddings with provider/model selection
21
+ - **Reliability controls** Retries, health checks, and fallback across configured providers
22
+ - **Declarative configuration** Versioned YAML, environment references, and JSON Schema validation
23
+ - **Prompt and context customization** Configurable prompt templates and context formatting
24
+ - **External tools** — Explicit, normalized web-search integration with SearXNG and SearchApi
25
+ - **Local and cloud support** — Ollama alongside hosted providers
26
+ - **Optional integrations** — Docker deployment and pgvector support without making either mandatory
24
27
 
25
28
  ## Supported Providers
26
29
 
@@ -242,6 +245,8 @@ run Prescient::API.new(
242
245
  Available endpoints include:
243
246
 
244
247
  - **`POST /v1/generate`**
248
+ - **`POST /v1/search`**
249
+ - **`POST /v1/search/generate`**
245
250
  - **`POST /v1/embeddings`**
246
251
  - **`POST /v1/embeddings/batch`**
247
252
  - **`GET /v1/providers`**
@@ -347,25 +352,139 @@ YAML values, then built-in defaults. Use `prescient config validate` to check a
347
352
  configuration before running an operation, or `prescient config example` to
348
353
  generate an annotated starter file.
349
354
 
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:
355
+ ### External Tools
356
+
357
+ External tools are opt-in capability adapters, separate from AI providers.
358
+ Supported web-search adapters are [SearXNG](https://searxng.org/) and
359
+ [SearchApi](https://www.searchapi.io/):
360
+
361
+ #### SearXNG
362
+
363
+ Setting `SEARXNG_URL` registers the default `web_search` tool for CLI and Ruby
364
+ environment-based configuration. YAML or programmatic configuration can be
365
+ used when more control is needed.
352
366
 
353
367
  ```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}"
368
+ tools:
369
+ web_search:
370
+ type: searxng
371
+ url_env: SEARXNG_URL
372
+ language: en
373
+ categories: [general, news]
374
+ timeout: 5
375
+ max_results: 5
363
376
  ```
364
377
 
378
+ Run the SearXNG-backed tool:
379
+
365
380
  ```bash
366
- prescient generate --prompt-templates-file prompts.yml "Summarize this"
381
+ docker compose up -d searxng
382
+ SEARXNG_URL=http://localhost:8080 bundle exec prescient search \
383
+ --format json "Ruby HTTP clients"
384
+ ```
385
+
386
+ #### SearchApi
387
+
388
+ [SearchApi](https://www.searchapi.io/) uses a hosted Google search engine and
389
+ requires an API key:
390
+
391
+ ```yaml
392
+ tools:
393
+ web_search:
394
+ type: searchapi
395
+ api_key_env: SEARCHAPI_API_KEY
396
+ engine: google
397
+ location: New York
398
+ hl: en
399
+ gl: us
400
+ timeout: 10
401
+ max_results: 5
402
+ ```
403
+
404
+ The `engine` value can select another SearchApi web or product engine when its
405
+ response uses `organic_results`, such as `bing`, `yahoo`, `yandex`,
406
+ `amazon_search`, or `walmart_search`.
407
+
408
+ Adapters can be grouped under one logical capability for ordered fallback:
409
+
410
+ ```yaml
411
+ tools:
412
+ web_search:
413
+ adapters:
414
+ - type: searxng
415
+ url_env: SEARXNG_URL
416
+ - type: searchapi
417
+ api_key_env: SEARCHAPI_API_KEY
418
+ engine: google
419
+ ```
420
+
421
+ Adapters are tried in order. Fallback is limited to transient connection and
422
+ rate-limit failures; invalid configuration, authentication failures, and
423
+ malformed responses are not retried with another adapter.
424
+
425
+ Run the SearchApi-backed tool:
426
+
427
+ ```bash
428
+ SEARCHAPI_API_KEY=your-key bundle exec prescient search \
429
+ --config prescient.yml --format json "Ruby HTTP clients"
430
+ ```
431
+
432
+ Invoke a configured tool explicitly from Ruby or the CLI:
433
+
434
+ ```ruby
435
+ result = Prescient.tool(:web_search).search('Ruby HTTP clients', limit: 3)
436
+ ```
437
+
438
+ ```bash
439
+ bundle exec prescient search --config prescient.yml \
440
+ --tool web_search --format json "Ruby HTTP clients"
367
441
  ```
368
442
 
443
+ Results use a normalized envelope containing `tool`, `query`, `source`, and
444
+ `results` entries with `title`, `url`, `snippet`, and `source`. Requests have
445
+ bounded query length, timeout, result count, and response size. Tool execution
446
+ is explicit; Prescient does not autonomously invoke tools, and tool endpoints
447
+ are not exposed as raw tool endpoints through `Prescient::API`; the API exposes
448
+ the explicit combined search-and-generation operation below. Other adapters can
449
+ implement the same contract without changing provider integrations.
450
+
451
+ Search results are not sent to an AI provider by default. Opt in when you want
452
+ the normalized results assembled as generation context:
453
+
454
+ ```ruby
455
+ response = Prescient.search_and_generate(
456
+ 'Ruby HTTP clients',
457
+ tool: :web_search,
458
+ provider: :openai,
459
+ )
460
+ ```
461
+
462
+ The CLI exposes the same opt-in behavior with `--generate`:
463
+
464
+ ```bash
465
+ prescient search --generate --provider openai "Ruby HTTP clients"
466
+ ```
467
+
468
+ The REST API exposes the same opt-in behavior:
469
+
470
+ Raw normalized search results are available without generation:
471
+
472
+ ```bash
473
+ curl -X POST http://localhost:9292/v1/search \
474
+ -H 'Content-Type: application/json' \
475
+ -d '{"query":"Ruby HTTP clients","limit":5}'
476
+ ```
477
+
478
+ ```bash
479
+ curl -X POST http://localhost:9292/v1/search/generate \
480
+ -H 'Content-Type: application/json' \
481
+ -d '{"query":"Ruby HTTP clients","provider":"openai","limit":5}'
482
+ ```
483
+
484
+ Use `fallback: false` to disable provider fallback for the request. The
485
+ response is the normalized AI provider response; omit this endpoint and use
486
+ `POST /v1/generate` when search context is not wanted.
487
+
369
488
  ### Programmatic Configuration
370
489
 
371
490
  ```ruby
@@ -602,6 +721,24 @@ client = Prescient.client(:customer_service)
602
721
  response = client.generate_response("What's your return policy?")
603
722
  ```
604
723
 
724
+ Templates can also be configured in YAML or overridden for one CLI operation:
725
+
726
+ ```yaml
727
+ providers:
728
+ openai:
729
+ type: openai
730
+ api_key_env: OPENAI_API_KEY
731
+ chat_model: gpt-4.1-mini
732
+ prompt_templates:
733
+ system_prompt: You are a concise assistant.
734
+ no_context_template: "%{system_prompt}\n\nUser: %{query}"
735
+ with_context_template: "%{system_prompt}\n\nContext:\n%{context}\n\nUser: %{query}"
736
+ ```
737
+
738
+ ```bash
739
+ prescient generate --prompt-templates-file prompts.yml "Summarize this"
740
+ ```
741
+
605
742
  ### Template Placeholders
606
743
 
607
744
  - `%{system_prompt}` - The system/role instruction
@@ -1132,6 +1269,8 @@ The included `docker-compose.yml` provides:
1132
1269
 
1133
1270
  - **ollama**: Ollama AI service with persistent model storage
1134
1271
  - **ollama-init**: Automatically pulls required models on startup
1272
+ - **searxng**: Optional SearXNG web-search service with JSON output enabled
1273
+ - **postgres**: Optional PostgreSQL database with pgvector support
1135
1274
  - **redis**: Optional caching layer for embeddings
1136
1275
  - **prescient-app**: Example Ruby application container
1137
1276
 
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
@@ -19,6 +19,10 @@ bundle install
19
19
  and similarity search.
20
20
  - `rest_api.ru` — a tiny Rack-compatible application that mounts
21
21
  `Prescient::API` and lists its endpoints at `/`.
22
+ - `web_search.rb` — explicit SearXNG tool invocation with normalized JSON output.
23
+
24
+ The same `web_search` capability can use SearchApi instead of SearXNG when the
25
+ tool is configured with `type: searchapi` and `SEARCHAPI_API_KEY`.
22
26
 
23
27
  Run the REST API example with a Rack server such as `rackup`:
24
28
 
@@ -29,6 +33,17 @@ PRESCIENT_API_TOKEN=change-me BUNDLE_WITH=rack_example \
29
33
  curl http://localhost:9292/
30
34
  ```
31
35
 
36
+ The endpoint catalog includes `POST /v1/search/generate`. With a configured
37
+ SearXNG tool and AI provider, call it explicitly to feed search results into
38
+ generation:
39
+
40
+ ```bash
41
+ curl -X POST http://localhost:9292/v1/search/generate \
42
+ -H 'Authorization: Bearer change-me' \
43
+ -H 'Content-Type: application/json' \
44
+ -d '{"query":"Ruby HTTP clients","provider":"openai","limit":5}'
45
+ ```
46
+
32
47
  Running `bundle exec ruby examples/rest_api.ru` directly prints the same
33
48
  endpoint catalog without starting a server.
34
49
 
@@ -57,6 +72,25 @@ configuration. The scripts are demonstrations rather than isolated test
57
72
  fixtures; they may make real provider requests when the configured service is
58
73
  available.
59
74
 
75
+ The web-search example requires a reachable SearXNG instance:
76
+
77
+ ```bash
78
+ docker compose up -d searxng
79
+ SEARXNG_URL=http://localhost:8080 bundle exec ruby examples/web_search.rb "Ruby HTTP clients"
80
+ ```
81
+
82
+ The example returns normalized search results directly by default. Opt in to
83
+ feeding those results to the configured AI provider with `--generate`:
84
+
85
+ ```bash
86
+ SEARXNG_URL=http://localhost:8080 PRESCIENT_PROVIDER=openai \
87
+ bundle exec ruby examples/web_search.rb --generate "Ruby HTTP clients"
88
+ ```
89
+
90
+ Omit `--generate` to keep the search results direct. `PRESCIENT_PROVIDER` is
91
+ only used with `--generate` and may be omitted when the default provider is
92
+ configured.
93
+
60
94
  See the [main README](../README.md) for configuration, fallback behavior,
61
95
  prompt templates, context exclusions, embeddings, and the public API. Rails
62
96
  applications can also use the [integration guide](../INTEGRATION_GUIDE.md),
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative '../lib/prescient'
5
+
6
+ generate = ARGV.delete('--generate')
7
+ if ARGV.include?('--help')
8
+ puts 'Usage: ruby examples/web_search.rb [--generate] [QUERY]'
9
+ puts ' --generate Feed normalized search results to the configured AI provider'
10
+ puts ' PRESCIENT_PROVIDER Provider used with --generate (default: configured provider)'
11
+ exit
12
+ end
13
+
14
+ query = ARGV.empty? ? 'Ruby HTTP clients' : ARGV.join(' ')
15
+
16
+ Prescient.configure do |config|
17
+ config.add_tool(
18
+ :web_search,
19
+ Prescient::Tool::SearXNG,
20
+ url: ENV.fetch('SEARXNG_URL', 'http://localhost:8080'),
21
+ )
22
+ end
23
+
24
+ result = if generate
25
+ Prescient.search_and_generate(
26
+ query,
27
+ provider: ENV['PRESCIENT_PROVIDER']&.to_sym,
28
+ limit: 20,
29
+ )
30
+ else
31
+ Prescient.tool(:web_search).search(query, limit: 20)
32
+ end
33
+
34
+ puts JSON.pretty_generate(result)
data/lib/prescient/api.rb CHANGED
@@ -27,6 +27,8 @@ class Prescient::API
27
27
  ['GET', '/v1/capabilities'] => :capabilities_response,
28
28
  ['GET', '/v1/health'] => :health_response,
29
29
  ['POST', '/v1/generate'] => :generate_response,
30
+ ['POST', '/v1/search'] => :search_response,
31
+ ['POST', '/v1/search/generate'] => :search_generate_response,
30
32
  ['POST', '/v1/embeddings'] => :embeddings_response,
31
33
  ['POST', '/v1/embeddings/batch'] => :batch_embeddings_response,
32
34
  }.freeze
@@ -88,6 +90,56 @@ class Prescient::API
88
90
  json_response(200, result, request_id)
89
91
  end
90
92
 
93
+ def search_generate_response(env, _query, request_id)
94
+ payload = request_payload(env)
95
+ query = required_string(payload, 'query')
96
+ tool = search_tool_name(payload)
97
+ fallback = search_fallback(payload)
98
+ limit = search_limit(payload)
99
+
100
+ result = Prescient.search_and_generate(
101
+ query,
102
+ tool: tool,
103
+ provider: payload['provider']&.to_sym,
104
+ limit: limit,
105
+ enable_fallback: fallback,
106
+ **generation_options(payload),
107
+ )
108
+ json_response(200, result, request_id)
109
+ end
110
+
111
+ def search_response(env, _query, request_id)
112
+ payload = request_payload(env)
113
+ query = required_string(payload, 'query')
114
+ tool_name = search_tool_name(payload)
115
+ tool = Prescient.tool(tool_name)
116
+ raise Prescient::ToolConfigurationError, "tool not configured: #{tool_name}" unless tool
117
+
118
+ result = tool.search(query, limit: search_limit(payload))
119
+ json_response(200, result, request_id)
120
+ end
121
+
122
+ def search_tool_name(payload)
123
+ value = payload.fetch('tool', 'web_search')
124
+ raise ArgumentError, 'tool must be a non-empty string' unless value.is_a?(String) && !value.empty?
125
+
126
+ value.to_sym
127
+ end
128
+
129
+ def search_fallback(payload)
130
+ fallback = payload.key?('fallback') ? payload['fallback'] : true
131
+ raise ArgumentError, 'fallback must be boolean' unless [true, false].include?(fallback)
132
+
133
+ fallback
134
+ end
135
+
136
+ def search_limit(payload)
137
+ limit = payload['limit']
138
+ raise ArgumentError, 'limit must be a positive integer' if limit && (!limit.is_a?(Integer) || !limit.positive?)
139
+
140
+ limit
141
+ end
142
+
91
143
  def embeddings_response(env, _query, request_id)
92
144
  payload = request_payload(env)
93
145
  input = required_string(payload, 'input')