prescient 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +21 -268
- data/CHANGELOG.md +59 -0
- data/INTEGRATION_GUIDE.md +19 -1
- data/README.md +369 -21
- data/Steepfile +12 -12
- data/db/migrate/001_create_prescient_tables.rb +15 -16
- data/docker-compose.yml +22 -0
- data/examples/README.md +36 -1
- data/examples/custom_contexts.rb +4 -4
- data/examples/web_search.rb +34 -0
- data/exe/prescient +2 -2
- data/exe/prescient-mcp +7 -0
- data/lib/prescient/agent/audit_log.rb +37 -0
- data/lib/prescient/agent/cli_adapter.rb +29 -0
- data/lib/prescient/agent/configuration.rb +57 -0
- data/lib/prescient/agent/context.rb +56 -0
- data/lib/prescient/agent/error_serializer.rb +47 -0
- data/lib/prescient/agent/errors.rb +25 -0
- data/lib/prescient/agent/parser.rb +49 -0
- data/lib/prescient/agent/prompt_builder.rb +31 -0
- data/lib/prescient/agent/result.rb +36 -0
- data/lib/prescient/agent/runtime.rb +175 -0
- data/lib/prescient/agent/schema_validator.rb +215 -0
- data/lib/prescient/agent/tool_registry.rb +89 -0
- data/lib/prescient/agent.rb +22 -0
- data/lib/prescient/api.rb +346 -231
- data/lib/prescient/base.rb +370 -372
- data/lib/prescient/cli.rb +591 -402
- data/lib/prescient/client.rb +31 -4
- data/lib/prescient/configuration_loader.rb +511 -336
- data/lib/prescient/document_source.rb +114 -0
- data/lib/prescient/errors.rb +13 -3
- data/lib/prescient/mcp/authentication.rb +39 -0
- data/lib/prescient/mcp/configuration.rb +38 -0
- data/lib/prescient/mcp/rack.rb +243 -0
- data/lib/prescient/mcp/server.rb +202 -0
- data/lib/prescient/mcp/stdio.rb +42 -0
- data/lib/prescient/mcp.rb +8 -0
- data/lib/prescient/pgvector.rb +193 -189
- data/lib/prescient/provider/anthropic.rb +129 -125
- data/lib/prescient/provider/deepseek.rb +122 -118
- data/lib/prescient/provider/gemini.rb +153 -149
- data/lib/prescient/provider/huggingface.rb +191 -187
- data/lib/prescient/provider/mistral.rb +151 -147
- data/lib/prescient/provider/ollama.rb +168 -165
- data/lib/prescient/provider/openai.rb +174 -171
- data/lib/prescient/provider/xai.rb +122 -118
- data/lib/prescient/tool/search_api.rb +130 -0
- data/lib/prescient/tool/searxng.rb +128 -0
- data/lib/prescient/tool.rb +125 -0
- data/lib/prescient/version.rb +1 -1
- data/lib/prescient.rb +129 -55
- data/schema/prescient.configuration.schema.json +119 -0
- data/searxng/settings.yml +18 -0
- data/sig/prescient.rbs +228 -1
- metadata +33 -5
|
@@ -1,183 +1,186 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
# Generate an embedding through Ollama's `/api/embed` endpoint.
|
|
16
|
-
# @param text [String] Text to embed
|
|
17
|
-
# @return [Array<Float>] Embedding vector from the first returned input item
|
|
18
|
-
def generate_embedding(text, **_options)
|
|
19
|
-
handle_errors do
|
|
20
|
-
embeddings = fetch_and_parse('post', '/api/embed',
|
|
21
|
-
root_key: 'embeddings',
|
|
22
|
-
headers: { 'Content-Type' => 'application/json' },
|
|
23
|
-
body: {
|
|
24
|
-
model: options[:model] || @options[:embedding_model],
|
|
25
|
-
input: clean_text(text),
|
|
26
|
-
}.to_json)
|
|
27
|
-
|
|
28
|
-
embedding = embeddings.is_a?(Array) ? embeddings.first : nil # : Array[Float]?
|
|
29
|
-
raise Prescient::InvalidResponseError, 'No embedding returned' unless embedding.is_a?(Array)
|
|
30
|
-
|
|
31
|
-
expected_dimensions = @options[:embedding_dimensions]
|
|
32
|
-
if expected_dimensions && embedding.length != expected_dimensions
|
|
33
|
-
raise Prescient::InvalidResponseError,
|
|
34
|
-
"Invalid embedding dimensions: expected #{expected_dimensions}, got #{embedding.length}"
|
|
3
|
+
require "httparty"
|
|
4
|
+
|
|
5
|
+
module Prescient
|
|
6
|
+
module Provider
|
|
7
|
+
# Ollama local or hosted API provider adapter.
|
|
8
|
+
class Ollama < Prescient::Base
|
|
9
|
+
include HTTParty
|
|
10
|
+
|
|
11
|
+
def initialize(**options)
|
|
12
|
+
super
|
|
13
|
+
self.class.base_uri(@options[:url])
|
|
14
|
+
self.class.default_timeout(@options[:timeout] || 60)
|
|
35
15
|
end
|
|
36
16
|
|
|
37
|
-
embedding
|
|
38
|
-
|
|
39
|
-
|
|
17
|
+
# Generate an embedding through Ollama's `/api/embed` endpoint.
|
|
18
|
+
# @param text [String] Text to embed
|
|
19
|
+
# @return [Array<Float>] Embedding vector from the first returned input item
|
|
20
|
+
def generate_embedding(text, **_options)
|
|
21
|
+
handle_errors do
|
|
22
|
+
embeddings = fetch_and_parse("post", "/api/embed",
|
|
23
|
+
root_key: "embeddings",
|
|
24
|
+
headers: { "Content-Type" => "application/json" },
|
|
25
|
+
body: {
|
|
26
|
+
model: options[:model] || @options[:embedding_model],
|
|
27
|
+
input: clean_text(text)
|
|
28
|
+
}.to_json)
|
|
29
|
+
|
|
30
|
+
embedding = embeddings.is_a?(Array) ? embeddings.first : nil # : Array[Float]?
|
|
31
|
+
raise Prescient::InvalidResponseError, "No embedding returned" unless embedding.is_a?(Array)
|
|
32
|
+
|
|
33
|
+
expected_dimensions = @options[:embedding_dimensions]
|
|
34
|
+
if expected_dimensions && embedding.length != expected_dimensions
|
|
35
|
+
raise Prescient::InvalidResponseError,
|
|
36
|
+
"Invalid embedding dimensions: expected #{expected_dimensions}, got #{embedding.length}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
embedding
|
|
40
|
+
end
|
|
41
|
+
end
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
43
|
+
# Generate text through Ollama's generation endpoint.
|
|
44
|
+
# @param prompt [String] Prompt to send
|
|
45
|
+
# @param context_items [Array<Hash, String>] Optional context items
|
|
46
|
+
# @return [Hash] Normalized response data
|
|
47
|
+
def generate_response(prompt, context_items = [], **options)
|
|
48
|
+
handle_errors do
|
|
49
|
+
request_options = prepare_generate_response(prompt, context_items, **options)
|
|
50
|
+
|
|
51
|
+
# Make the request and store both text and full response
|
|
52
|
+
response = self.class.post("/api/generate", **request_options)
|
|
53
|
+
validate_response!(response, "POST /api/generate")
|
|
54
|
+
|
|
55
|
+
generated_text = response.parsed_response["response"]
|
|
56
|
+
raise Prescient::InvalidResponseError, "No response generated" unless generated_text
|
|
57
|
+
|
|
58
|
+
{
|
|
59
|
+
response: generated_text.strip,
|
|
60
|
+
model: options[:model] || @options[:chat_model],
|
|
61
|
+
provider: "ollama",
|
|
62
|
+
processing_time: response.parsed_response["total_duration"]&./(1_000_000_000.0),
|
|
63
|
+
metadata: {
|
|
64
|
+
eval_count: response.parsed_response["eval_count"],
|
|
65
|
+
eval_duration: response.parsed_response["eval_duration"],
|
|
66
|
+
prompt_eval_count: response.parsed_response["prompt_eval_count"]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
end
|
|
69
71
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
72
|
+
# Check whether the configured Ollama models are available locally.
|
|
73
|
+
#
|
|
74
|
+
# `reachable` indicates the Ollama API answered successfully. `ready`
|
|
75
|
+
# indicates that both configured models are present in the local model list.
|
|
76
|
+
#
|
|
77
|
+
# @return [Hash] Provider health information
|
|
78
|
+
def health_check
|
|
79
|
+
handle_errors do
|
|
80
|
+
models = available_models
|
|
81
|
+
embedding_available = models.any? { |m| m[:embedding] }
|
|
82
|
+
chat_available = models.any? { |m| m[:chat] }
|
|
83
|
+
|
|
84
|
+
{
|
|
85
|
+
status: "healthy",
|
|
86
|
+
provider: "ollama",
|
|
87
|
+
reachable: true,
|
|
88
|
+
url: @options[:url],
|
|
89
|
+
models_available: models.map { |m| m[:name] },
|
|
90
|
+
embedding_model: {
|
|
91
|
+
name: @options[:embedding_model],
|
|
92
|
+
available: embedding_available
|
|
93
|
+
},
|
|
94
|
+
chat_model: {
|
|
95
|
+
name: @options[:chat_model],
|
|
96
|
+
available: chat_available
|
|
97
|
+
},
|
|
98
|
+
ready: embedding_available && chat_available
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
rescue Prescient::Error => e
|
|
102
|
+
{
|
|
103
|
+
status: "unavailable",
|
|
104
|
+
provider: "ollama",
|
|
105
|
+
reachable: false,
|
|
106
|
+
error: e.class.name,
|
|
107
|
+
message: e.message,
|
|
108
|
+
url: @options[:url],
|
|
109
|
+
ready: false
|
|
110
|
+
}
|
|
111
|
+
end
|
|
110
112
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
113
|
+
# List models currently available from Ollama.
|
|
114
|
+
# @return [Array<Hash>] Model descriptors including name, size, digest,
|
|
115
|
+
# modified_at, and booleans for configured embedding/chat roles
|
|
116
|
+
def available_models
|
|
117
|
+
return @_available_models if defined?(@_available_models)
|
|
118
|
+
|
|
119
|
+
handle_errors do
|
|
120
|
+
@_available_models = (fetch_and_parse("get", "/api/tags", root_key: "models") || []).map do |model|
|
|
121
|
+
{ embedding: model["name"] == @options[:embedding_model],
|
|
122
|
+
chat: model["name"] == @options[:chat_model],
|
|
123
|
+
name: model["name"], size: model["size"], modified_at: model["modified_at"], digest: model["digest"] }
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
125
127
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
128
|
+
# Pull a model into the Ollama installation.
|
|
129
|
+
# @param model_name [String] Model identifier to download
|
|
130
|
+
# @return [Hash] Pull result
|
|
131
|
+
def pull_model(model_name)
|
|
132
|
+
handle_errors do
|
|
133
|
+
fetch_and_parse("post", "/api/pull",
|
|
134
|
+
headers: { "Content-Type" => "application/json" },
|
|
135
|
+
body: { name: model_name }.to_json,
|
|
136
|
+
timeout: 300) # 5 minutes for model download
|
|
137
|
+
{
|
|
138
|
+
success: true,
|
|
139
|
+
model: model_name,
|
|
140
|
+
message: "Model #{model_name} pulled successfully"
|
|
141
|
+
}
|
|
142
|
+
end
|
|
143
|
+
end
|
|
142
144
|
|
|
143
|
-
|
|
145
|
+
protected
|
|
144
146
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
def validate_configuration!
|
|
148
|
+
required_options = %i[url embedding_model chat_model]
|
|
149
|
+
missing_options = required_options.select { |opt| @options[opt].nil? }
|
|
148
150
|
|
|
149
|
-
|
|
151
|
+
return unless missing_options.any?
|
|
150
152
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
+
raise Prescient::Error, "Missing required options: #{missing_options.join(", ")}"
|
|
154
|
+
end
|
|
153
155
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
156
|
+
private
|
|
157
|
+
|
|
158
|
+
def prepare_generate_response(prompt, context_items = [], **options)
|
|
159
|
+
formatted_prompt = build_prompt(prompt, context_items)
|
|
160
|
+
{ root_key: "response",
|
|
161
|
+
headers: { "Content-Type" => "application/json" },
|
|
162
|
+
body: {
|
|
163
|
+
model: options[:model] || @options[:chat_model],
|
|
164
|
+
prompt: formatted_prompt,
|
|
165
|
+
stream: false,
|
|
166
|
+
options: {
|
|
167
|
+
num_predict: options[:max_tokens] || 2000,
|
|
168
|
+
temperature: options[:temperature] || 0.7,
|
|
169
|
+
top_p: options[:top_p] || 0.9
|
|
170
|
+
}
|
|
171
|
+
}.to_json }
|
|
172
|
+
end
|
|
171
173
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
174
|
+
def fetch_and_parse(htt_verb, endpoint, **options)
|
|
175
|
+
options = options.dup
|
|
176
|
+
root_key = options.delete(:root_key)
|
|
175
177
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
178
|
+
response = self.class.send(htt_verb, endpoint, **options)
|
|
179
|
+
validate_response!(response, "#{htt_verb.upcase} #{endpoint}")
|
|
180
|
+
return unless root_key
|
|
179
181
|
|
|
180
|
-
|
|
182
|
+
response.parsed_response[root_key]
|
|
183
|
+
end
|
|
184
|
+
end
|
|
181
185
|
end
|
|
182
|
-
|
|
183
186
|
end
|