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/lib/prescient.rb CHANGED
@@ -1,19 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'prescient/version'
4
- require_relative 'prescient/errors'
5
- require_relative 'prescient/pgvector'
6
- require_relative 'prescient/base'
7
- require_relative 'prescient/provider/ollama'
8
- require_relative 'prescient/provider/anthropic'
9
- require_relative 'prescient/provider/openai'
10
- require_relative 'prescient/provider/huggingface'
11
- require_relative 'prescient/provider/gemini'
12
- require_relative 'prescient/provider/mistral'
13
- require_relative 'prescient/provider/deepseek'
14
- require_relative 'prescient/provider/xai'
15
- require_relative 'prescient/configuration_loader'
16
- require_relative 'prescient/client'
3
+ require_relative "prescient/version"
4
+ require_relative "prescient/errors"
5
+ Prescient.autoload :Tool, "prescient/tool"
6
+ Prescient.autoload :DocumentSource, "prescient/document_source"
7
+ Prescient.autoload :Agent, "prescient/agent"
8
+ require_relative "prescient/pgvector"
9
+ require_relative "prescient/base"
10
+ require_relative "prescient/provider/ollama"
11
+ require_relative "prescient/provider/anthropic"
12
+ require_relative "prescient/provider/openai"
13
+ require_relative "prescient/provider/huggingface"
14
+ require_relative "prescient/provider/gemini"
15
+ require_relative "prescient/provider/mistral"
16
+ require_relative "prescient/provider/deepseek"
17
+ require_relative "prescient/provider/xai"
18
+ require_relative "prescient/configuration_loader"
19
+ require_relative "prescient/client"
17
20
 
18
21
  # Main Prescient module for AI provider abstraction
19
22
  #
@@ -33,9 +36,13 @@ require_relative 'prescient/client'
33
36
  # @example Embedding generation
34
37
  # embedding = client.generate_embedding("Some text to embed")
35
38
  # puts embedding.length # => 1536 (for OpenAI text-embedding-3-small)
39
+ # rubocop:disable Metrics/ModuleLength
36
40
  module Prescient
37
- autoload :API, 'prescient/api'
38
- autoload :CLI, 'prescient/cli'
41
+ autoload :Tool, "prescient/tool"
42
+ autoload :DocumentSource, "prescient/document_source"
43
+ autoload :API, "prescient/api"
44
+ autoload :CLI, "prescient/cli"
45
+ autoload :MCP, "prescient/mcp"
39
46
 
40
47
  # Configure Prescient with custom settings and providers
41
48
  #
@@ -58,14 +65,21 @@ module Prescient
58
65
  #
59
66
  # @return [Configuration] The current configuration
60
67
  def self.configuration
61
- @_configuration ||= Configuration.new
68
+ @configuration ||= Configuration.new
69
+ end
70
+
71
+ # Look up a configured external tool.
72
+ # @param name [Symbol, String] Tool name
73
+ # @return [Prescient::Tool::Base, nil] Configured tool instance
74
+ def self.tool(name)
75
+ configuration.tool(name)
62
76
  end
63
77
 
64
78
  # Reset configuration to defaults
65
79
  #
66
80
  # @return [Configuration] New configuration instance
67
81
  def self.reset_configuration!
68
- @_configuration = Configuration.new
82
+ @configuration = Configuration.new
69
83
  end
70
84
 
71
85
  # Load configuration from a YAML file and replace the current configuration.
@@ -78,16 +92,17 @@ module Prescient
78
92
  # @param env [Hash] Environment variables used while loading configuration
79
93
  # @return [Configuration] The loaded configuration
80
94
  def self.load_configuration(path = nil, env: ENV)
81
- effective_path = path || env['PRESCIENT_CONFIG']
95
+ effective_path = path || env["PRESCIENT_CONFIG"]
82
96
  configuration = if effective_path
83
97
  ConfigurationLoader.load_file(effective_path, env:)
