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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +21 -268
  3. data/CHANGELOG.md +37 -0
  4. data/INTEGRATION_GUIDE.md +7 -1
  5. data/README.md +210 -1
  6. data/Steepfile +12 -12
  7. data/db/migrate/001_create_prescient_tables.rb +15 -16
  8. data/examples/README.md +2 -1
  9. data/examples/custom_contexts.rb +4 -4
  10. data/exe/prescient +2 -2
  11. data/exe/prescient-mcp +7 -0
  12. data/lib/prescient/agent/audit_log.rb +37 -0
  13. data/lib/prescient/agent/cli_adapter.rb +29 -0
  14. data/lib/prescient/agent/configuration.rb +57 -0
  15. data/lib/prescient/agent/context.rb +56 -0
  16. data/lib/prescient/agent/error_serializer.rb +47 -0
  17. data/lib/prescient/agent/errors.rb +25 -0
  18. data/lib/prescient/agent/parser.rb +49 -0
  19. data/lib/prescient/agent/prompt_builder.rb +31 -0
  20. data/lib/prescient/agent/result.rb +36 -0
  21. data/lib/prescient/agent/runtime.rb +175 -0
  22. data/lib/prescient/agent/schema_validator.rb +215 -0
  23. data/lib/prescient/agent/tool_registry.rb +89 -0
  24. data/lib/prescient/agent.rb +22 -0
  25. data/lib/prescient/api.rb +337 -274
  26. data/lib/prescient/base.rb +370 -372
  27. data/lib/prescient/cli.rb +586 -526
  28. data/lib/prescient/client.rb +7 -6
  29. data/lib/prescient/configuration_loader.rb +492 -488
  30. data/lib/prescient/document_source.rb +114 -0
  31. data/lib/prescient/errors.rb +1 -3
  32. data/lib/prescient/mcp/authentication.rb +39 -0
  33. data/lib/prescient/mcp/configuration.rb +38 -0
  34. data/lib/prescient/mcp/rack.rb +243 -0
  35. data/lib/prescient/mcp/server.rb +202 -0
  36. data/lib/prescient/mcp/stdio.rb +42 -0
  37. data/lib/prescient/mcp.rb +8 -0
  38. data/lib/prescient/pgvector.rb +193 -189
  39. data/lib/prescient/provider/anthropic.rb +129 -125
  40. data/lib/prescient/provider/deepseek.rb +122 -118
  41. data/lib/prescient/provider/gemini.rb +153 -149
  42. data/lib/prescient/provider/huggingface.rb +191 -187
  43. data/lib/prescient/provider/mistral.rb +151 -147
  44. data/lib/prescient/provider/ollama.rb +168 -165
  45. data/lib/prescient/provider/openai.rb +174 -169
  46. data/lib/prescient/provider/xai.rb +122 -118
  47. data/lib/prescient/tool/search_api.rb +125 -121
  48. data/lib/prescient/tool/searxng.rb +123 -119
  49. data/lib/prescient/tool.rb +100 -98
  50. data/lib/prescient/version.rb +1 -1
  51. data/lib/prescient.rb +68 -62
  52. data/sig/prescient.rbs +176 -1
  53. metadata +23 -1
@@ -1,171 +1,175 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'httparty'
3
+ require "httparty"
4
4
 
5
- # Mistral AI API provider adapter.
6
- class Prescient::Provider::Mistral < Prescient::Base
7
- include HTTParty
5
+ module Prescient
6
+ module Provider
7
+ # Mistral AI API provider adapter.
8
+ class Mistral < Prescient::Base
9
+ include HTTParty
8
10
 
9
- base_uri 'https://api.mistral.ai'
11
+ base_uri "https://api.mistral.ai"
10
12
 
11
- def initialize(**options)
12
- super
13
- self.class.default_timeout(@options[:timeout] || 60)
14
- end
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
- # Check whether the configured Mistral models are available.
80
- # @return [Hash] Provider health information
81
- def health_check
82
- handle_errors do
83
- response = self.class.get('/v1/models', headers: api_headers)
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
- if response.success?
86
- models = response.parsed_response['data'] || []
87
- embedding_available = model_available?(models, @options[:embedding_model])
88
- chat_available = model_available?(models, @options[:chat_model])
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: 'healthy',
92
- provider: 'mistral',
93
- reachable: true,
94
- models_available: models.map { |model| model['id'] },
95
- embedding_model: { name: @options[:embedding_model], available: embedding_available },
96
- chat_model: { name: @options[:chat_model], available: chat_available },
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
- # List models available to the configured Mistral API key.
122
- # @return [Array<Hash>] Model descriptors
123
- def list_models
124
- handle_errors do
125
- response = self.class.get('/v1/models', headers: api_headers)
126
- validate_response!(response, 'model listing')
127
-
128
- (response.parsed_response['data'] || []).map do |model|
129
- {
130
- name: model['id'],
131
- object: model['object'],
132
- created: model['created'],
133
- owned_by: model['owned_by'],
134
- capabilities: model['capabilities'],
135
- max_context_length: model['max_context_length'],
136
- }.compact
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
- protected
143
+ protected
142
144
 
143
- def validate_configuration!
144
- required_options = [:api_key, :embedding_model, :chat_model]
145
- missing_options = required_options.select { |option| @options[option].nil? }
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
- return unless missing_options.any?
149
+ return unless missing_options.any?
148
150
 
149
- raise Prescient::Error, "Missing required options: #{missing_options.join(', ')}"
150
- end
151
+ raise Prescient::Error, "Missing required options: #{missing_options.join(", ")}"
152
+ end
151
153
 
152
- private
154
+ private
153
155
 
154
- def api_headers
155
- {
156
- 'Content-Type' => 'application/json',
157
- 'Authorization' => "Bearer #{@options[:api_key]}",
158
- }
159
- end
156
+ def api_headers
157
+ {
158
+ "Content-Type" => "application/json",
159
+ "Authorization" => "Bearer #{@options[:api_key]}"
160
+ }
161
+ end
160
162
 
161
- def model_available?(models, model_name)
162
- models.any? { |model| model['id'] == model_name }
163
- end
163
+ def model_available?(models, model_name)
164
+ models.any? { |model| model["id"] == model_name }
165
+ end
164
166
 
165
- def normalize_content(content)
166
- return content if content.is_a?(String)
167
- return unless content.is_a?(Array)
167
+ def normalize_content(content)
168
+ return content if content.is_a?(String)
169
+ return unless content.is_a?(Array)
168
170
 
169
- content.filter_map { |part| part['text'] if part.is_a?(Hash) }.join
171
+ content.filter_map { |part| part["text"] if part.is_a?(Hash) }.join
172
+ end
173
+ end
170
174
  end
171
175
  end