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
data/lib/prescient/base.rb
CHANGED
|
@@ -1,429 +1,427 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
|
|
6
|
-
# Base class for all AI provider implementations
|
|
7
|
-
#
|
|
8
|
-
# This abstract base class defines the common interface that all AI providers
|
|
9
|
-
# must implement. It provides shared functionality for text processing, context
|
|
10
|
-
# formatting, prompt building, and error handling.
|
|
11
|
-
#
|
|
12
|
-
# @abstract Subclass and implement {#generate_embedding}, {#generate_response},
|
|
13
|
-
# {#health_check}, and any configuration validation required by
|
|
14
|
-
# {#validate_configuration!}
|
|
15
|
-
#
|
|
16
|
-
# @example Creating a custom provider
|
|
17
|
-
# class MyProvider < Prescient::Base
|
|
18
|
-
# def generate_embedding(text, **options)
|
|
19
|
-
# # Implementation here
|
|
20
|
-
# end
|
|
21
|
-
#
|
|
22
|
-
# def generate_response(prompt, context_items = [], **options)
|
|
23
|
-
# # Implementation here
|
|
24
|
-
# end
|
|
25
|
-
#
|
|
26
|
-
# def health_check
|
|
27
|
-
# # Implementation here
|
|
28
|
-
# end
|
|
29
|
-
# end
|
|
30
|
-
#
|
|
31
|
-
class Prescient::Base
|
|
32
|
-
# @return [Hash] Configuration options for this provider instance
|
|
33
|
-
attr_reader :options, :provider_name
|
|
34
|
-
|
|
35
|
-
# Initialize the provider with configuration options
|
|
36
|
-
#
|
|
37
|
-
# @param options [Hash] Provider-specific configuration options
|
|
38
|
-
# @option options [String] :api_key API key for authenticated providers
|
|
39
|
-
# @option options [String] :url Base URL for self-hosted providers
|
|
40
|
-
# @option options [Integer] :timeout Request timeout in seconds
|
|
41
|
-
# @option options [Hash] :prompt_templates Custom prompt templates
|
|
42
|
-
# @option options [Hash] :context_configs Context formatting configurations
|
|
43
|
-
# @option options [Integer] :embedding_dimensions Expected custom embedding size
|
|
44
|
-
# @option options [Array<Symbol, String>] :context_excluded_fields Additional
|
|
45
|
-
# field names excluded from generic embedding text
|
|
46
|
-
def initialize(**options)
|
|
47
|
-
@options = options
|
|
48
|
-
@provider_name = options.fetch(:provider_name, self.class.to_s.split('::').last).to_s.sub(/\A./, &:upcase)
|
|
49
|
-
validate_configuration!
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Generate embeddings for the given text
|
|
53
|
-
#
|
|
54
|
-
# This method must be implemented by subclasses to provide embedding
|
|
55
|
-
# generation functionality.
|
|
56
|
-
#
|
|
57
|
-
# @param text [String] The text to generate embeddings for
|
|
58
|
-
# @param options [Hash] Provider-specific options
|
|
59
|
-
# @return [Array<Float>] Array of embedding values
|
|
60
|
-
# @raise [NotImplementedError] If not implemented by subclass
|
|
61
|
-
# @abstract
|
|
62
|
-
def generate_embedding(text, **options)
|
|
63
|
-
raise NotImplementedError, "#{self.class} must implement #generate_embedding"
|
|
64
|
-
end
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
65
5
|
|
|
66
|
-
|
|
6
|
+
module Prescient
|
|
7
|
+
# Base class for all AI provider implementations
|
|
67
8
|
#
|
|
68
|
-
# This
|
|
69
|
-
# functionality
|
|
9
|
+
# This abstract base class defines the common interface that all AI providers
|
|
10
|
+
# must implement. It provides shared functionality for text processing, context
|
|
11
|
+
# formatting, prompt building, and error handling.
|
|
70
12
|
#
|
|
71
|
-
# @
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
# @option options [Float] :temperature Sampling temperature (0.0-2.0)
|
|
75
|
-
# @option options [Integer] :max_tokens Maximum tokens to generate
|
|
76
|
-
# @option options [Float] :top_p Nucleus sampling parameter
|
|
77
|
-
# @return [Hash] Response hash with :response, :model, :provider keys
|
|
78
|
-
# @raise [NotImplementedError] If not implemented by subclass
|
|
79
|
-
# @abstract
|
|
80
|
-
def generate_response(prompt, context_items = [], **options)
|
|
81
|
-
raise NotImplementedError, "#{self.class} must implement #generate_response"
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# Check the health and availability of the provider
|
|
13
|
+
# @abstract Subclass and implement {#generate_embedding}, {#generate_response},
|
|
14
|
+
# {#health_check}, and any configuration validation required by
|
|
15
|
+
# {#validate_configuration!}
|
|
85
16
|
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
17
|
+
# @example Creating a custom provider
|
|
18
|
+
# class MyProvider < Prescient::Base
|
|
19
|
+
# def generate_embedding(text, **options)
|
|
20
|
+
# # Implementation here
|
|
21
|
+
# end
|
|
88
22
|
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
# @abstract
|
|
93
|
-
def health_check
|
|
94
|
-
raise NotImplementedError, "#{self.class} must implement #health_check"
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Check if the provider is currently available
|
|
23
|
+
# def generate_response(prompt, context_items = [], **options)
|
|
24
|
+
# # Implementation here
|
|
25
|
+
# end
|
|
98
26
|
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
27
|
+
# def health_check
|
|
28
|
+
# # Implementation here
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
102
31
|
#
|
|
103
|
-
#
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
32
|
+
# rubocop:disable Metrics/ClassLength
|
|
33
|
+
class Base
|
|
34
|
+
# @return [Hash] Configuration options for this provider instance
|
|
35
|
+
attr_reader :options, :provider_name
|
|
36
|
+
|
|
37
|
+
# Initialize the provider with configuration options
|
|
38
|
+
#
|
|
39
|
+
# @param options [Hash] Provider-specific configuration options
|
|
40
|
+
# @option options [String] :api_key API key for authenticated providers
|
|
41
|
+
# @option options [String] :url Base URL for self-hosted providers
|
|
42
|
+
# @option options [Integer] :timeout Request timeout in seconds
|
|
43
|
+
# @option options [Hash] :prompt_templates Custom prompt templates
|
|
44
|
+
# @option options [Hash] :context_configs Context formatting configurations
|
|
45
|
+
# @option options [Integer] :embedding_dimensions Expected custom embedding size
|
|
46
|
+
# @option options [Array<Symbol, String>] :context_excluded_fields Additional
|
|
47
|
+
# field names excluded from generic embedding text
|
|
48
|
+
def initialize(**options)
|
|
49
|
+
@options = options
|
|
50
|
+
@provider_name = options.fetch(:provider_name, self.class.to_s.split("::").last).to_s.sub(/\A./, &:upcase)
|
|
51
|
+
validate_configuration!
|
|
52
|
+
end
|
|
112
53
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
#
|
|
122
|
-
|
|
54
|
+
# Generate embeddings for the given text
|
|
55
|
+
#
|
|
56
|
+
# This method must be implemented by subclasses to provide embedding
|
|
57
|
+
# generation functionality.
|
|
58
|
+
#
|
|
59
|
+
# @param text [String] The text to generate embeddings for
|
|
60
|
+
# @param options [Hash] Provider-specific options
|
|
61
|
+
# @return [Array<Float>] Array of embedding values
|
|
62
|
+
# @raise [NotImplementedError] If not implemented by subclass
|
|
63
|
+
# @abstract
|
|
64
|
+
def generate_embedding(text, **options)
|
|
65
|
+
raise NotImplementedError, "#{self.class} must implement #generate_embedding"
|
|
66
|
+
end
|
|
123
67
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
raise Prescient::ConnectionError, "Request timeout: #{e.message}"
|
|
142
|
-
rescue Net::HTTPError => e
|
|
143
|
-
raise Prescient::ConnectionError, "HTTP error: #{e.message}"
|
|
144
|
-
rescue JSON::ParserError => e
|
|
145
|
-
raise Prescient::InvalidResponseError, "Invalid JSON response: #{e.message}"
|
|
146
|
-
rescue StandardError => e
|
|
147
|
-
raise Prescient::Error, "Unexpected error: #{e.message}"
|
|
148
|
-
end
|
|
68
|
+
# Generate text response for the given prompt
|
|
69
|
+
#
|
|
70
|
+
# This method must be implemented by subclasses to provide text generation
|
|
71
|
+
# functionality with optional context items.
|
|
72
|
+
#
|
|
73
|
+
# @param prompt [String] The prompt to generate a response for
|
|
74
|
+
# @param context_items [Array<Hash, String>] Optional context items to include
|
|
75
|
+
# @param options [Hash] Provider-specific generation options
|
|
76
|
+
# @option options [Float] :temperature Sampling temperature (0.0-2.0)
|
|
77
|
+
# @option options [Integer] :max_tokens Maximum tokens to generate
|
|
78
|
+
# @option options [Float] :top_p Nucleus sampling parameter
|
|
79
|
+
# @return [Hash] Response hash with :response, :model, :provider keys
|
|
80
|
+
# @raise [NotImplementedError] If not implemented by subclass
|
|
81
|
+
# @abstract
|
|
82
|
+
def generate_response(prompt, context_items = [], **options)
|
|
83
|
+
raise NotImplementedError, "#{self.class} must implement #generate_response"
|
|
84
|
+
end
|
|
149
85
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return embedding if embedding.length == target_dimensions
|
|
164
|
-
|
|
165
|
-
raise Prescient::InvalidResponseError,
|
|
166
|
-
"Invalid embedding dimensions: expected #{target_dimensions}, got #{embedding.length}"
|
|
167
|
-
end
|
|
86
|
+
# Check the health and availability of the provider
|
|
87
|
+
#
|
|
88
|
+
# This method must be implemented by subclasses to provide health check
|
|
89
|
+
# functionality.
|
|
90
|
+
#
|
|
91
|
+
# @return [Hash] Health status with at least :status and :provider keys,
|
|
92
|
+
# and typically :reachable and :ready for modern adapters
|
|
93
|
+
# @raise [NotImplementedError] If not implemented by subclass
|
|
94
|
+
# @abstract
|
|
95
|
+
def health_check
|
|
96
|
+
raise NotImplementedError, "#{self.class} must implement #health_check"
|
|
97
|
+
end
|
|
168
98
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
99
|
+
# Check if the provider is currently available
|
|
100
|
+
#
|
|
101
|
+
# Returns `true` when the health check reports `reachable: true`.
|
|
102
|
+
# For legacy adapters that only return a status string, `status == "healthy"`
|
|
103
|
+
# is also treated as available.
|
|
104
|
+
#
|
|
105
|
+
# @return [Boolean] true if the provider is currently reachable
|
|
106
|
+
def available?
|
|
107
|
+
health = health_check
|
|
108
|
+
health.key?(:reachable) ? health[:reachable] == true : health[:status] == "healthy"
|
|
109
|
+
rescue StandardError
|
|
110
|
+
false
|
|
111
|
+
end
|
|
180
112
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
113
|
+
protected
|
|
114
|
+
|
|
115
|
+
# Validate provider configuration
|
|
116
|
+
#
|
|
117
|
+
# Override this method in subclasses to validate required configuration
|
|
118
|
+
# options and raise appropriate errors for missing or invalid settings.
|
|
119
|
+
#
|
|
120
|
+
# @return [void]
|
|
121
|
+
# @raise [Prescient::Error] If configuration is invalid
|
|
122
|
+
def validate_configuration!
|
|
123
|
+
# Override in subclasses to validate required configuration
|
|
124
|
+
end
|
|
193
125
|
|
|
194
|
-
|
|
126
|
+
# Handle and standardize errors from provider operations
|
|
127
|
+
#
|
|
128
|
+
# Wraps provider-specific operations and converts common exceptions
|
|
129
|
+
# into standardized Prescient error types while preserving existing
|
|
130
|
+
# Prescient errors.
|
|
131
|
+
#
|
|
132
|
+
# @yield The operation block to execute with error handling
|
|
133
|
+
# @return [Object] The result of the yielded block
|
|
134
|
+
# @raise [Prescient::ConnectionError] For network/timeout errors
|
|
135
|
+
# @raise [Prescient::InvalidResponseError] For JSON parsing errors
|
|
136
|
+
# @raise [Prescient::Error] For other unexpected errors
|
|
137
|
+
def handle_errors
|
|
138
|
+
yield
|
|
139
|
+
rescue Prescient::Error
|
|
140
|
+
# Re-raise Prescient errors without wrapping
|
|
141
|
+
raise
|
|
142
|
+
rescue Net::ReadTimeout, Net::OpenTimeout => e
|
|
143
|
+
raise Prescient::ConnectionError, "Request timeout: #{e.message}"
|
|
144
|
+
rescue Net::HTTPError => e
|
|
145
|
+
raise Prescient::ConnectionError, "HTTP error: #{e.message}"
|
|
146
|
+
rescue JSON::ParserError => e
|
|
147
|
+
raise Prescient::InvalidResponseError, "Invalid JSON response: #{e.message}"
|
|
148
|
+
rescue StandardError => e
|
|
149
|
+
raise Prescient::Error, "Unexpected error: #{e.message}"
|
|
150
|
+
end
|
|
195
151
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
152
|
+
# Validate embedding dimensions against the configured model dimension.
|
|
153
|
+
#
|
|
154
|
+
# Embedding dimensions are part of the vector-storage contract. Vectors are
|
|
155
|
+
# never padded or truncated because either operation changes their meaning.
|
|
156
|
+
#
|
|
157
|
+
# @param embedding [Array<Float>] The embedding vector to validate
|
|
158
|
+
# @param target_dimensions [Integer] The required number of dimensions
|
|
159
|
+
# @return [Array<Float>] The original embedding when dimensions are valid
|
|
160
|
+
# @raise [Prescient::InvalidResponseError] If the vector is malformed or has
|
|
161
|
+
# an unexpected dimension
|
|
162
|
+
def validate_embedding_dimensions(embedding, target_dimensions)
|
|
163
|
+
raise Prescient::InvalidResponseError, "Embedding response is not an array" unless embedding.is_a?(Array)
|
|
164
|
+
|
|
165
|
+
return embedding if embedding.length == target_dimensions
|
|
166
|
+
|
|
167
|
+
raise Prescient::InvalidResponseError,
|
|
168
|
+
"Invalid embedding dimensions: expected #{target_dimensions}, got #{embedding.length}"
|
|
169
|
+
end
|
|
200
170
|
|
|
201
|
-
|
|
202
|
-
|
|
171
|
+
# Clean and preprocess text for AI processing
|
|
172
|
+
#
|
|
173
|
+
# Removes excess whitespace, normalizes spacing, and truncates to the
|
|
174
|
+
# library's current 8,000-character input ceiling.
|
|
175
|
+
#
|
|
176
|
+
# @param text [String, nil] The text to clean
|
|
177
|
+
# @return [String] Cleaned text, empty string if input was nil/empty
|
|
178
|
+
def clean_text(text)
|
|
179
|
+
# Limit length for most models
|
|
180
|
+
text.to_s.gsub(/\s+/, " ").strip.slice(0, 8000)
|
|
181
|
+
end
|
|
203
182
|
|
|
204
|
-
|
|
183
|
+
# Get default prompt templates
|
|
184
|
+
#
|
|
185
|
+
# Provides standard templates for system prompts and context handling
|
|
186
|
+
# that can be overridden via provider options.
|
|
187
|
+
#
|
|
188
|
+
# @return [Hash] Hash containing template strings with placeholders
|
|
189
|
+
# @private
|
|
190
|
+
def default_prompt_templates
|
|
191
|
+
{
|
|
192
|
+
system_prompt: "You are a helpful AI assistant. Answer questions clearly and accurately.",
|
|
193
|
+
no_context_template: <<~TEMPLATE.strip,
|
|
194
|
+
%<system_prompt>s
|
|
195
|
+
|
|
196
|
+
Question: %<query>s
|
|
197
|
+
|
|
198
|
+
Please provide a helpful response based on your knowledge.
|
|
199
|
+
TEMPLATE
|
|
200
|
+
with_context_template: <<~TEMPLATE.strip
|
|
201
|
+
%<system_prompt>s Use the following context to answer the question. If the context doesn't contain relevant information, say so clearly.
|
|
202
|
+
|
|
203
|
+
Context:
|
|
204
|
+
%<context>s
|
|
205
|
+
|
|
206
|
+
Question: %<query>s
|
|
207
|
+
|
|
208
|
+
Please provide a helpful response based on the context above.
|
|
209
|
+
TEMPLATE
|
|
210
|
+
}
|
|
211
|
+
end
|
|
205
212
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
213
|
+
# Build formatted prompt from query and context items
|
|
214
|
+
#
|
|
215
|
+
# Creates a properly formatted prompt using configurable templates,
|
|
216
|
+
# incorporating context items when provided.
|
|
217
|
+
#
|
|
218
|
+
# @param query [String] The user's question or prompt
|
|
219
|
+
# @param context_items [Array<Hash, String>] Optional context items
|
|
220
|
+
# @return [String] Formatted prompt ready for AI processing
|
|
221
|
+
def build_prompt(query, context_items = [])
|
|
222
|
+
templates = default_prompt_templates.merge(@options[:prompt_templates] || {})
|
|
223
|
+
system_prompt = templates[:system_prompt]
|
|
224
|
+
|
|
225
|
+
if context_items.empty?
|
|
226
|
+
format(templates[:no_context_template], system_prompt: system_prompt, query: query)
|
|
227
|
+
else
|
|
228
|
+
context_text = context_items.map.with_index(1) do |item, index|
|
|
229
|
+
"#{index}. #{format_context_item(item)}"
|
|
230
|
+
end.join("\n\n")
|
|
231
|
+
|
|
232
|
+
format(templates[:with_context_template], system_prompt: system_prompt, context: context_text,
|
|
233
|
+
query: query)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
210
236
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if context_items.empty?
|
|
224
|
-
templates[:no_context_template] % {
|
|
225
|
-
system_prompt: system_prompt,
|
|
226
|
-
query: query,
|
|
227
|
-
}
|
|
228
|
-
else
|
|
229
|
-
context_text = context_items.map.with_index(1) { |item, index|
|
|
230
|
-
"#{index}. #{format_context_item(item)}"
|
|
231
|
-
}.join("\n\n")
|
|
232
|
-
|
|
233
|
-
templates[:with_context_template] % {
|
|
234
|
-
system_prompt: system_prompt,
|
|
235
|
-
context: context_text,
|
|
236
|
-
query: query,
|
|
237
|
+
# Minimal default context configuration - users should define their own contexts
|
|
238
|
+
def default_context_configs
|
|
239
|
+
embedding_fields = [] # : Array[untyped]
|
|
240
|
+
fields = [] # : Array[untyped]
|
|
241
|
+
|
|
242
|
+
{
|
|
243
|
+
# Generic fallback configuration - works with any hash structure
|
|
244
|
+
"default" => {
|
|
245
|
+
fields: fields, # Will be dynamically determined from item keys
|
|
246
|
+
format: nil, # Will use fallback formatting
|
|
247
|
+
embedding_fields: embedding_fields # Will use all string/text fields
|
|
248
|
+
}
|
|
237
249
|
}
|
|
238
250
|
end
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
# Minimal default context configuration - users should define their own contexts
|
|
242
|
-
def default_context_configs
|
|
243
|
-
embedding_fields = [] # : Array[untyped]
|
|
244
|
-
fields = [] # : Array[untyped]
|
|
245
|
-
|
|
246
|
-
{
|
|
247
|
-
# Generic fallback configuration - works with any hash structure
|
|
248
|
-
'default' => {
|
|
249
|
-
fields: fields, # Will be dynamically determined from item keys
|
|
250
|
-
format: nil, # Will use fallback formatting
|
|
251
|
-
embedding_fields: embedding_fields, # Will use all string/text fields
|
|
252
|
-
},
|
|
253
|
-
}
|
|
254
|
-
end
|
|
255
251
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
252
|
+
# Extract text for embedding generation based on context configuration
|
|
253
|
+
def extract_embedding_text(item, context_type = nil)
|
|
254
|
+
return item.to_s unless item.is_a?(Hash)
|
|
259
255
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
256
|
+
config = resolve_context_config(item, context_type)
|
|
257
|
+
text_values = extract_configured_fields(item, config) || extract_text_values(item)
|
|
258
|
+
text_values.join(" ").strip
|
|
259
|
+
end
|
|
264
260
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
261
|
+
# Extract text values from hash, excluding non-textual fields
|
|
262
|
+
def extract_text_values(item)
|
|
263
|
+
# Common fields to exclude from embedding text. Provider-specific fields can
|
|
264
|
+
# be added with the :context_excluded_fields option.
|
|
265
|
+
default_excluded_fields = %w[id _id uuid created_at updated_at timestamp version status
|
|
266
|
+
active]
|
|
267
|
+
configured_fields = Array(@options[:context_excluded_fields]) # : Array[untyped]
|
|
268
|
+
configured_excluded_fields = configured_fields.map { |field| field.to_s.downcase }
|
|
269
|
+
exclude_fields = default_excluded_fields | configured_excluded_fields
|
|
270
|
+
|
|
271
|
+
item.filter_map do |key, value|
|
|
272
|
+
next if exclude_fields.include?(key.to_s.downcase)
|
|
273
|
+
next unless value.is_a?(String) || value.is_a?(Numeric)
|
|
274
|
+
next if value.to_s.strip.empty?
|
|
275
|
+
|
|
276
|
+
value.to_s
|
|
277
|
+
end
|
|
278
|
+
end
|
|
283
279
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
280
|
+
# Generic context item formatting using configurable contexts
|
|
281
|
+
def format_context_item(item)
|
|
282
|
+
case item
|
|
283
|
+
when Hash then format_hash_item(item)
|
|
284
|
+
when String then item
|
|
285
|
+
else item.to_s
|
|
286
|
+
end
|
|
290
287
|
end
|
|
291
|
-
end
|
|
292
288
|
|
|
293
|
-
|
|
289
|
+
private
|
|
294
290
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
291
|
+
# Resolve context configuration for an item
|
|
292
|
+
def resolve_context_config(item, context_type)
|
|
293
|
+
context_configs = default_context_configs.merge(@options[:context_configs] || {})
|
|
294
|
+
return context_configs["default"] if context_configs.empty?
|
|
299
295
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
296
|
+
detected_type = context_type || detect_context_type(item)
|
|
297
|
+
context_configs[detected_type] || context_configs["default"]
|
|
298
|
+
end
|
|
303
299
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
300
|
+
# Extract fields configured for embeddings
|
|
301
|
+
def extract_configured_fields(item, config)
|
|
302
|
+
return nil unless config[:embedding_fields]&.any?
|
|
307
303
|
|
|
308
|
-
|
|
309
|
-
|
|
304
|
+
config[:embedding_fields].filter_map { |field| item[field] || item[field.to_sym] }
|
|
305
|
+
end
|
|
310
306
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
307
|
+
# Format a hash item using context configuration
|
|
308
|
+
def format_hash_item(item)
|
|
309
|
+
config = resolve_context_config(item, nil)
|
|
310
|
+
return fallback_format_hash(item) unless config[:format]
|
|
315
311
|
|
|
316
|
-
|
|
317
|
-
|
|
312
|
+
format_data = build_format_data(item, config)
|
|
313
|
+
return fallback_format_hash(item) unless format_data.any?
|
|
318
314
|
|
|
319
|
-
|
|
320
|
-
|
|
315
|
+
apply_format_template(config[:format], format_data) || fallback_format_hash(item)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Build format data from item fields
|
|
319
|
+
def build_format_data(item, config)
|
|
320
|
+
format_data = {} # : Hash[Symbol, untyped]
|
|
321
|
+
fields_to_check = config[:fields].any? ? config[:fields] : item.keys.map(&:to_s)
|
|
321
322
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
323
|
+
fields_to_check.each do |field|
|
|
324
|
+
value = item[field] || item[field.to_sym]
|
|
325
|
+
format_data[field.to_sym] = value if value
|
|
326
|
+
end
|
|
326
327
|
|
|
327
|
-
|
|
328
|
-
value = item[field] || item[field.to_sym]
|
|
329
|
-
format_data[field.to_sym] = value if value
|
|
328
|
+
format_data
|
|
330
329
|
end
|
|
331
330
|
|
|
332
|
-
|
|
333
|
-
|
|
331
|
+
# Apply format template with error handling
|
|
332
|
+
def apply_format_template(template, format_data)
|
|
333
|
+
template % format_data
|
|
334
|
+
rescue KeyError
|
|
335
|
+
nil
|
|
336
|
+
end
|
|
334
337
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
rescue KeyError
|
|
339
|
-
nil
|
|
340
|
-
end
|
|
338
|
+
# Detect context type from item structure
|
|
339
|
+
def detect_context_type(item)
|
|
340
|
+
return "default" unless item.is_a?(Hash)
|
|
341
341
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
342
|
+
# Check for explicit type fields (user-defined)
|
|
343
|
+
return item["type"].to_s if item["type"]
|
|
344
|
+
return item["context_type"].to_s if item["context_type"]
|
|
345
|
+
return item["model_type"].to_s.downcase if item["model_type"]
|
|
345
346
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
return item['model_type'].to_s.downcase if item['model_type']
|
|
347
|
+
# If no explicit type and user has configured contexts, try to match
|
|
348
|
+
context_configs = @options[:context_configs] || {}
|
|
349
|
+
return match_context_by_fields(item, context_configs) if context_configs.any?
|
|
350
350
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
351
|
+
# Default fallback
|
|
352
|
+
"default"
|
|
353
|
+
end
|
|
354
354
|
|
|
355
|
-
#
|
|
356
|
-
|
|
357
|
-
|
|
355
|
+
# Match context type based on configured field patterns
|
|
356
|
+
def match_context_by_fields(item, context_configs)
|
|
357
|
+
item_fields = item.keys.map(&:to_s)
|
|
358
|
+
best_match = find_best_field_match(item_fields, context_configs)
|
|
359
|
+
best_match || "default"
|
|
360
|
+
end
|
|
358
361
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
best_match || 'default'
|
|
364
|
-
end
|
|
362
|
+
# Find the best matching context configuration
|
|
363
|
+
def find_best_field_match(item_fields, context_configs)
|
|
364
|
+
best_match = nil
|
|
365
|
+
best_score = 0
|
|
365
366
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
best_match = nil
|
|
369
|
-
best_score = 0
|
|
367
|
+
context_configs.each do |context_type, config|
|
|
368
|
+
next unless config[:fields]&.any?
|
|
370
369
|
|
|
371
|
-
|
|
372
|
-
|
|
370
|
+
score = calculate_field_match_score(item_fields, config[:fields])
|
|
371
|
+
next unless score >= 0.5 && score > best_score
|
|
373
372
|
|
|
374
|
-
|
|
375
|
-
|
|
373
|
+
best_match = context_type
|
|
374
|
+
best_score = score
|
|
375
|
+
end
|
|
376
376
|
|
|
377
|
-
best_match
|
|
378
|
-
best_score = score
|
|
377
|
+
best_match
|
|
379
378
|
end
|
|
380
379
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
# Calculate field matching score
|
|
385
|
-
def calculate_field_match_score(item_fields, config_fields)
|
|
386
|
-
return 0 if config_fields.empty?
|
|
380
|
+
# Calculate field matching score
|
|
381
|
+
def calculate_field_match_score(item_fields, config_fields)
|
|
382
|
+
return 0 if config_fields.empty?
|
|
387
383
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
384
|
+
matching_fields = (item_fields & config_fields).size
|
|
385
|
+
matching_fields.to_f / config_fields.size
|
|
386
|
+
end
|
|
391
387
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
388
|
+
# Fallback formatting for hash items
|
|
389
|
+
def fallback_format_hash(item, format_data = nil)
|
|
390
|
+
# Fallback: join key-value pairs
|
|
391
|
+
(format_data || item).map { |k, v| "#{k}: #{v}" }.join(", ")
|
|
392
|
+
end
|
|
397
393
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
394
|
+
def validate_response!(response, operation)
|
|
395
|
+
return if response.success?
|
|
396
|
+
|
|
397
|
+
resp_message, error_class = case response.code
|
|
398
|
+
when 400
|
|
399
|
+
["Bad Request", Prescient::Error]
|
|
400
|
+
when 401
|
|
401
|
+
["Authentication Failure", Prescient::AuthenticationError]
|
|
402
|
+
when 403
|
|
403
|
+
["Forbidden Access", Prescient::AuthenticationError]
|
|
404
|
+
when 404
|
|
405
|
+
["Model Not Available", Prescient::ModelNotAvailableError]
|
|
406
|
+
when 429
|
|
407
|
+
["Rate Limit Exceeded", Prescient::RateLimitError]
|
|
408
|
+
when 500..599
|
|
409
|
+
["#{provider_name} Server Error", Prescient::ProviderError]
|
|
410
|
+
else
|
|
411
|
+
["#{provider_name} Request Failure", Prescient::Error]
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
raise provider_error(resp_message, response, error_class:, operation:)
|
|
415
|
+
end
|
|
420
416
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
417
|
+
def provider_error(message, response, operation:, provider: nil, error_class: Prescient::ProviderError)
|
|
418
|
+
error_class.new(
|
|
419
|
+
message,
|
|
420
|
+
provider: provider || provider_name,
|
|
421
|
+
operation:,
|
|
422
|
+
status: response.code
|
|
423
|
+
)
|
|
424
|
+
end
|
|
428
425
|
end
|
|
426
|
+
# rubocop:enable Metrics/ClassLength
|
|
429
427
|
end
|