84
98
  else
85
99
  Configuration.new.tap do |config|
86
100
  configure_default_providers(config, env)
101
+ configure_default_tools(config, env)
87
102
  end
88
103
  end
89
104
 
90
- @_configuration = configuration
105
+ @configuration = configuration
91
106
  end
92
107
 
93
108
  # Configuration class for managing Prescient settings and providers
@@ -96,7 +111,7 @@ module Prescient
96
111
  # provider registration and instantiation.
97
112
  class Configuration
98
113
  # @return [Array<Symbol>] Built-in provider option keys removed from output
99
- DEFAULT_SENSITIVE_KEYS = [:api_key, :password, :token, :secret].freeze
114
+ DEFAULT_SENSITIVE_KEYS = %i[api_key password token secret].freeze
100
115
 
101
116
  # @return [Symbol] The default provider to use when none specified
102
117
  attr_accessor :default_provider
@@ -119,6 +134,9 @@ module Prescient
119
134
  # @return [Hash] Registered providers configuration
120
135
  attr_reader :providers
121
136
 
137
+ # @return [Hash] Registered tools configuration
138
+ attr_reader :tools
139
+
122
140
  # Initialize configuration with default values
123
141
  def initialize
124
142
  @default_provider = :ollama
@@ -129,6 +147,8 @@ module Prescient
129
147
  @sensitive_keys = []
130
148
  @providers = {}
131
149
  @provider_instances = {} # : Hash[Symbol, untyped]
150
+ @tools = {}
151
+ @tool_instances = {} # : Hash[Symbol, untyped]
132
152
  end
133
153
 
134
154
  # Configure additional keys to remove from provider information.
@@ -158,8 +178,8 @@ module Prescient
158
178
  def add_provider(name, provider_class, **options)
159
179
  provider_name = name.to_sym
160
180
  @providers[provider_name] = {
161
- class: provider_class,
162
- options: options,
181
+ class: provider_class,
182
+ options: options
163
183
  }
164
184
  @provider_instances.delete(provider_name)
165
185
  end
@@ -177,6 +197,52 @@ module Prescient
177
197
  @provider_instances[provider_name] ||= provider_config[:class].new(**provider_options)
178
198
  end
179
199
 
200
+ # Register an external tool.
201
+ # @param name [Symbol] Unique identifier for the tool
202
+ # @param tool_class [Class] Tool adapter class
203
+ # @param options [Hash] Tool-specific configuration options
204
+ # @return [void]
205
+ def add_tool(name, tool_class, **options)
206
+ tool_name = name.to_sym
207
+ @tools[tool_name] = {
208
+ class: tool_class,
209
+ options: options
210
+ }
211
+ @tool_instances.delete(tool_name)
212
+ end
213
+
214
+ # Register ordered implementations for one logical external tool.
215
+ # @param name [Symbol] Unique logical capability identifier
216
+ # @param adapters [Array<Hash>] Adapter class and option configurations
217
+ # @return [void]
218
+ def add_tool_group(name, adapters)
219
+ tool_name = name.to_sym
220
+ @tools[tool_name] = { adapters: adapters }
221
+ @tool_instances.delete(tool_name)
222
+ end
223
+
224
+ # Instantiate a tool by name.
225
+ # @param name [Symbol, String] Tool name
226
+ # @return [Prescient::Tool::Base, nil] Tool instance or nil
227
+ def tool(name)
228
+ tool_name = name.to_sym
229
+ tool_config = @tools[tool_name]
230
+ return nil unless tool_config
231
+
232
+ if tool_config[:adapters]
233
+ @tool_instances[tool_name] ||= Prescient::Tool::Group.new(
234
+ adapters: tool_config[:adapters].map do |adapter|
235
+ adapter_options = adapter[:options] # : Hash[Symbol, untyped]
236
+ adapter[:class].new(**adapter_options)
237
+ end
238
+ )
239
+ return @tool_instances[tool_name]
240
+ end
241
+
242
+ tool_options = tool_config[:options] # : Hash[Symbol, untyped]
243
+ @tool_instances[tool_name] ||= tool_config[:class].new(**tool_options)
244
+ end
245
+
180
246
  # Get list of providers that currently pass {Prescient::Base#available?}.
