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,190 +1,195 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "httparty"
|
|
4
|
+
|
|
5
|
+
module Prescient
|
|
6
|
+
module Provider
|
|
7
|
+
# OpenAI API provider adapter.
|
|
8
|
+
class OpenAI < Prescient::Base
|
|
9
|
+
include HTTParty
|
|
10
|
+
|
|
11
|
+
base_uri "https://api.openai.com"
|
|
12
|
+
|
|
13
|
+
# Known embedding dimensions for OpenAI embedding models.
|
|
14
|
+
EMBEDDING_DIMENSIONS = {
|
|
15
|
+
"text-embedding-3-small" => 1536,
|
|
16
|
+
"text-embedding-3-large" => 3072,
|
|
17
|
+
"text-embedding-ada-002" => 1536
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
def initialize(**options)
|
|
21
|
+
super
|
|
22
|
+
@provider_name = "OpenAI"
|
|
23
|
+
self.class.default_timeout(@options[:timeout] || 60)
|
|
24
|
+
end
|
|
4
25
|
|
|
5
|
-
# OpenAI API
|
|
6
|
-
|
|
7
|
-
|
|
26
|
+
# Generate an embedding through the OpenAI embeddings API.
|
|
27
|
+
# @param text [String] Text to embed
|
|
28
|
+
# @return [Array<Float>] Embedding vector
|
|
29
|
+
def generate_embedding(text, **options)
|
|
30
|
+
handle_errors do
|
|
31
|
+
clean_text_input = clean_text(text)
|
|
32
|
+
|
|
33
|
+
embedding_model = options[:model] || @options[:embedding_model]
|
|
34
|
+
response = self.class.post("/v1/embeddings",
|
|
35
|
+
headers: {
|
|
36
|
+
"Content-Type" => "application/json",
|
|
37
|
+
"Authorization" => "Bearer #{@options[:api_key]}"
|
|
38
|
+
},
|
|
39
|
+
body: {
|
|
40
|
+
model: embedding_model,
|
|
41
|
+
input: clean_text_input,
|
|
42
|
+
encoding_format: "float"
|
|
43
|
+
}.to_json)
|
|
8
44
|
|
|
9
|
-
|
|
45
|
+
validate_response!(response, "embedding generation")
|
|
10
46
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
'text-embedding-3-small' => 1536,
|
|
14
|
-
'text-embedding-3-large' => 3072,
|
|
15
|
-
'text-embedding-ada-002' => 1536,
|
|
16
|
-
}.freeze
|
|
47
|
+
embedding_data = response.parsed_response.dig("data", 0, "embedding")
|
|
48
|
+
raise Prescient::InvalidResponseError, "No embedding returned" unless embedding_data
|
|
17
49
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
50
|
+
expected_dimensions = EMBEDDING_DIMENSIONS[embedding_model] || @options[:embedding_dimensions]
|
|
51
|
+
unless expected_dimensions
|
|
52
|
+
raise Prescient::Error,
|
|
53
|
+
"Embedding dimensions are required for model #{embedding_model}"
|
|
54
|
+
end
|
|
23
55
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
# @return [Array<Float>] Embedding vector
|
|
27
|
-
def generate_embedding(text, **options)
|
|
28
|
-
handle_errors do
|
|
29
|
-
clean_text_input = clean_text(text)
|
|
30
|
-
|
|
31
|
-
embedding_model = options[:model] || @options[:embedding_model]
|
|
32
|
-
response = self.class.post('/v1/embeddings',
|
|
33
|
-
headers: {
|
|
34
|
-
'Content-Type' => 'application/json',
|
|
35
|
-
'Authorization' => "Bearer #{@options[:api_key]}",
|
|
36
|
-
},
|
|
37
|
-
body: {
|
|
38
|
-
model: embedding_model,
|
|
39
|
-
input: clean_text_input,
|
|
40
|
-
encoding_format: 'float',
|
|
41
|
-
}.to_json)
|
|
42
|
-
|
|
43
|
-
validate_response!(response, 'embedding generation')
|
|
44
|
-
|
|
45
|
-
embedding_data = response.parsed_response.dig('data', 0, 'embedding')
|
|
46
|
-
raise Prescient::InvalidResponseError, 'No embedding returned' unless embedding_data
|
|
47
|
-
|
|
48
|
-
expected_dimensions = EMBEDDING_DIMENSIONS[embedding_model] || @options[:embedding_dimensions]
|
|
49
|
-
unless expected_dimensions
|
|
50
|
-
raise Prescient::Error,
|
|
51
|
-
"Embedding dimensions are required for model #{embedding_model}"
|
|
56
|
+
validate_embedding_dimensions(embedding_data, expected_dimensions)
|
|
57
|
+
end
|
|
52
58
|
end
|
|
53
59
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
response = self.class.post('/v1/chat/completions',
|
|
67
|
-
headers: {
|
|
68
|
-
'Content-Type' => 'application/json',
|
|
69
|
-
'Authorization' => "Bearer #{@options[:api_key]}",
|
|
70
|
-
},
|
|
71
|
-
body: {
|
|
72
|
-
model: options[:model] || @options[:chat_model],
|
|
73
|
-
messages: [
|
|
74
|
-
{
|
|
75
|
-
role: 'user',
|
|
76
|
-
content: formatted_prompt,
|
|
60
|
+
# Generate a response through the OpenAI chat completions API.
|
|
61
|
+
# @param prompt [String] Prompt to send
|
|
62
|
+
# @param context_items [Array<Hash, String>] Optional context items
|
|
63
|
+
# @return [Hash] Normalized response data
|
|
64
|
+
def generate_response(prompt, context_items = [], **options)
|
|
65
|
+
handle_errors do
|
|
66
|
+
formatted_prompt = build_prompt(prompt, context_items)
|
|
67
|
+
|
|
68
|
+
response = self.class.post("/v1/chat/completions",
|
|
69
|
+
headers: {
|
|
70
|
+
"Content-Type" => "application/json",
|
|
71
|
+
"Authorization" => "Bearer #{@options[:api_key]}"
|
|
77
72
|
},
|
|
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
|
-
# @return [Hash] Provider health information
|
|
108
|
-
def health_check
|
|
109
|
-
handle_errors do
|
|
110
|
-
response = self.class.get('/v1/models',
|
|
111
|
-
headers: {
|
|
112
|
-
'Authorization' => "Bearer #{@options[:api_key]}",
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
if response.success?
|
|
116
|
-
models = response.parsed_response['data'] || []
|
|
117
|
-
embedding_available = models.any? { |m| m['id'] == @options[:embedding_model] }
|
|
118
|
-
chat_available = models.any? { |m| m['id'] == @options[:chat_model] }
|
|
119
|
-
|
|
120
|
-
{
|
|
121
|
-
status: 'healthy',
|
|
122
|
-
provider: 'openai',
|
|
123
|
-
reachable: true,
|
|
124
|
-
models_available: models.map { |m| m['id'] },
|
|
125
|
-
embedding_model: {
|
|
126
|
-
name: @options[:embedding_model],
|
|
127
|
-
available: embedding_available,
|
|
128
|
-
},
|
|
129
|
-
chat_model: {
|
|
130
|
-
name: @options[:chat_model],
|
|
131
|
-
available: chat_available,
|
|
132
|
-
},
|
|
133
|
-
ready: embedding_available && chat_available,
|
|
134
|
-
}
|
|
135
|
-
else
|
|
136
|
-
{
|
|
137
|
-
status: 'unhealthy',
|
|
138
|
-
provider: 'openai',
|
|
139
|
-
reachable: true,
|
|
140
|
-
error: "HTTP #{response.code}",
|
|
141
|
-
message: response.message,
|
|
142
|
-
ready: false,
|
|
143
|
-
}
|
|
73
|
+
body: {
|
|
74
|
+
model: options[:model] || @options[:chat_model],
|
|
75
|
+
messages: [
|
|
76
|
+
{
|
|
77
|
+
role: "user",
|
|
78
|
+
content: formatted_prompt
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
max_tokens: options[:max_tokens] || 2000,
|
|
82
|
+
temperature: options[:temperature] || 0.7,
|
|
83
|
+
top_p: options[:top_p] || 0.9
|
|
84
|
+
}.to_json)
|
|
85
|
+
|
|
86
|
+
validate_response!(response, "text generation")
|
|
87
|
+
|
|
88
|
+
content = response.parsed_response.dig("choices", 0, "message", "content")
|
|
89
|
+
raise Prescient::InvalidResponseError, "No response generated" unless content
|
|
90
|
+
|
|
91
|
+
{
|
|
92
|
+
response: content.strip,
|
|
93
|
+
model: options[:model] || @options[:chat_model],
|
|
94
|
+
provider: "openai",
|
|
95
|
+
processing_time: nil,
|
|
96
|
+
metadata: {
|
|
97
|
+
usage: response.parsed_response["usage"],
|
|
98
|
+
finish_reason: response.parsed_response.dig("choices", 0, "finish_reason")
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
end
|
|
144
102
|
end
|
|
145
|
-
end
|
|
146
|
-
rescue Prescient::Error => e
|
|
147
|
-
{
|
|
148
|
-
status: 'unavailable',
|
|
149
|
-
provider: 'openai',
|
|
150
|
-
reachable: false,
|
|
151
|
-
error: e.class.name,
|
|
152
|
-
message: e.message,
|
|
153
|
-
ready: false,
|
|
154
|
-
}
|
|
155
|
-
end
|
|
156
103
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
104
|
+
# Check OpenAI model availability via `/v1/models`.
|
|
105
|
+
#
|
|
106
|
+
# `reachable` indicates the API answered successfully. `ready` indicates that
|
|
107
|
+
# both configured models appear in the returned model list.
|
|
108
|
+
#
|
|
109
|
+
# @return [Hash] Provider health information
|
|
110
|
+
# rubocop:disable Metrics/MethodLength
|
|
111
|
+
def health_check
|
|
112
|
+
handle_errors do
|
|
113
|
+
response = self.class.get("/v1/models",
|
|
114
|
+
headers: {
|
|
115
|
+
"Authorization" => "Bearer #{@options[:api_key]}"
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
if response.success?
|
|
119
|
+
models = response.parsed_response["data"] || []
|
|
120
|
+
embedding_available = models.any? { |m| m["id"] == @options[:embedding_model] }
|
|
121
|
+
chat_available = models.any? { |m| m["id"] == @options[:chat_model] }
|
|
122
|
+
|
|
123
|
+
{
|
|
124
|
+
status: "healthy",
|
|
125
|
+
provider: "openai",
|
|
126
|
+
reachable: true,
|
|
127
|
+
models_available: models.map { |m| m["id"] },
|
|
128
|
+
embedding_model: {
|
|
129
|
+
name: @options[:embedding_model],
|
|
130
|
+
available: embedding_available
|
|
131
|
+
},
|
|
132
|
+
chat_model: {
|
|
133
|
+
name: @options[:chat_model],
|
|
134
|
+
available: chat_available
|
|
135
|
+
},
|
|
136
|
+
ready: embedding_available && chat_available
|
|
137
|
+
}
|
|
138
|
+
else
|
|
139
|
+
{
|
|
140
|
+
status: "unhealthy",
|
|
141
|
+
provider: "openai",
|
|
142
|
+
reachable: true,
|
|
143
|
+
error: "HTTP #{response.code}",
|
|
144
|
+
message: response.message,
|
|
145
|
+
ready: false
|
|
146
|
+
}
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
rescue Prescient::Error => e
|
|
170
150
|
{
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
151
|
+
status: "unavailable",
|
|
152
|
+
provider: "openai",
|
|
153
|
+
reachable: false,
|
|
154
|
+
error: e.class.name,
|
|
155
|
+
message: e.message,
|
|
156
|
+
ready: false
|
|
174
157
|
}
|
|
175
158
|
end
|
|
176
|
-
|
|
177
|
-
|
|
159
|
+
# rubocop:enable Metrics/MethodLength
|
|
160
|
+
|
|
161
|
+
# List models available to the configured OpenAI account.
|
|
162
|
+
#
|
|
163
|
+
# @return [Array<Hash>] Model descriptors
|
|
164
|
+
def list_models
|
|
165
|
+
handle_errors do
|
|
166
|
+
response = self.class.get("/v1/models",
|
|
167
|
+
headers: {
|
|
168
|
+
"Authorization" => "Bearer #{@options[:api_key]}"
|
|
169
|
+
})
|
|
170
|
+
validate_response!(response, "model listing")
|
|
171
|
+
|
|
172
|
+
models = response.parsed_response["data"] || []
|
|
173
|
+
models.map do |model|
|
|
174
|
+
{
|
|
175
|
+
name: model["id"],
|
|
176
|
+
created: model["created"],
|
|
177
|
+
owned_by: model["owned_by"]
|
|
178
|
+
}
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
178
182
|
|
|
179
|
-
|
|
183
|
+
protected
|
|
180
184
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
185
|
+
def validate_configuration!
|
|
186
|
+
required_options = %i[api_key embedding_model chat_model]
|
|
187
|
+
missing_options = required_options.select { |opt| @options[opt].nil? }
|
|
184
188
|
|
|
185
|
-
|
|
189
|
+
return unless missing_options.any?
|
|
186
190
|
|
|
187
|
-
|
|
191
|
+
raise Prescient::Error, "Missing required options: #{missing_options.join(", ")}"
|
|
192
|
+
end
|
|
193
|
+
end
|
|
188
194
|
end
|
|
189
|
-
|
|
190
195
|
end
|
|
@@ -1,139 +1,143 @@
|
|
|
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
|
+
# xAI API provider adapter.
|
|
8
|
+
class XAI < Prescient::Base
|
|
9
|
+
include HTTParty
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
base_uri "https://api.x.ai"
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
# xAI does not expose a standard embeddings API for this adapter.
|
|
18
|
-
# @raise [Prescient::Error] Always, because embeddings are unsupported
|
|
19
|
-
def generate_embedding(_text, **_options)
|
|
20
|
-
raise Prescient::Error, 'xAI provider does not support embeddings.'
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# Generate a response through xAI's OpenAI-compatible chat API.
|
|
24
|
-
# @param prompt [String] Prompt to send
|
|
25
|
-
# @param context_items [Array<Hash, String>] Optional context items
|
|
26
|
-
# @return [Hash] Normalized response data
|
|
27
|
-
def generate_response(prompt, context_items = [], **options)
|
|
28
|
-
handle_errors do
|
|
29
|
-
model = options[:model] || @options[:chat_model]
|
|
30
|
-
response = self.class.post(
|
|
31
|
-
'/v1/chat/completions',
|
|
32
|
-
headers: api_headers,
|
|
33
|
-
body: {
|
|
34
|
-
model: model,
|
|
35
|
-
messages: [{ role: 'user', content: build_prompt(prompt, context_items) }],
|
|
36
|
-
max_tokens: options[:max_tokens] || 2000,
|
|
37
|
-
temperature: options[:temperature] || 0.7,
|
|
38
|
-
top_p: options[:top_p] || 0.9,
|
|
39
|
-
}.to_json,
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
validate_response!(response, 'text generation')
|
|
43
|
-
|
|
44
|
-
parsed_response = response.parsed_response
|
|
45
|
-
content = parsed_response.dig('choices', 0, 'message', 'content')
|
|
46
|
-
raise Prescient::InvalidResponseError, 'No response generated' unless content.is_a?(String) && !content.empty?
|
|
47
|
-
|
|
48
|
-
{
|
|
49
|
-
response: content.strip,
|
|
50
|
-
model: model,
|
|
51
|
-
provider: 'xai',
|
|
52
|
-
processing_time: nil,
|
|
53
|
-
metadata: {
|
|
54
|
-
usage: parsed_response['usage'],
|
|
55
|
-
finish_reason: parsed_response.dig('choices', 0, 'finish_reason'),
|
|
56
|
-
},
|
|
57
|
-
}
|
|
58
|
-
end
|
|
59
|
-
end
|
|
13
|
+
def initialize(**options)
|
|
14
|
+
super
|
|
15
|
+
@provider_name = "xAI"
|
|
16
|
+
self.class.default_timeout(@options[:timeout] || 60)
|
|
17
|
+
end
|
|
60
18
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
19
|
+
# xAI does not expose a standard embeddings API for this adapter.
|
|
20
|
+
# @raise [Prescient::Error] Always, because embeddings are unsupported
|
|
21
|
+
def generate_embedding(_text, **_options)
|
|
22
|
+
raise Prescient::Error, "xAI provider does not support embeddings."
|
|
23
|
+
end
|
|
66
24
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
25
|
+
# Generate a response through xAI's OpenAI-compatible chat API.
|
|
26
|
+
# @param prompt [String] Prompt to send
|
|
27
|
+
# @param context_items [Array<Hash, String>] Optional context items
|
|
28
|
+
# @return [Hash] Normalized response data
|
|
29
|
+
def generate_response(prompt, context_items = [], **options)
|
|
30
|
+
handle_errors do
|
|
31
|
+
model = options[:model] || @options[:chat_model]
|
|
32
|
+
response = self.class.post(
|
|
33
|
+
"/v1/chat/completions",
|
|
34
|
+
headers: api_headers,
|
|
35
|
+
body: {
|
|
36
|
+
model: model,
|
|
37
|
+
messages: [{ role: "user", content: build_prompt(prompt, context_items) }],
|
|
38
|
+
max_tokens: options[:max_tokens] || 2000,
|
|
39
|
+
temperature: options[:temperature] || 0.7,
|
|
40
|
+
top_p: options[:top_p] || 0.9
|
|
41
|
+
}.to_json
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
validate_response!(response, "text generation")
|
|
45
|
+
|
|
46
|
+
parsed_response = response.parsed_response
|
|
47
|
+
content = parsed_response.dig("choices", 0, "message", "content")
|
|
48
|
+
raise Prescient::InvalidResponseError, "No response generated" unless content.is_a?(String) && !content.empty?
|
|
49
|
+
|
|
50
|
+
{
|
|
51
|
+
response: content.strip,
|
|
52
|
+
model: model,
|
|
53
|
+
provider: "xai",
|
|
54
|
+
processing_time: nil,
|
|
55
|
+
metadata: {
|
|
56
|
+
usage: parsed_response["usage"],
|
|
57
|
+
finish_reason: parsed_response.dig("choices", 0, "finish_reason")
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
end
|
|
70
62
|
|
|
63
|
+
# Check whether the configured xAI model is available.
|
|
64
|
+
# @return [Hash] Provider health information
|
|
65
|
+
def health_check
|
|
66
|
+
handle_errors do
|
|
67
|
+
response = self.class.get("/v1/models", headers: api_headers)
|
|
68
|
+
|
|
69
|
+
if response.success?
|
|
70
|
+
models = response.parsed_response["data"] || []
|
|
71
|
+
model_available = models.any? { |model| model["id"] == @options[:chat_model] }
|
|
72
|
+
|
|
73
|
+
{
|
|
74
|
+
status: "healthy",
|
|
75
|
+
provider: "xai",
|
|
76
|
+
reachable: true,
|
|
77
|
+
models_available: models.map { |model| model["id"] },
|
|
78
|
+
chat_model: { name: @options[:chat_model], available: model_available },
|
|
79
|
+
ready: model_available
|
|
80
|
+
}
|
|
81
|
+
else
|
|
82
|
+
{
|
|
83
|
+
status: "unhealthy",
|
|
84
|
+
provider: "xai",
|
|
85
|
+
reachable: true,
|
|
86
|
+
error: "HTTP #{response.code}",
|
|
87
|
+
message: response.message,
|
|
88
|
+
ready: false
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
rescue Prescient::Error => e
|
|
71
93
|
{
|
|
72
|
-
status:
|
|
73
|
-
provider:
|
|
74
|
-
reachable:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
ready:
|
|
78
|
-
}
|
|
79
|
-
else
|
|
80
|
-
{
|
|
81
|
-
status: 'unhealthy',
|
|
82
|
-
provider: 'xai',
|
|
83
|
-
reachable: true,
|
|
84
|
-
error: "HTTP #{response.code}",
|
|
85
|
-
message: response.message,
|
|
86
|
-
ready: false,
|
|
94
|
+
status: "unavailable",
|
|
95
|
+
provider: "xai",
|
|
96
|
+
reachable: false,
|
|
97
|
+
error: e.class.name,
|
|
98
|
+
message: e.message,
|
|
99
|
+
ready: false
|
|
87
100
|
}
|
|
88
101
|
end
|
|
89
|
-
end
|
|
90
|
-
rescue Prescient::Error => e
|
|
91
|
-
{
|
|
92
|
-
status: 'unavailable',
|
|
93
|
-
provider: 'xai',
|
|
94
|
-
reachable: false,
|
|
95
|
-
error: e.class.name,
|
|
96
|
-
message: e.message,
|
|
97
|
-
ready: false,
|
|
98
|
-
}
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# List models available to the configured xAI API key.
|
|
102
|
-
# @return [Array<Hash>] Model descriptors
|
|
103
|
-
def list_models
|
|
104
|
-
handle_errors do
|
|
105
|
-
response = self.class.get('/v1/models', headers: api_headers)
|
|
106
|
-
validate_response!(response, 'model listing')
|
|
107
102
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
103
|
+
# List models available to the configured xAI API key.
|
|
104
|
+
# @return [Array<Hash>] Model descriptors
|
|
105
|
+
def list_models
|
|
106
|
+
handle_errors do
|
|
107
|
+
response = self.class.get("/v1/models", headers: api_headers)
|
|
108
|
+
validate_response!(response, "model listing")
|
|
109
|
+
|
|
110
|
+
(response.parsed_response["data"] || []).map do |model|
|
|
111
|
+
{
|
|
112
|
+
name: model["id"],
|
|
113
|
+
object: model["object"],
|
|
114
|
+
created: model["created"],
|
|
115
|
+
owned_by: model["owned_by"],
|
|
116
|
+
context_length: model["context_length"]
|
|
117
|
+
}.compact
|
|
118
|
+
end
|
|
119
|
+
end
|
|
116
120
|
end
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
121
|
|
|
120
|
-
|
|
122
|
+
protected
|
|
121
123
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
def validate_configuration!
|
|
125
|
+
required_options = %i[api_key chat_model]
|
|
126
|
+
missing_options = required_options.select { |option| @options[option].nil? }
|
|
125
127
|
|
|
126
|
-
|
|
128
|
+
return unless missing_options.any?
|
|
127
129
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
raise Prescient::Error, "Missing required options: #{missing_options.join(", ")}"
|
|
131
|
+
end
|
|
130
132
|
|
|
131
|
-
|
|
133
|
+
private
|
|
132
134
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
def api_headers
|
|
136
|
+
{
|
|
137
|
+
"Content-Type" => "application/json",
|
|
138
|
+
"Authorization" => "Bearer #{@options[:api_key]}"
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
end
|
|
138
142
|
end
|
|
139
143
|
end
|