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 +4 -4
- data/CHANGELOG.md +22 -0
- data/INTEGRATION_GUIDE.md +13 -1
- data/README.md +159 -20
- data/docker-compose.yml +22 -0
- data/examples/README.md +34 -0
- data/examples/web_search.rb +34 -0
- data/lib/prescient/api.rb +52 -0
- data/lib/prescient/cli.rb +135 -6
- data/lib/prescient/client.rb +26 -0
- data/lib/prescient/configuration_loader.rb +171 -0
- data/lib/prescient/errors.rb +12 -0
- data/lib/prescient/provider/openai.rb +0 -2
- data/lib/prescient/tool/search_api.rb +126 -0
- data/lib/prescient/tool/searxng.rb +124 -0
- data/lib/prescient/tool.rb +123 -0
- data/lib/prescient/version.rb +1 -1
- data/lib/prescient.rb +68 -0
- data/schema/prescient.configuration.schema.json +119 -0
- data/searxng/settings.yml +18 -0
- data/sig/prescient.rbs +52 -0
- metadata +11 -5
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'httparty'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'timeout'
|
|
6
|
+
require 'uri'
|
|
7
|
+
|
|
8
|
+
# SearXNG web-search tool adapter.
|
|
9
|
+
class Prescient::Tool::SearXNG < Prescient::Tool::Base
|
|
10
|
+
# Network failures that should be reported as connection errors.
|
|
11
|
+
NETWORK_ERRORS = [Net::OpenTimeout, Net::ReadTimeout, Timeout::Error, SocketError, Errno::ECONNREFUSED].freeze
|
|
12
|
+
|
|
13
|
+
# @param options [Hash] SearXNG configuration options
|
|
14
|
+
# @option options [String] :url SearXNG base URL
|
|
15
|
+
# @option options [String] :language Optional search language
|
|
16
|
+
# @option options [String, Array<String>] :categories Optional categories
|
|
17
|
+
def initialize(**options)
|
|
18
|
+
super
|
|
19
|
+
@base_url = normalized_base_url(options.fetch(:url))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Search SearXNG and return normalized result metadata.
|
|
23
|
+
# @param query [String] Search query
|
|
24
|
+
# @param limit [Integer, nil] Maximum number of results
|
|
25
|
+
# @param timeout [Numeric, nil] Request timeout in seconds
|
|
26
|
+
# @return [Hash] Normalized search result envelope
|
|
27
|
+
def search(query, limit: nil, timeout: nil)
|
|
28
|
+
normalized_query = validate_query(query)
|
|
29
|
+
requested_limit = result_limit(limit)
|
|
30
|
+
requested_timeout = request_timeout(timeout)
|
|
31
|
+
response = HTTParty.get(
|
|
32
|
+
search_url,
|
|
33
|
+
query: request_parameters(normalized_query),
|
|
34
|
+
timeout: requested_timeout,
|
|
35
|
+
)
|
|
36
|
+
validate_response!(response)
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
tool: 'web_search',
|
|
40
|
+
query: normalized_query,
|
|
41
|
+
source: 'searxng',
|
|
42
|
+
results: normalize_results(response.parsed_response, requested_limit),
|
|
43
|
+
}
|
|
44
|
+
rescue Prescient::Error
|
|
45
|
+
raise
|
|
46
|
+
rescue *NETWORK_ERRORS => e
|
|
47
|
+
raise Prescient::ToolConnectionError, "SearXNG request failed: #{e.class}"
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
raise Prescient::ToolError, "SearXNG request failed: #{e.message}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
protected
|
|
53
|
+
|
|
54
|
+
# Validate SearXNG-specific configuration.
|
|
55
|
+
# @return [void]
|
|
56
|
+
def validate_configuration!
|
|
57
|
+
normalized_base_url(@options.fetch(:url))
|
|
58
|
+
max_response_bytes
|
|
59
|
+
result_limit(nil)
|
|
60
|
+
request_timeout(nil)
|
|
61
|
+
rescue KeyError
|
|
62
|
+
raise Prescient::ToolConfigurationError, 'SearXNG requires a url'
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def normalized_base_url(value)
|
|
68
|
+
uri = URI.parse(value.to_s)
|
|
69
|
+
valid_scheme = ['http', 'https'].include?(uri.scheme)
|
|
70
|
+
if !valid_scheme || uri.host.nil? || uri.userinfo
|
|
71
|
+
raise Prescient::ToolConfigurationError, 'SearXNG url must be an HTTP(S) URL without credentials'
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
uri.to_s.delete_suffix('/')
|
|
75
|
+
rescue URI::InvalidURIError
|
|
76
|
+
raise Prescient::ToolConfigurationError, 'SearXNG url must be a valid HTTP(S) URL'
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def search_url
|
|
80
|
+
"#{@base_url}/search"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def request_parameters(query)
|
|
84
|
+
parameters = { q: query, format: 'json' }
|
|
85
|
+
parameters[:language] = @options[:language] if @options[:language]
|
|
86
|
+
parameters[:categories] = Array(@options[:categories]).join(',') if @options[:categories]
|
|
87
|
+
parameters
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def validate_response!(response)
|
|
91
|
+
if response.respond_to?(:body) && response.body.to_s.bytesize > max_response_bytes
|
|
92
|
+
raise Prescient::ToolInvalidResponseError, 'SearXNG response exceeds configured size limit'
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
return if response.success?
|
|
96
|
+
|
|
97
|
+
error_class = case response.code.to_i
|
|
98
|
+
when 401, 403 then Prescient::AuthenticationError
|
|
99
|
+
when 429 then Prescient::RateLimitError
|
|
100
|
+
when 500..599 then Prescient::ToolConnectionError
|
|
101
|
+
else Prescient::ToolError
|
|
102
|
+
end
|
|
103
|
+
raise error_class, "SearXNG returned HTTP #{response.code}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def normalize_results(payload, limit)
|
|
107
|
+
raw_results = payload.is_a?(Hash) ? payload['results'] : nil
|
|
108
|
+
unless raw_results.is_a?(Array)
|
|
109
|
+
raise Prescient::ToolInvalidResponseError, 'SearXNG response did not contain results'
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
raw_results.filter_map { |result|
|
|
113
|
+
next unless result.is_a?(Hash)
|
|
114
|
+
next unless result['url'].is_a?(String) && !result['url'].empty?
|
|
115
|
+
|
|
116
|
+
{
|
|
117
|
+
title: result['title'].to_s,
|
|
118
|
+
url: result['url'],
|
|
119
|
+
snippet: result['content'].to_s,
|
|
120
|
+
source: 'searxng',
|
|
121
|
+
}
|
|
122
|
+
}.first(limit)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Namespace for external capability adapters.
|
|
4
|
+
module Prescient::Tool
|
|
5
|
+
autoload :SearXNG, 'prescient/tool/searxng'
|
|
6
|
+
autoload :SearchApi, 'prescient/tool/search_api'
|
|
7
|
+
|
|
8
|
+
# Base contract for explicit external tool invocation.
|
|
9
|
+
class Base
|
|
10
|
+
# @return [Integer] Default request timeout in seconds
|
|
11
|
+
DEFAULT_TIMEOUT = 5
|
|
12
|
+
# @return [Integer] Default maximum number of returned results
|
|
13
|
+
DEFAULT_MAX_RESULTS = 5
|
|
14
|
+
# @return [Integer] Maximum accepted search query length
|
|
15
|
+
MAX_QUERY_LENGTH = 2_000
|
|
16
|
+
# @return [Integer] Maximum accepted tool response size in bytes
|
|
17
|
+
DEFAULT_MAX_RESPONSE_BYTES = 1_048_576
|
|
18
|
+
|
|
19
|
+
# @return [Hash<Symbol, untyped>] Tool configuration options
|
|
20
|
+
attr_reader :options
|
|
21
|
+
|
|
22
|
+
# @param options [Hash] Tool-specific configuration options
|
|
23
|
+
def initialize(**options)
|
|
24
|
+
@options = options
|
|
25
|
+
validate_configuration!
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Execute a search using the tool.
|
|
29
|
+
# @param query [String] Search query
|
|
30
|
+
# @param options [Hash] Per-request options
|
|
31
|
+
# @return [Hash] Normalized tool result
|
|
32
|
+
# @raise [NotImplementedError] If the adapter does not support searching
|
|
33
|
+
def search(query, **options)
|
|
34
|
+
raise NotImplementedError, "#{self.class} must implement #search"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
protected
|
|
38
|
+
|
|
39
|
+
# Validate adapter configuration.
|
|
40
|
+
# @return [void]
|
|
41
|
+
def validate_configuration!
|
|
42
|
+
# Override in subclasses.
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param query [String] Search query
|
|
46
|
+
# @return [String] Validated query
|
|
47
|
+
def validate_query(query)
|
|
48
|
+
unless query.is_a?(String) && !query.strip.empty?
|
|
49
|
+
raise Prescient::ToolConfigurationError, 'search query must be a non-empty string'
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
cleaned_query = query.strip
|
|
53
|
+
if cleaned_query.length > MAX_QUERY_LENGTH
|
|
54
|
+
raise Prescient::ToolConfigurationError,
|
|
55
|
+
"search query cannot contain more than #{MAX_QUERY_LENGTH} characters"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
cleaned_query
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @param value [Object] Requested result limit
|
|
62
|
+
# @return [Integer] Validated result limit
|
|
63
|
+
def result_limit(value)
|
|
64
|
+
limit = value.nil? ? @options.fetch(:max_results, DEFAULT_MAX_RESULTS) : value
|
|
65
|
+
unless limit.is_a?(Integer) && limit.positive?
|
|
66
|
+
raise Prescient::ToolConfigurationError, 'max_results must be a positive integer'
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
[limit, 20].min
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @param value [Object] Requested timeout
|
|
73
|
+
# @return [Numeric] Validated timeout
|
|
74
|
+
def request_timeout(value)
|
|
75
|
+
timeout = value.nil? ? @options.fetch(:timeout, DEFAULT_TIMEOUT) : value
|
|
76
|
+
unless timeout.is_a?(Numeric) && timeout.positive?
|
|
77
|
+
raise Prescient::ToolConfigurationError, 'timeout must be a positive number'
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
timeout
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @return [Integer] Maximum accepted response size
|
|
84
|
+
def max_response_bytes
|
|
85
|
+
value = @options.fetch(:max_response_bytes, DEFAULT_MAX_RESPONSE_BYTES)
|
|
86
|
+
unless value.is_a?(Integer) && value.positive?
|
|
87
|
+
raise Prescient::ToolConfigurationError,
|
|
88
|
+
'max_response_bytes must be a positive integer'
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
value
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Ordered implementations for one logical tool capability.
|
|
96
|
+
class Group < Base
|
|
97
|
+
# @return [Array<Base>] Ordered capability implementations
|
|
98
|
+
attr_reader :adapters
|
|
99
|
+
|
|
100
|
+
# @param adapters [Array<Base>] Ordered capability implementations
|
|
101
|
+
def initialize(adapters:)
|
|
102
|
+
super
|
|
103
|
+
@adapters = adapters
|
|
104
|
+
raise Prescient::ToolConfigurationError, 'tool group requires an adapter' if @adapters.empty?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Execute the first successful adapter, falling back only for transient
|
|
108
|
+
# connection and rate-limit failures.
|
|
109
|
+
# @param query [String] Search query
|
|
110
|
+
# @param options [Hash] Per-request options
|
|
111
|
+
# @return [Hash] Normalized tool result
|
|
112
|
+
def search(query, **options)
|
|
113
|
+
failures = []
|
|
114
|
+
@adapters.each do |adapter|
|
|
115
|
+
return adapter.search(query, **options)
|
|
116
|
+
rescue Prescient::ToolConnectionError, Prescient::RateLimitError => e
|
|
117
|
+
failures << e
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
raise failures.last
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
data/lib/prescient/version.rb
CHANGED
data/lib/prescient.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'prescient/version'
|
|
4
4
|
require_relative 'prescient/errors'
|
|
5
|
+
Prescient.autoload :Tool, 'prescient/tool'
|
|
5
6
|
require_relative 'prescient/pgvector'
|
|
6
7
|
require_relative 'prescient/base'
|
|
7
8
|
require_relative 'prescient/provider/ollama'
|
|
@@ -34,6 +35,7 @@ require_relative 'prescient/client'
|
|
|
34
35
|
# embedding = client.generate_embedding("Some text to embed")
|
|
35
36
|
# puts embedding.length # => 1536 (for OpenAI text-embedding-3-small)
|
|
36
37
|
module Prescient
|
|
38
|
+
autoload :Tool, 'prescient/tool'
|
|
37
39
|
autoload :API, 'prescient/api'
|
|
38
40
|
autoload :CLI, 'prescient/cli'
|
|
39
41
|
|
|
@@ -61,6 +63,13 @@ module Prescient
|
|
|
61
63
|
@_configuration ||= Configuration.new
|
|
62
64
|
end
|
|
63
65
|
|
|
66
|
+
# Look up a configured external tool.
|
|
67
|
+
# @param name [Symbol, String] Tool name
|
|
68
|
+
# @return [Prescient::Tool::Base, nil] Configured tool instance
|
|
69
|
+
def self.tool(name)
|
|
70
|
+
configuration.tool(name)
|
|
71
|
+
end
|
|
72
|
+
|
|
64
73
|
# Reset configuration to defaults
|
|
65
74
|
#
|
|
66
75
|
# @return [Configuration] New configuration instance
|
|
@@ -84,6 +93,7 @@ module Prescient
|
|
|
84
93
|
else
|
|
85
94
|
Configuration.new.tap do |config|
|
|
86
95
|
configure_default_providers(config, env)
|
|
96
|
+
configure_default_tools(config, env)
|
|
87
97
|
end
|
|
88
98
|
end
|
|
89
99
|
|
|
@@ -119,6 +129,9 @@ module Prescient
|
|
|
119
129
|
# @return [Hash] Registered providers configuration
|
|
120
130
|
attr_reader :providers
|
|
121
131
|
|
|
132
|
+
# @return [Hash] Registered tools configuration
|
|
133
|
+
attr_reader :tools
|
|
134
|
+
|
|
122
135
|
# Initialize configuration with default values
|
|
123
136
|
def initialize
|
|
124
137
|
@default_provider = :ollama
|
|
@@ -129,6 +142,8 @@ module Prescient
|
|
|
129
142
|
@sensitive_keys = []
|
|
130
143
|
@providers = {}
|
|
131
144
|
@provider_instances = {} # : Hash[Symbol, untyped]
|
|
145
|
+
@tools = {}
|
|
146
|
+
@tool_instances = {} # : Hash[Symbol, untyped]
|
|
132
147
|
end
|
|
133
148
|
|
|
134
149
|
# Configure additional keys to remove from provider information.
|
|
@@ -177,6 +192,52 @@ module Prescient
|
|
|
177
192
|
@provider_instances[provider_name] ||= provider_config[:class].new(**provider_options)
|
|
178
193
|
end
|
|
179
194
|
|
|
195
|
+
# Register an external tool.
|
|
196
|
+
# @param name [Symbol] Unique identifier for the tool
|
|
197
|
+
# @param tool_class [Class] Tool adapter class
|
|
198
|
+
# @param options [Hash] Tool-specific configuration options
|
|
199
|
+
# @return [void]
|
|
200
|
+
def add_tool(name, tool_class, **options)
|
|
201
|
+
tool_name = name.to_sym
|
|
202
|
+
@tools[tool_name] = {
|
|
203
|
+
class: tool_class,
|
|
204
|
+
options: options,
|
|
205
|
+
}
|
|
206
|
+
@tool_instances.delete(tool_name)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Register ordered implementations for one logical external tool.
|
|
210
|
+
# @param name [Symbol] Unique logical capability identifier
|
|
211
|
+
# @param adapters [Array<Hash>] Adapter class and option configurations
|
|
212
|
+
# @return [void]
|
|
213
|
+
def add_tool_group(name, adapters)
|
|
214
|
+
tool_name = name.to_sym
|
|
215
|
+
@tools[tool_name] = { adapters: adapters }
|
|
216
|
+
@tool_instances.delete(tool_name)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Instantiate a tool by name.
|
|
220
|
+
# @param name [Symbol, String] Tool name
|
|
221
|
+
# @return [Prescient::Tool::Base, nil] Tool instance or nil
|
|
222
|
+
def tool(name)
|
|
223
|
+
tool_name = name.to_sym
|
|
224
|
+
tool_config = @tools[tool_name]
|
|
225
|
+
return nil unless tool_config
|
|
226
|
+
|
|
227
|
+
if tool_config[:adapters]
|
|
228
|
+
@tool_instances[tool_name] ||= Prescient::Tool::Group.new(
|
|
229
|
+
adapters: tool_config[:adapters].map do |adapter|
|
|
230
|
+
adapter_options = adapter[:options] # : Hash[Symbol, untyped]
|
|
231
|
+
adapter[:class].new(**adapter_options)
|
|
232
|
+
end,
|
|
233
|
+
)
|
|
234
|
+
return @tool_instances[tool_name]
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
tool_options = tool_config[:options] # : Hash[Symbol, untyped]
|
|
238
|
+
@tool_instances[tool_name] ||= tool_config[:class].new(**tool_options)
|
|
239
|
+
end
|
|
240
|
+
|
|
180
241
|
# Get list of providers that currently pass {Prescient::Base#available?}.
|
|
181
242
|
#
|
|
182
243
|
# Providers are included when their health check reports `reachable: true`,
|
|
@@ -206,6 +267,12 @@ module Prescient
|
|
|
206
267
|
configure_huggingface(config, env)
|
|
207
268
|
end
|
|
208
269
|
|
|
270
|
+
def configure_default_tools(config, env)
|
|
271
|
+
return unless env['SEARXNG_URL']
|
|
272
|
+
|
|
273
|
+
config.add_tool(:web_search, Prescient::Tool::SearXNG, url: env['SEARXNG_URL'])
|
|
274
|
+
end
|
|
275
|
+
|
|
209
276
|
def configure_ollama(config, env)
|
|
210
277
|
config.add_provider(
|
|
211
278
|
:ollama,
|
|
@@ -307,5 +374,6 @@ module Prescient
|
|
|
307
374
|
# Default configuration
|
|
308
375
|
configure do |config|
|
|
309
376
|
configure_default_providers(config, ENV)
|
|
377
|
+
configure_default_tools(config, ENV)
|
|
310
378
|
end
|
|
311
379
|
end
|
|
@@ -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
|
data/sig/prescient.rbs
CHANGED
|
@@ -33,6 +33,49 @@ module Prescient
|
|
|
33
33
|
class ProviderError < Error
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
class ToolError < Error
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class ToolConfigurationError < ToolError
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class ToolConnectionError < ToolError
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class ToolInvalidResponseError < ToolError
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
module Tool
|
|
49
|
+
class Base
|
|
50
|
+
DEFAULT_TIMEOUT: Integer
|
|
51
|
+
DEFAULT_MAX_RESULTS: Integer
|
|
52
|
+
MAX_QUERY_LENGTH: Integer
|
|
53
|
+
DEFAULT_MAX_RESPONSE_BYTES: Integer
|
|
54
|
+
|
|
55
|
+
attr_reader options: Hash[Symbol, untyped]
|
|
56
|
+
|
|
57
|
+
def initialize: (**untyped) -> void
|
|
58
|
+
def search: (String, **untyped) -> Hash[Symbol, untyped]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class Group < Base
|
|
62
|
+
attr_reader adapters: Array[Base]
|
|
63
|
+
def initialize: (adapters: Array[Base]) -> void
|
|
64
|
+
def search: (String, **untyped) -> Hash[Symbol, untyped]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class SearXNG < Base
|
|
68
|
+
def initialize: (**untyped) -> void
|
|
69
|
+
def search: (String, ?limit: Integer?, ?timeout: Numeric?) -> Hash[Symbol, untyped]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
class SearchApi < Base
|
|
73
|
+
API_URL: String
|
|
74
|
+
def initialize: (**untyped) -> void
|
|
75
|
+
def search: (String, ?limit: Integer?, ?timeout: Numeric?) -> Hash[Symbol, untyped]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
36
79
|
class Base
|
|
37
80
|
attr_reader options: Hash[Symbol, untyped]
|
|
38
81
|
attr_reader provider_name: String
|
|
@@ -257,11 +300,15 @@ module Prescient
|
|
|
257
300
|
attr_accessor fallback_providers: Array[Symbol]
|
|
258
301
|
attr_reader sensitive_keys: Array[Symbol]
|
|
259
302
|
attr_reader providers: Hash[Symbol, Hash[Symbol, untyped]]
|
|
303
|
+
attr_reader tools: Hash[Symbol, Hash[Symbol, untyped]]
|
|
260
304
|
|
|
261
305
|
def initialize: () -> void
|
|
262
306
|
def sensitive_keys=: (Array[Symbol | String]) -> Array[Symbol]
|
|
263
307
|
def add_provider: (Symbol | String, Class, **untyped) -> void
|
|
264
308
|
def provider: (Symbol | String) -> untyped
|
|
309
|
+
def add_tool: (Symbol | String, Class, **untyped) -> void
|
|
310
|
+
def add_tool_group: (Symbol | String, Array[Hash[Symbol, untyped]]) -> void
|
|
311
|
+
def tool: (Symbol | String) -> untyped
|
|
265
312
|
def available_providers: () -> Array[Symbol]
|
|
266
313
|
end
|
|
267
314
|
|
|
@@ -313,6 +360,8 @@ module Prescient
|
|
|
313
360
|
TOP_LEVEL_KEYS: Array[String]
|
|
314
361
|
PROVIDER_TYPES: Hash[String, untyped]
|
|
315
362
|
COMMON_PROVIDER_KEYS: Array[String]
|
|
363
|
+
TOOL_TYPES: Hash[String, untyped]
|
|
364
|
+
COMMON_TOOL_KEYS: Array[String]
|
|
316
365
|
PROMPT_TEMPLATE_KEYS: Array[String]
|
|
317
366
|
ATTR_KEYS: Array[String]
|
|
318
367
|
|
|
@@ -335,11 +384,14 @@ module Prescient
|
|
|
335
384
|
def self.client: (?Symbol, ?enable_fallback: bool, ?provider_options: Hash[Symbol, untyped]) -> Client
|
|
336
385
|
def self.generate_embedding: (String, ?provider: Symbol, ?enable_fallback: bool, **untyped) -> Array[Float]
|
|
337
386
|
def self.generate_response: (String, ?Array[untyped], ?provider: Symbol, ?enable_fallback: bool, **untyped) -> Hash[Symbol, untyped]
|
|
387
|
+
def self.search_and_generate: (String, ?tool: Symbol | String, ?provider: Symbol, ?limit: Integer?, ?enable_fallback: bool, ?provider_options: Hash[Symbol, untyped], **untyped) -> Hash[Symbol, untyped]
|
|
338
388
|
def self.health_check: (?provider: Symbol) -> Hash[Symbol, untyped]
|
|
389
|
+
def self.tool: (Symbol | String) -> untyped
|
|
339
390
|
|
|
340
391
|
private
|
|
341
392
|
|
|
342
393
|
def self.configure_default_providers: (Configuration, untyped) -> void
|
|
394
|
+
def self.configure_default_tools: (Configuration, untyped) -> void
|
|
343
395
|
def self.configure_ollama: (Configuration, untyped) -> void
|
|
344
396
|
def self.configure_openai: (Configuration, untyped) -> void
|
|
345
397
|
def self.configure_anthropic: (Configuration, untyped) -> void
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: prescient
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ken C. Demanawa
|
|
@@ -23,9 +23,10 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: 0.24.0
|
|
26
|
-
description:
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
description: |
|
|
27
|
+
Prescient provides a consistent Ruby API, CLI, or REST API for AI providers including
|
|
28
|
+
Ollama, OpenAI, Anthropic, Hugging Face, Google Gemini, Mistral, DeepSeek, and xAI,
|
|
29
|
+
with provider selection, retries, health checks, and fallback across configured providers.
|
|
29
30
|
email:
|
|
30
31
|
- kenneth.c.demanawa@gmail.com
|
|
31
32
|
executables:
|
|
@@ -60,6 +61,7 @@ files:
|
|
|
60
61
|
- examples/custom_prompts.rb
|
|
61
62
|
- examples/rest_api.ru
|
|
62
63
|
- examples/vector_search.rb
|
|
64
|
+
- examples/web_search.rb
|
|
63
65
|
- exe/prescient
|
|
64
66
|
- lib/prescient.rb
|
|
65
67
|
- lib/prescient/api.rb
|
|
@@ -77,9 +79,13 @@ files:
|
|
|
77
79
|
- lib/prescient/provider/ollama.rb
|
|
78
80
|
- lib/prescient/provider/openai.rb
|
|
79
81
|
- lib/prescient/provider/xai.rb
|
|
82
|
+
- lib/prescient/tool.rb
|
|
83
|
+
- lib/prescient/tool/search_api.rb
|
|
84
|
+
- lib/prescient/tool/searxng.rb
|
|
80
85
|
- lib/prescient/version.rb
|
|
81
86
|
- schema/prescient.configuration.schema.json
|
|
82
87
|
- scripts/setup-ollama-models.sh
|
|
88
|
+
- searxng/settings.yml
|
|
83
89
|
- sig/prescient.rbs
|
|
84
90
|
homepage: https://kanutocd.github.io/prescient
|
|
85
91
|
licenses:
|
|
@@ -106,5 +112,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
106
112
|
requirements: []
|
|
107
113
|
rubygems_version: 4.0.16
|
|
108
114
|
specification_version: 4
|
|
109
|
-
summary: A boring AI provider
|
|
115
|
+
summary: A boring AI provider gateway for Ruby
|
|
110
116
|
test_files: []
|