181
247
  #
182
248
  # Providers are included when their health check reports `reachable: true`,
@@ -206,100 +272,106 @@ module Prescient
206
272
  configure_huggingface(config, env)
207
273
  end
208
274
 
275
+ def configure_default_tools(config, env)
276
+ return unless env["SEARXNG_URL"]
277
+
278
+ config.add_tool(:web_search, Prescient::Tool::SearXNG, url: env["SEARXNG_URL"])
279
+ end
280
+
209
281
  def configure_ollama(config, env)
210
282
  config.add_provider(
211
283
  :ollama,
212
284
  Prescient::Provider::Ollama,
213
- url: env.fetch('OLLAMA_URL', 'http://localhost:11434'),
214
- embedding_model: env.fetch('OLLAMA_EMBEDDING_MODEL', 'nomic-embed-text'),
215
- chat_model: env.fetch('OLLAMA_CHAT_MODEL', 'llama3.2:3b'),
285
+ url: env.fetch("OLLAMA_URL", "http://localhost:11434"),
286
+ embedding_model: env.fetch("OLLAMA_EMBEDDING_MODEL", "nomic-embed-text"),
287
+ chat_model: env.fetch("OLLAMA_CHAT_MODEL", "llama3.2:3b")
216
288
  )
217
289
  end
218
290
 
219
291
  def configure_openai(config, env)
220
- return unless env['OPENAI_API_KEY']
292
+ return unless env["OPENAI_API_KEY"]
221
293
 
222
294
  config.add_provider(
223
295
  :openai,
224
296
  Prescient::Provider::OpenAI,
225
- api_key: env['OPENAI_API_KEY'],
226
- embedding_model: env.fetch('OPENAI_EMBEDDING_MODEL', 'text-embedding-3-small'),
227
- chat_model: env.fetch('OPENAI_CHAT_MODEL', 'gpt-4.1-mini'),
297
+ api_key: env["OPENAI_API_KEY"],
298
+ embedding_model: env.fetch("OPENAI_EMBEDDING_MODEL", "text-embedding-3-small"),
299
+ chat_model: env.fetch("OPENAI_CHAT_MODEL", "gpt-4.1-mini")
228
300
  )
229
301
  end
230
302
 
231
303
  def configure_anthropic(config, env)
232
- return unless env['ANTHROPIC_API_KEY']
304
+ return unless env["ANTHROPIC_API_KEY"]
233
305
 
234
306
  config.add_provider(
235
307
  :anthropic,
236
308
  Prescient::Provider::Anthropic,
237
- api_key: env['ANTHROPIC_API_KEY'],
238
- model: env.fetch('ANTHROPIC_MODEL', 'claude-sonnet-4-20250514'),
309
+ api_key: env["ANTHROPIC_API_KEY"],
310
+ model: env.fetch("ANTHROPIC_MODEL", "claude-sonnet-4-20250514")
239
311
  )
240
312
  end
241
313
 
242
314
  def configure_gemini(config, env)
243
- return unless env['GEMINI_API_KEY']
315
+ return unless env["GEMINI_API_KEY"]
244
316
 
245
317
  config.add_provider(
246
318
  :gemini,
247
319
  Prescient::Provider::Gemini,
248
- api_key: env['GEMINI_API_KEY'],
249
- embedding_model: env.fetch('GEMINI_EMBEDDING_MODEL', 'gemini-embedding-001'),
250
- chat_model: env.fetch('GEMINI_CHAT_MODEL', 'gemini-2.5-flash'),
320
+ api_key: env["GEMINI_API_KEY"],
321
+ embedding_model: env.fetch("GEMINI_EMBEDDING_MODEL", "gemini-embedding-001"),
322
+ chat_model: env.fetch("GEMINI_CHAT_MODEL", "gemini-2.5-flash")
251
323
  )
