prescient 0.7.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 +37 -0
- data/INTEGRATION_GUIDE.md +7 -1
- data/README.md +210 -1
- data/Steepfile +12 -12
- data/db/migrate/001_create_prescient_tables.rb +15 -16
- data/examples/README.md +2 -1
- data/examples/custom_contexts.rb +4 -4
- 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 +337 -274
- data/lib/prescient/base.rb +370 -372
- data/lib/prescient/cli.rb +586 -526
- data/lib/prescient/client.rb +7 -6
- data/lib/prescient/configuration_loader.rb +492 -488
- data/lib/prescient/document_source.rb +114 -0
- data/lib/prescient/errors.rb +1 -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 -169
- data/lib/prescient/provider/xai.rb +122 -118
- data/lib/prescient/tool/search_api.rb +125 -121
- data/lib/prescient/tool/searxng.rb +123 -119
- data/lib/prescient/tool.rb +100 -98
- data/lib/prescient/version.rb +1 -1
- data/lib/prescient.rb +68 -62
- data/sig/prescient.rbs +176 -1
- metadata +23 -1
|
@@ -1,171 +1,175 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "httparty"
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
module Prescient
|
|
6
|
+
module Provider
|
|
7
|
+
# Mistral AI API provider adapter.
|
|
8
|
+
class Mistral < Prescient::Base
|
|
9
|
+
include HTTParty
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
base_uri "https://api.mistral.ai"
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
# Generate an embedding through Mistral's embeddings API.
|
|
17
|
-
# @param text [String] Text to embed
|
|
18
|
-
# @return [Array<Float>] Embedding vector
|
|
19
|
-
def generate_embedding(text, **options)
|
|
20
|
-
handle_errors do
|
|
21
|
-
embedding_model = options[:model] || @options[:embedding_model]
|
|
22
|
-
response = self.class.post(
|
|
23
|
-
'/v1/embeddings',
|
|
24
|
-
headers: api_headers,
|
|
25
|
-
body: {
|
|
26
|
-
model: embedding_model,
|
|
27
|
-
input: clean_text(text),
|
|
28
|
-
}.to_json,
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
validate_response!(response, 'embedding generation')
|
|
32
|
-
|
|
33
|
-
embedding = response.parsed_response.dig('data', 0, 'embedding')
|
|
34
|
-
raise Prescient::InvalidResponseError, 'No embedding returned' unless embedding.is_a?(Array)
|
|
35
|
-
|
|
36
|
-
expected_dimensions = @options[:embedding_dimensions]
|
|
37
|
-
expected_dimensions ? validate_embedding_dimensions(embedding, expected_dimensions) : embedding
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# Generate a response through Mistral's chat completions API.
|
|
42
|
-
# @param prompt [String] Prompt to send
|
|
43
|
-
# @param context_items [Array<Hash, String>] Optional context items
|
|
44
|
-
# @return [Hash] Normalized response data
|
|
45
|
-
def generate_response(prompt, context_items = [], **options)
|
|
46
|
-
handle_errors do
|
|
47
|
-
model = options[:model] || @options[:chat_model]
|
|
48
|
-
response = self.class.post(
|
|
49
|
-
'/v1/chat/completions',
|
|
50
|
-
headers: api_headers,
|
|
51
|
-
body: {
|
|
52
|
-
model: model,
|
|
53
|
-
messages: [{ role: 'user', content: build_prompt(prompt, context_items) }],
|
|
54
|
-
max_tokens: options[:max_tokens] || 2000,
|
|
55
|
-
temperature: options[:temperature] || 0.7,
|
|
56
|
-
top_p: options[:top_p] || 0.9,
|
|
57
|
-
}.to_json,
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
validate_response!(response, 'text generation')
|
|
61
|
-
|
|
62
|
-
parsed_response = response.parsed_response
|
|
63
|
-
content = normalize_content(parsed_response.dig('choices', 0, 'message', 'content'))
|
|
64
|
-
raise Prescient::InvalidResponseError, 'No response generated' if content.nil? || content.empty?
|
|
65
|
-
|
|
66
|
-
{
|
|
67
|
-
response: content.strip,
|
|
68
|
-
model: model,
|
|
69
|
-
provider: 'mistral',
|
|
70
|
-
processing_time: nil,
|
|
71
|
-
metadata: {
|
|
72
|
-
usage: parsed_response['usage'],
|
|
73
|
-
finish_reason: parsed_response.dig('choices', 0, 'finish_reason'),
|
|
74
|
-
},
|
|
75
|
-
}
|
|
76
|
-
end
|
|
77
|
-
end
|
|
13
|
+
def initialize(**options)
|
|
14
|
+
super
|
|
15
|
+
self.class.default_timeout(@options[:timeout] || 60)
|
|
16
|
+
end
|
|
78
17
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
18
|
+
# Generate an embedding through Mistral's embeddings API.
|
|
19
|
+
# @param text [String] Text to embed
|
|
20
|
+
# @return [Array<Float>] Embedding vector
|
|
21
|
+
def generate_embedding(text, **options)
|
|
22
|
+
handle_errors do
|
|
23
|
+
embedding_model = options[:model] || @options[:embedding_model]
|
|
24
|
+
response = self.class.post(
|
|
25
|
+
"/v1/embeddings",
|
|
26
|
+
headers: api_headers,
|
|
27
|
+
body: {
|
|
28
|
+
model: embedding_model,
|
|
29
|
+
input: clean_text(text)
|
|
30
|
+
}.to_json
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
validate_response!(response, "embedding generation")
|
|
34
|
+
|
|
35
|
+
embedding = response.parsed_response.dig("data", 0, "embedding")
|
|
36
|
+
raise Prescient::InvalidResponseError, "No embedding returned" unless embedding.is_a?(Array)
|
|
37
|
+
|
|
38
|
+
expected_dimensions = @options[:embedding_dimensions]
|
|
39
|
+
expected_dimensions ? validate_embedding_dimensions(embedding, expected_dimensions) : embedding
|
|
40
|
+
end
|
|
41
|
+
end
|
|
84
42
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
43
|
+
# Generate a response through Mistral's chat completions API.
|
|
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
|
+
model = options[:model] || @options[:chat_model]
|
|
50
|
+
response = self.class.post(
|
|
51
|
+
"/v1/chat/completions",
|
|
52
|
+
headers: api_headers,
|
|
53
|
+
body: {
|
|
54
|
+
model: model,
|
|
55
|
+
messages: [{ role: "user", content: build_prompt(prompt, context_items) }],
|
|
56
|
+
max_tokens: options[:max_tokens] || 2000,
|
|
57
|
+
temperature: options[:temperature] || 0.7,
|
|
58
|
+
top_p: options[:top_p] || 0.9
|
|
59
|
+
}.to_json
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
validate_response!(response, "text generation")
|
|
63
|
+
|
|
64
|
+
parsed_response = response.parsed_response
|
|
65
|
+
content = normalize_content(parsed_response.dig("choices", 0, "message", "content"))
|
|
66
|
+
raise Prescient::InvalidResponseError, "No response generated" if content.nil? || content.empty?
|
|
67
|
+
|
|
68
|
+
{
|
|
69
|
+
response: content.strip,
|
|
70
|
+
model: model,
|
|
71
|
+
provider: "mistral",
|
|
72
|
+
processing_time: nil,
|
|
73
|
+
metadata: {
|
|
74
|
+
usage: parsed_response["usage"],
|
|
75
|
+
finish_reason: parsed_response.dig("choices", 0, "finish_reason")
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
end
|
|
89
80
|
|
|
81
|
+
# Check whether the configured Mistral models are available.
|
|
82
|
+
# @return [Hash] Provider health information
|
|
83
|
+
def health_check
|
|
84
|
+
handle_errors do
|
|
85
|
+
response = self.class.get("/v1/models", headers: api_headers)
|
|
86
|
+
|
|
87
|
+
if response.success?
|
|
88
|
+
models = response.parsed_response["data"] || []
|
|
89
|
+
embedding_available = model_available?(models, @options[:embedding_model])
|
|
90
|
+
chat_available = model_available?(models, @options[:chat_model])
|
|
91
|
+
|
|
92
|
+
{
|
|
93
|
+
status: "healthy",
|
|
94
|
+
provider: "mistral",
|
|
95
|
+
reachable: true,
|
|
96
|
+
models_available: models.map { |model| model["id"] },
|
|
97
|
+
embedding_model: { name: @options[:embedding_model], available: embedding_available },
|
|
98
|
+
chat_model: { name: @options[:chat_model], available: chat_available },
|
|
99
|
+
ready: embedding_available && chat_available
|
|
100
|
+
}
|
|
101
|
+
else
|
|
102
|
+
{
|
|
103
|
+
status: "unhealthy",
|
|
104
|
+
provider: "mistral",
|
|
105
|
+
reachable: true,
|
|
106
|
+
error: "HTTP #{response.code}",
|
|
107
|
+
message: response.message,
|
|
108
|
+
ready: false
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
rescue Prescient::Error => e
|
|
90
113
|
{
|
|
91
|
-
status:
|
|
92
|
-
provider:
|
|
93
|
-
reachable:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
ready: embedding_available && chat_available,
|
|
98
|
-
}
|
|
99
|
-
else
|
|
100
|
-
{
|
|
101
|
-
status: 'unhealthy',
|
|
102
|
-
provider: 'mistral',
|
|
103
|
-
reachable: true,
|
|
104
|
-
error: "HTTP #{response.code}",
|
|
105
|
-
message: response.message,
|
|
106
|
-
ready: false,
|
|
114
|
+
status: "unavailable",
|
|
115
|
+
provider: "mistral",
|
|
116
|
+
reachable: false,
|
|
117
|
+
error: e.class.name,
|
|
118
|
+
message: e.message,
|
|
119
|
+
ready: false
|
|
107
120
|
}
|
|
108
121
|
end
|
|
109
|
-
end
|
|
110
|
-
rescue Prescient::Error => e
|
|
111
|
-
{
|
|
112
|
-
status: 'unavailable',
|
|
113
|
-
provider: 'mistral',
|
|
114
|
-
reachable: false,
|
|
115
|
-
error: e.class.name,
|
|
116
|
-
message: e.message,
|
|
117
|
-
ready: false,
|
|
118
|
-
}
|
|
119
|
-
end
|
|
120
122
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
123
|
+
# List models available to the configured Mistral API key.
|
|
124
|
+
# @return [Array<Hash>] Model descriptors
|
|
125
|
+
def list_models
|
|
126
|
+
handle_errors do
|
|
127
|
+
response = self.class.get("/v1/models", headers: api_headers)
|
|
128
|
+
validate_response!(response, "model listing")
|
|
129
|
+
|
|
130
|
+
(response.parsed_response["data"] || []).map do |model|
|
|
131
|
+
{
|
|
132
|
+
name: model["id"],
|
|
133
|
+
object: model["object"],
|
|
134
|
+
created: model["created"],
|
|
135
|
+
owned_by: model["owned_by"],
|
|
136
|
+
capabilities: model["capabilities"],
|
|
137
|
+
max_context_length: model["max_context_length"]
|
|
138
|
+
}.compact
|
|
139
|
+
end
|
|
140
|
+
end
|
|
137
141
|
end
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
142
|
|
|
141
|
-
|
|
143
|
+
protected
|
|
142
144
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
145
|
+
def validate_configuration!
|
|
146
|
+
required_options = %i[api_key embedding_model chat_model]
|
|
147
|
+
missing_options = required_options.select { |option| @options[option].nil? }
|
|
146
148
|
|
|
147
|
-
|
|
149
|
+
return unless missing_options.any?
|
|
148
150
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
+
raise Prescient::Error, "Missing required options: #{missing_options.join(", ")}"
|
|
152
|
+
end
|
|
151
153
|
|
|
152
|
-
|
|
154
|
+
private
|
|
153
155
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
156
|
+
def api_headers
|
|
157
|
+
{
|
|
158
|
+
"Content-Type" => "application/json",
|
|
159
|
+
"Authorization" => "Bearer #{@options[:api_key]}"
|
|
160
|
+
}
|
|
161
|
+
end
|
|
160
162
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
163
|
+
def model_available?(models, model_name)
|
|
164
|
+
models.any? { |model| model["id"] == model_name }
|
|
165
|
+
end
|
|
164
166
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
167
|
+
def normalize_content(content)
|
|
168
|
+
return content if content.is_a?(String)
|
|
169
|
+
return unless content.is_a?(Array)
|
|
168
170
|
|
|
169
|
-
|
|
171
|
+
content.filter_map { |part| part["text"] if part.is_a?(Hash) }.join
|
|
172
|
+
end
|
|
173
|
+
end
|
|
170
174
|
end
|
|
171
175
|
end
|