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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +21 -268
  3. data/CHANGELOG.md +59 -0
  4. data/INTEGRATION_GUIDE.md +19 -1
  5. data/README.md +369 -21
  6. data/Steepfile +12 -12
  7. data/db/migrate/001_create_prescient_tables.rb +15 -16
  8. data/docker-compose.yml +22 -0
  9. data/examples/README.md +36 -1
  10. data/examples/custom_contexts.rb +4 -4
  11. data/examples/web_search.rb +34 -0
  12. data/exe/prescient +2 -2
  13. data/exe/prescient-mcp +7 -0
  14. data/lib/prescient/agent/audit_log.rb +37 -0
  15. data/lib/prescient/agent/cli_adapter.rb +29 -0
  16. data/lib/prescient/agent/configuration.rb +57 -0
  17. data/lib/prescient/agent/context.rb +56 -0
  18. data/lib/prescient/agent/error_serializer.rb +47 -0
  19. data/lib/prescient/agent/errors.rb +25 -0
  20. data/lib/prescient/agent/parser.rb +49 -0
  21. data/lib/prescient/agent/prompt_builder.rb +31 -0
  22. data/lib/prescient/agent/result.rb +36 -0
  23. data/lib/prescient/agent/runtime.rb +175 -0
  24. data/lib/prescient/agent/schema_validator.rb +215 -0
  25. data/lib/prescient/agent/tool_registry.rb +89 -0
  26. data/lib/prescient/agent.rb +22 -0
  27. data/lib/prescient/api.rb +346 -231
  28. data/lib/prescient/base.rb +370 -372
  29. data/lib/prescient/cli.rb +591 -402
  30. data/lib/prescient/client.rb +31 -4
  31. data/lib/prescient/configuration_loader.rb +511 -336
  32. data/lib/prescient/document_source.rb +114 -0
  33. data/lib/prescient/errors.rb +13 -3
  34. data/lib/prescient/mcp/authentication.rb +39 -0
  35. data/lib/prescient/mcp/configuration.rb +38 -0
  36. data/lib/prescient/mcp/rack.rb +243 -0
  37. data/lib/prescient/mcp/server.rb +202 -0
  38. data/lib/prescient/mcp/stdio.rb +42 -0
  39. data/lib/prescient/mcp.rb +8 -0
  40. data/lib/prescient/pgvector.rb +193 -189
  41. data/lib/prescient/provider/anthropic.rb +129 -125
  42. data/lib/prescient/provider/deepseek.rb +122 -118
  43. data/lib/prescient/provider/gemini.rb +153 -149
  44. data/lib/prescient/provider/huggingface.rb +191 -187
  45. data/lib/prescient/provider/mistral.rb +151 -147
  46. data/lib/prescient/provider/ollama.rb +168 -165
  47. data/lib/prescient/provider/openai.rb +174 -171
  48. data/lib/prescient/provider/xai.rb +122 -118
  49. data/lib/prescient/tool/search_api.rb +130 -0
  50. data/lib/prescient/tool/searxng.rb +128 -0
  51. data/lib/prescient/tool.rb +125 -0
  52. data/lib/prescient/version.rb +1 -1
  53. data/lib/prescient.rb +129 -55
  54. data/schema/prescient.configuration.schema.json +119 -0
  55. data/searxng/settings.yml +18 -0
  56. data/sig/prescient.rbs +228 -1
  57. metadata +33 -5
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Style/ClassAndModuleChildren
4
+ module Prescient::Agent
5
+ # Validates the JSON-schema subset used by agent tools.
6
+ # rubocop:disable Metrics/ClassLength
7
+ class SchemaValidator
8
+ # Validate a value against a schema.
9
+ # @param schema [Hash] Supported JSON-schema subset
10
+ # @param value [Object] Value to validate
11
+ # @return [Object] The validated value
12
+ def self.validate!(schema, value)
13
+ new(schema).validate!(value)
14
+ end
15
+
16
+ def initialize(schema)
17
+ @schema = schema.is_a?(Hash) ? schema : {}
18
+ end
19
+
20
+ # Validate a value against this validator's schema.
21
+ # @param value [Object] Value to validate
22
+ # @return [Object] The validated value
23
+ def validate!(value)
24
+ validate_schema(@schema, value, [])
25
+ value
26
+ end
27
+
28
+ private
29
+
30
+ def validate_schema(schema, value, path)
31
+ schema = resolve_reference(schema)
32
+ validate_composition(schema, value, path)
33
+ validate_enum(schema, value, path)
34
+ validate_type(schema, value, path)
35
+ validate_string_constraints(schema, value, path)
36
+ validate_number_constraints(schema, value, path)
37
+ validate_array_constraints(schema, value, path)
38
+ validate_object_constraints(schema, value, path)
39
+ end
40
+
41
+ def validate_composition(schema, value, path)
42
+ schemas = schema_value(schema, "oneOf")
43
+ return unless schemas
44
+
45
+ matches = schemas.count do |candidate|
46
+ validate_schema(candidate, value, path)
47
+ true
48
+ rescue MalformedActionError
49
+ false
50
+ end
51
+ return if matches == 1
52
+
53
+ raise MalformedActionError, "agent tool arguments must match exactly one schema#{path_suffix(path)}"
54
+ end
55
+
56
+ def validate_enum(schema, value, path)
57
+ values = schema_value(schema, "enum")
58
+ return unless values
59
+ return if values.include?(value)
60
+
61
+ raise MalformedActionError, "agent tool argument is not an allowed value#{path_suffix(path)}"
62
+ end
63
+
64
+ def validate_type(schema, value, path)
65
+ type = schema_value(schema, "type")
66
+ return unless type
67
+
68
+ valid = {
69
+ "object" => value.is_a?(Hash),
70
+ "array" => value.is_a?(Array),
71
+ "string" => value.is_a?(String),
72
+ "integer" => value.is_a?(Integer),
73
+ "number" => value.is_a?(Numeric) && !value.is_a?(Complex),
74
+ "boolean" => [true, false].include?(value)
75
+ }.fetch(type.to_s, false)
76
+ return if valid
77
+
78
+ raise MalformedActionError, "agent tool arguments must be a #{type}#{path_suffix(path)}"
79
+ end
80
+
81
+ def validate_string_constraints(schema, value, path)
82
+ return unless value.is_a?(String)
83
+
84
+ minimum = schema_value(schema, "minLength")
85
+ maximum = schema_value(schema, "maxLength")
86
+ pattern = schema_value(schema, "pattern")
87
+ if minimum && value.length < minimum
88
+ raise MalformedActionError, "agent string is shorter than allowed#{path_suffix(path)}"
89
+ end
90
+ if maximum && value.length > maximum
91
+ raise MalformedActionError, "agent string is longer than allowed#{path_suffix(path)}"
92
+ end
93
+ return unless pattern && !Regexp.new(pattern).match?(value)
94
+
95
+ raise MalformedActionError, "agent string does not match the required pattern#{path_suffix(path)}"
96
+ rescue RegexpError
97
+ raise ConfigurationError, "agent tool schema contains an invalid pattern"
98
+ end
99
+
100
+ def validate_number_constraints(schema, value, path)
101
+ return unless value.is_a?(Numeric) && !value.is_a?(Complex)
102
+
103
+ minimum = schema_value(schema, "minimum")
104
+ maximum = schema_value(schema, "maximum")
105
+ exclusive_minimum = schema_value(schema, "exclusiveMinimum")
106
+ exclusive_maximum = schema_value(schema, "exclusiveMaximum")
107
+ validate_lower_bound(value, minimum, path)
108
+ validate_upper_bound(value, maximum, path)
109
+ validate_lower_bound(value, exclusive_minimum, path, exclusive: true)
110
+ validate_upper_bound(value, exclusive_maximum, path, exclusive: true)
111
+ end
112
+
113
+ def validate_array_constraints(schema, value, path)
114
+ return unless value.is_a?(Array)
115
+
116
+ minimum = schema_value(schema, "minItems")
117
+ maximum = schema_value(schema, "maxItems")
118
+ if minimum && value.length < minimum
119
+ raise MalformedActionError, "agent array has too few items#{path_suffix(path)}"
120
+ end
121
+ if maximum && value.length > maximum
122
+ raise MalformedActionError, "agent array has too many items#{path_suffix(path)}"
123
+ end
124
+
125
+ item_schema = schema_value(schema, "items")
126
+ return unless item_schema.is_a?(Hash)
127
+
128
+ value.each_with_index { |item, index| validate_schema(item_schema, item, path + [index]) }
129
+ end
130
+
131
+ def validate_object_constraints(schema, value, path)
132
+ return unless value.is_a?(Hash)
133
+
134
+ validate_required(value, schema, path)
135
+ validate_properties(value, schema, path)
136
+ validate_additional_properties(value, schema, path)
137
+ end
138
+
139
+ def validate_required(value, schema, path)
140
+ (schema_value(schema, "required") || []).each do |key|
141
+ next if value.key?(key) || value.key?(key.to_sym)
142
+
143
+ raise MalformedActionError, "agent tool arguments missing: #{key}#{path_suffix(path)}"
144
+ end
145
+ end
146
+
147
+ def validate_properties(value, schema, path)
148
+ (schema_value(schema, "properties") || {}).each do |key, property_schema|
149
+ next unless value.key?(key) || value.key?(key.to_sym)
150
+
151
+ property_value = value.key?(key) ? value[key] : value[key.to_sym]
152
+ validate_schema(property_schema, property_value, path + [key])
153
+ end
154
+ end
155
+
156
+ def validate_additional_properties(value, schema, path)
157
+ return unless schema_value(schema, "additionalProperties") == false
158
+
159
+ property_names = (schema_value(schema, "properties") || {}).keys.map(&:to_s)
160
+ value.each_key do |key|
161
+ next if property_names.include?(key.to_s)
162
+
163
+ raise MalformedActionError, "agent tool arguments not allowed: #{key}#{path_suffix(path)}"
164
+ end
165
+ end
166
+
167
+ def validate_lower_bound(value, bound, path, exclusive: false)
168
+ return unless bound.is_a?(Numeric)
169
+ return if exclusive ? value > bound : value >= bound
170
+
171
+ description = exclusive ? "exclusive minimum" : "minimum"
172
+ raise MalformedActionError, "agent number is below the #{description}#{path_suffix(path)}"
173
+ end
174
+
175
+ def validate_upper_bound(value, bound, path, exclusive: false)
176
+ return unless bound.is_a?(Numeric)
177
+ return if exclusive ? value < bound : value <= bound
178
+
179
+ description = exclusive ? "exclusive maximum" : "maximum"
180
+ raise MalformedActionError, "agent number exceeds the #{description}#{path_suffix(path)}"
181
+ end
182
+
183
+ def resolve_reference(schema)
184
+ reference = schema_value(schema, "$ref")
185
+ return schema unless reference
186
+ return resolve_pointer(reference) if reference.start_with?("#/")
187
+
188
+ raise ConfigurationError, "agent tool schema only supports local $ref values"
189
+ end
190
+
191
+ def resolve_pointer(reference)
192
+ reference.delete_prefix("#/").split("/").reduce(@schema) do |current, token|
193
+ key = token.gsub("~1", "/").gsub("~0", "~")
194
+ current.fetch(key) { current.fetch(key.to_sym) }
195
+ end
196
+ rescue KeyError, NoMethodError
197
+ raise ConfigurationError, "agent tool schema contains an unresolved $ref"
198
+ end
199
+
200
+ def schema_value(schema, key)
201
+ return schema[key] if schema.key?(key)
202
+ return schema[key.to_sym] if schema.key?(key.to_sym)
203
+
204
+ nil
205
+ end
206
+
207
+ def path_suffix(path)
208
+ return "" if path.empty?
209
+
210
+ " at #{path.map { |part| part.is_a?(Integer) ? "[#{part}]" : part }.join(".")}"
211
+ end
212
+ end
213
+ # rubocop:enable Metrics/ClassLength
214
+ end
215
+ # rubocop:enable Style/ClassAndModuleChildren
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Style/ClassAndModuleChildren
4
+ module Prescient::Agent
5
+ # Explicit allowlist and invocation boundary for agent tools.
6
+ class ToolRegistry
7
+ # @return [Class] Internal immutable tool descriptor
8
+ Tool = Struct.new(:name, :description, :schema, :callable, keyword_init: true)
9
+
10
+ def initialize(tools)
11
+ @tools = tools.to_h { |name, tool| [name.to_sym, build_tool(name, tool)] }
12
+ end
13
+
14
+ # Return all explicitly allowed tools.
15
+ # @return [Array<Tool>] Tool descriptors
16
+ def all
17
+ @tools.values
18
+ end
19
+
20
+ def invoke(name, arguments)
21
+ tool = @tools[name.to_sym]
22
+ raise UnauthorizedToolError, "agent tool not allowed: #{name}" unless tool
23
+
24
+ SchemaValidator.validate!(tool.schema, arguments)
25
+ tool.callable.call(arguments)
26
+ end
27
+
28
+ private
29
+
30
+ def build_tool(name, tool)
31
+ unless tool.respond_to?(:search) || tool.respond_to?(:call)
32
+ raise ConfigurationError, "agent tool does not support invocation: #{name}"
33
+ end
34
+
35
+ validate_callable_schema!(name, tool) if tool.respond_to?(:call) && !tool.respond_to?(:search)
36
+
37
+ Tool.new(
38
+ name: name.to_sym,
39
+ description: tool_description(tool),
40
+ schema: tool_schema(tool),
41
+ callable: ->(arguments) { invoke_tool(tool, arguments) }
42
+ )
43
+ end
44
+
45
+ def invoke_tool(tool, arguments)
46
+ return tool.call(arguments) if tool.respond_to?(:call)
47
+
48
+ invoke_search(tool, arguments)
49
+ end
50
+
51
+ def invoke_search(tool, arguments)
52
+ query = arguments["query"] || arguments[:query]
53
+ valid_query = query.is_a?(String) && !query.strip.empty?
54
+ raise MalformedActionError, "search tool requires a non-empty query" unless valid_query
55
+
56
+ tool.search(query, limit: arguments["limit"] || arguments[:limit])
57
+ end
58
+
59
+ def tool_description(tool)
60
+ return tool.description if tool.respond_to?(:description)
61
+
62
+ return "Search using the configured external capability." if tool.respond_to?(:search)
63
+
64
+ raise ConfigurationError, "generic agent tool requires a description"
65
+ end
66
+
67
+ def tool_schema(tool)
68
+ return tool.schema if tool.respond_to?(:schema)
69
+
70
+ { type: "object", required: ["query"] }
71
+ end
72
+
73
+ def validate_callable_schema!(name, tool)
74
+ schema = tool.respond_to?(:schema) ? tool.schema : nil
75
+ valid = schema.is_a?(Hash) && !schema.empty? && schema_value(schema, "type") == "object"
76
+ return if valid
77
+
78
+ raise ConfigurationError, "generic agent tool requires a non-empty object schema: #{name}"
79
+ end
80
+
81
+ def schema_value(schema, key)
82
+ return schema[key] if schema.key?(key)
83
+ return schema[key.to_sym] if schema.key?(key.to_sym)
84
+
85
+ nil
86
+ end
87
+ end
88
+ end
89
+ # rubocop:enable Style/ClassAndModuleChildren
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Prescient
6
+ # Optional bounded agent orchestration over Prescient Core.
7
+ module Agent
8
+ end
9
+ end
10
+
11
+ require_relative "agent/configuration"
12
+ require_relative "agent/audit_log"
13
+ require_relative "agent/errors"
14
+ require_relative "agent/error_serializer"
15
+ require_relative "agent/schema_validator"
16
+ require_relative "agent/cli_adapter"
17
+ require_relative "agent/result"
18
+ require_relative "agent/context"
19
+ require_relative "agent/parser"
20
+ require_relative "agent/prompt_builder"
21
+ require_relative "agent/tool_registry"
22
+ require_relative "agent/runtime"