252
324
  end
253
325
 
254
326
  def configure_huggingface(config, env)
255
- return unless env['HUGGINGFACE_API_KEY']
327
+ return unless env["HUGGINGFACE_API_KEY"]
256
328
 
257
329
  config.add_provider(
258
330
  :huggingface,
259
331
  Prescient::Provider::HuggingFace,
260
- api_key: env['HUGGINGFACE_API_KEY'],
332
+ api_key: env["HUGGINGFACE_API_KEY"],
261
333
  embedding_model: env.fetch(
262
- 'HUGGINGFACE_EMBEDDING_MODEL',
263
- 'sentence-transformers/all-MiniLM-L6-v2',
264
- ),
265
- chat_model: env.fetch(
266
- 'HUGGINGFACE_CHAT_MODEL',
267
- 'google/gemma-2-2b-it',
334
+ "HUGGINGFACE_EMBEDDING_MODEL",
335
+ "sentence-transformers/all-MiniLM-L6-v2"
268
336
  ),
337
+ chat_model: env.fetch(
338
+ "HUGGINGFACE_CHAT_MODEL",
339
+ "google/gemma-2-2b-it"
340
+ )
269
341
  )
270
342
  end
271
343
 
272
344
  def configure_deepseek(config, env)
273
- return unless env['DEEPSEEK_API_KEY']
345
+ return unless env["DEEPSEEK_API_KEY"]
274
346
 
275
347
  config.add_provider(
276
348
  :deepseek,
277
349
  Prescient::Provider::DeepSeek,
278
- api_key: env['DEEPSEEK_API_KEY'],
279
- chat_model: env.fetch('DEEPSEEK_CHAT_MODEL', 'deepseek-v4-flash'),
350
+ api_key: env["DEEPSEEK_API_KEY"],
351
+ chat_model: env.fetch("DEEPSEEK_CHAT_MODEL", "deepseek-v4-flash")
280
352
  )
281
353
  end
282
354
 
283
355
  def configure_xai(config, env)
284
- return unless env['XAI_API_KEY']
356
+ return unless env["XAI_API_KEY"]
285
357
 
286
358
  config.add_provider(
287
359
  :xai,
288
360
  Prescient::Provider::XAI,
289
- api_key: env['XAI_API_KEY'],
290
- chat_model: env.fetch('XAI_CHAT_MODEL', 'grok-4.5'),
361
+ api_key: env["XAI_API_KEY"],
362
+ chat_model: env.fetch("XAI_CHAT_MODEL", "grok-4.5")
291
363
  )
292
364
  end
293
365
 
294
366
  def configure_mistral(config, env)
295
- return unless env['MISTRAL_API_KEY']
367
+ return unless env["MISTRAL_API_KEY"]
296
368
 
297
369
  config.add_provider(
298
370
  :mistral,
299
371
  Prescient::Provider::Mistral,
300
- api_key: env['MISTRAL_API_KEY'],
301
- embedding_model: env.fetch('MISTRAL_EMBEDDING_MODEL', 'mistral-embed'),
302
- chat_model: env.fetch('MISTRAL_CHAT_MODEL', 'mistral-large-latest'),
372
+ api_key: env["MISTRAL_API_KEY"],
373
+ embedding_model: env.fetch("MISTRAL_EMBEDDING_MODEL", "mistral-embed"),
374
+ chat_model: env.fetch("MISTRAL_CHAT_MODEL", "mistral-large-latest")
303
375
  )
304
376
  end
305
377
  end
@@ -307,5 +379,7 @@ module Prescient
307
379
  # Default configuration
308
380
  configure do |config|
309
381
  configure_default_providers(config, ENV)
382
+ configure_default_tools(config, ENV)
310
383
  end
311
384
  end
385
+ # rubocop:enable Metrics/ModuleLength
@@ -58,6 +58,12 @@
58
58
  "additionalProperties": {
59
59
  "$ref": "#/$defs/provider"
60
60
  }
61
+ },
62
+ "tools": {
63
+ "type": "object",
64
+ "additionalProperties": {
65
+ "$ref": "#/$defs/tool"
66
+ }
61
67
  }
62
68
  },
63
69
  "$defs": {
@@ -148,6 +154,119 @@
148
154
  "type": "string"
149
155
  }
150
156
  }
157
+ },
158
+ "tool": {
159
+ "type": "object",
160
+ "additionalProperties": false,
161
+ "anyOf": [
162
+ {
163
+ "required": [
164
+ "type"
165
+ ]
166
+ },
167
+ {
168
+ "required": [
169
+ "adapters"
170
+ ]
171
+ }
172
+ ],
173
+ "properties": {
174
+ "adapters": {
175
+ "type": "array",
176
+ "minItems": 1,
177
+ "items": {
178
+ "$ref": "#/$defs/tool"
179
+ }
180
+ },
181
+ "type": {
182
+ "type": "string",
183
+ "enum": [
184
+ "searxng",
185
+ "searchapi"
186
+ ]
187
+ },
188
+ "api_key": {
189
+ "type": "string"
190
+ },
191
+ "api_key_env": {
192
+ "type": "string"
193
+ },
194
+ "engine": {
195
+ "type": "string"
196
+ },
197
+ "engine_env": {
198
+ "type": "string"
199
+ },
200
+ "location": {
201
+ "type": "string"
202
+ },
203
+ "location_env": {
204
+ "type": "string"
205
+ },
206
+ "hl": {
207
+ "type": "string"
208
+ },
209
+ "hl_env": {
210
+ "type": "string"
211
+ },
212
+ "gl": {
213
+ "type": "string"
214
+ },
215
+ "gl_env": {
216
+ "type": "string"
217
+ },
218
+ "url": {
219
+ "type": "string",
220
+ "format": "uri-reference"
221
+ },
222
+ "url_env": {
223
+ "type": "string"
224
+ },
225
+ "language": {
226
+ "type": "string"
227
+ },
228
+ "language_env": {
229
+ "type": "string"
230
+ },
231
+ "categories": {
232
+ "oneOf": [
233
+ {
234
+ "type": "string"
235
+ },
236
+ {
237
+ "type": "array",
238
+ "items": {
239
+ "type": "string"
240
+ }
241
+ }
242
+ ]
243
+ },
244
+ "categories_env": {
245
+ "type": "string"
246
+ },
247
+ "timeout": {
248
+ "type": "number",
249
+ "exclusiveMinimum": 0
250
+ },
251
+ "timeout_env": {
252
+ "type": "string"
253
+ },
254
+ "max_results": {
255
+ "type": "integer",
256
+ "minimum": 1,
257
+ "maximum": 20
258
+ },
259
+ "max_results_env": {
260
+ "type": "string"
261
+ },
262
+ "max_response_bytes": {
263
+ "type": "integer",
264
+ "minimum": 1
265
+ },
266
+ "max_response_bytes_env": {
267
+ "type": "string"
268
+ }
269
+ }
151
270
  }
152
271
  }
153
272
  }
@@ -0,0 +1,18 @@
1
+ # Development settings for the SearXNG service in docker-compose.yml.
2
+ use_default_settings: true
3
+
4
+ general:
5
+ debug: false
6
+ instance_name: Prescient SearXNG
7
+
8
+ search:
9
+ formats:
10
+ - html
11
+ - json
12
+
13
+ server:
14
+ secret_key: prescient-development-secret
15
+ bind_address: 0.0.0.0
16
+ base_url: http://localhost:8080/
17
+ limiter: false
18
+ image_proxy: false