phronomy 0.10.0 → 0.11.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/CHANGELOG.md +41 -1
- data/README.md +18 -16
- data/lib/phronomy/agent/base.rb +34 -20
- data/lib/phronomy/agent/concerns/filterable.rb +184 -0
- data/lib/phronomy/agent/concerns/retryable.rb +3 -3
- data/lib/phronomy/agent/concerns/suspendable.rb +2 -2
- data/lib/phronomy/agent/context/capability/base.rb +2 -2
- data/lib/phronomy/filter/base.rb +56 -0
- data/lib/phronomy/{guardrail/prompt_injection_guardrail.rb → filter/prompt_injection_filter.rb} +20 -13
- data/lib/phronomy/filter.rb +5 -0
- data/lib/phronomy/generator_verifier.rb +1 -2
- data/lib/phronomy/multi_agent/team_coordinator.rb +1 -2
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy.rb +7 -4
- data/scripts/api_snapshot.rb +2 -2
- metadata +9 -11
- data/lib/phronomy/agent/concerns/guardrailable.rb +0 -45
- data/lib/phronomy/guardrail/base.rb +0 -45
- data/lib/phronomy/guardrail/input_guardrail.rb +0 -19
- data/lib/phronomy/guardrail/output_guardrail.rb +0 -19
- data/lib/phronomy/guardrail.rb +0 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e73d1b49c4638232021afad5cc807466c4bb7b2cfcfe60446930288e868a3db9
|
|
4
|
+
data.tar.gz: 624e783c532fc2ea009db99a7bda4a1308f06019bc0e776a78d206b70031b352
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0ce003a8f0b0c85cd29a744b86ee89ad7414c511ea46c1ad446227824100174b6bfae76de8a4caa32428a50f77606af8006c17ae307eb514692a92829331570c
|
|
7
|
+
data.tar.gz: 10a3e965b58c730d5ba21eefc3c7151725a26e6accc24dbde04dd91546aa6c65bea801faae0fe5ef1908995a78c259159712c0f8dffe3f19938fa64b14988c16
|
data/CHANGELOG.md
CHANGED
|
@@ -9,7 +9,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`Phronomy::Filter::Base` — unified value filter interface** (#389):
|
|
15
|
+
A single abstract base class `Filter::Base` with one method `call(value, **context)`
|
|
16
|
+
covers all three agent boundaries — user input, LLM output, and tool return values.
|
|
17
|
+
Subclasses return the (possibly transformed) value to continue, or call `block!` /
|
|
18
|
+
`raise Phronomy::FilterBlockError` to reject. The same filter instance can be
|
|
19
|
+
registered at multiple sites. Guardrails registered via `add_input_guardrail` /
|
|
20
|
+
`add_output_guardrail` are automatically included at the front of the filter chain.
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
class PiiMaskFilter < Phronomy::Filter::Base
|
|
24
|
+
def call(value, **_context)
|
|
25
|
+
value.to_s.gsub(/\b\d{2,4}-\d{2,4}-\d{4}\b/, "[PHONE]")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
f = PiiMaskFilter.new
|
|
30
|
+
agent.add_input_filter(f)
|
|
31
|
+
agent.add_output_filter(f)
|
|
32
|
+
agent.add_tool_result_filter(CustomerDataTool, f)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Class-level DSL counterparts: `input_filter`, `output_filter`, `tool_result_filter`.
|
|
36
|
+
The `tools(Hash)` DSL also accepts a `:result_filter` key for per-tool scoping.
|
|
37
|
+
|
|
38
|
+
- **`Guardrail::Base#call` — Filter::Base-compatible interface**:
|
|
39
|
+
`Guardrail::Base` now implements `call(value, **_context)`, which calls `check(value)`
|
|
40
|
+
and returns the value unchanged (or raises `GuardrailError`). Existing guardrail
|
|
41
|
+
subclasses require no changes.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- **Guardrail execution unified into the filter chain**:
|
|
46
|
+
Guardrails registered via `add_input_guardrail` / `add_output_guardrail` now run
|
|
47
|
+
as the first entries in `run_input_filters!` / `run_output_filters!`. The separate
|
|
48
|
+
`run_input_guardrails!` / `run_output_guardrails!` call sites in `invoke_once`,
|
|
49
|
+
`_stream_impl`, and `Suspendable#resume` have been removed. Behaviour is unchanged
|
|
50
|
+
— guardrails still run before any filters and `GuardrailError` still propagates.
|
|
51
|
+
|
|
52
|
+
|
|
13
53
|
|
|
14
54
|
## [0.10.0] - 2026-06-08
|
|
15
55
|
|
data/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
> We apologise for the instability this may cause.
|
|
8
8
|
|
|
9
9
|
**Phronomy** is a Ruby AI agent framework inspired by open-source AI agent frameworks.
|
|
10
|
-
It provides composable building blocks — Workflows, Agents, Tools,
|
|
10
|
+
It provides composable building blocks — Workflows, Agents, Tools, Filters, and Tracing — all powered by [RubyLLM](https://github.com/crmne/ruby_llm) for LLM abstraction.
|
|
11
11
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
@@ -31,8 +31,8 @@ It provides composable building blocks — Workflows, Agents, Tools, Guardrails,
|
|
|
31
31
|
| **Agent** — ReAct-style tool-calling agents with guardrails and conversation history | Stable |
|
|
32
32
|
| **Before-Completion Hook** — Three-tier LLM parameter injection | Stable |
|
|
33
33
|
| **Context Management** — Token budget calculation, estimation, and pruning; `Agent::Base` protected hooks: `build_context` (overridable), `trim_messages`, `trim_to_budget`, `compact_messages`, `budget_exceeded?`, `drop_messages_over` | Stable |
|
|
34
|
-
| **
|
|
35
|
-
| **`
|
|
34
|
+
| **Filters** — Input/output transformation and blocking via `Filter::Base`; call `block!(reason)` to reject and raise `FilterBlockError` | Beta |
|
|
35
|
+
| **`PromptInjectionFilter`** — Built-in `Filter::Base` subclass that detects prompt-injection patterns; usable standalone or as part of a filter chain | Beta |
|
|
36
36
|
| **`Agent::Context::Capability::Base.redact_params` / `.max_result_size`** — Class-level DSL: `redact_params` masks parameter values in log/trace output; `max_result_size` truncates oversized tool results before they reach the LLM | Beta |
|
|
37
37
|
| **Output Parser** — JSON and Struct-mapped parsers for structured LLM responses | Stable |
|
|
38
38
|
| **Eval Framework** — Dataset-driven evaluation with multiple scorer types | Beta |
|
|
@@ -78,6 +78,7 @@ It provides composable building blocks — Workflows, Agents, Tools, Guardrails,
|
|
|
78
78
|
| **Agent::SharedState** — Shared state pattern: peer agents collaborate via a shared KnowledgeStore; `member` DSL with per-agent instructions and `coordination` team protocol | Experimental |
|
|
79
79
|
| **`ScopePolicy`** — Configurable policy callable that maps (tool, scope, agent) to `:allow`/`:approve`/`:reject`; default policy auto-routes high-risk scopes through the approval gate | Experimental |
|
|
80
80
|
| **HITL Checkpoint/Resume** — `Agent::Base#invoke` returns `{ suspended: true, checkpoint: Checkpoint }` when an approval-required tool is encountered without a synchronous handler; `Agent::Base#resume(checkpoint, approved:)` resumes execution; `Agent::Base.resume(checkpoint, approved:)` (class-level) resolves the agent class automatically; `Checkpoint#to_h` / `Checkpoint.from_h` for serialization; `Agent::Base#checkpoint_store=` for custom idempotency backends; `CheckpointAlreadyResumedError` raised on duplicate resume | Experimental |
|
|
81
|
+
| **`Filter::Base` — unified value filter interface** — `Phronomy::Filter::Base` with a single abstract method `call(value, **context)`; apply to user input (`add_input_filter` / `input_filter` DSL), final LLM output (`add_output_filter` / `output_filter` DSL), or individual tool return values (`add_tool_result_filter(tool_class?, filter)` / `tool_result_filter` DSL); filters transform values and return the result, or raise `Phronomy::FilterBlockError` to reject; filter chains are composable; the same filter instance can be reused across all three sites | Beta |
|
|
81
82
|
|
|
82
83
|
> **Public API boundary**: The tables above are the complete list of classes, modules, and features
|
|
83
84
|
> intended for gem consumers. Every entry has an associated stability label.
|
|
@@ -252,30 +253,31 @@ result = OrchestratorAgent.new.invoke("Write a blog post about Ruby 3.4 features
|
|
|
252
253
|
puts result[:output]
|
|
253
254
|
```
|
|
254
255
|
|
|
255
|
-
###
|
|
256
|
+
### Filters — Input/output transformation and blocking
|
|
256
257
|
|
|
257
|
-
|
|
258
|
-
|
|
258
|
+
Filters sit between user input and the LLM (input filters) or between the LLM response and the caller (output filters).
|
|
259
|
+
A filter may **transform** the value (return the modified value) or **block** it (call `block!(reason)`, which raises `Phronomy::FilterBlockError`).
|
|
259
260
|
|
|
260
261
|
```ruby
|
|
261
|
-
class
|
|
262
|
-
def
|
|
263
|
-
|
|
262
|
+
class NoCreditCardFilter < Phronomy::Filter::Base
|
|
263
|
+
def call(value, **_context)
|
|
264
|
+
block!("Credit card numbers are not allowed") if value.match?(/\d{4}-\d{4}-\d{4}-\d{4}/)
|
|
265
|
+
value
|
|
264
266
|
end
|
|
265
267
|
end
|
|
266
268
|
|
|
267
269
|
agent = ResearchAgent.new
|
|
268
|
-
agent.
|
|
270
|
+
agent.add_input_filter(NoCreditCardFilter.new)
|
|
269
271
|
|
|
270
272
|
begin
|
|
271
273
|
agent.invoke("Charge 4111-1111-1111-1111")
|
|
272
|
-
rescue Phronomy::
|
|
274
|
+
rescue Phronomy::FilterBlockError => e
|
|
273
275
|
puts e.message # => "Credit card numbers are not allowed"
|
|
274
276
|
end
|
|
275
277
|
```
|
|
276
278
|
|
|
277
|
-
> **Note:** Phronomy includes `
|
|
278
|
-
> input
|
|
279
|
+
> **Note:** Phronomy includes `PromptInjectionFilter`, a built-in pattern-based
|
|
280
|
+
> input filter that detects common injection patterns (see the feature table above).
|
|
279
281
|
> PII scanning and content classification are **not** provided by the framework;
|
|
280
282
|
> that logic must be implemented by the application. Reference implementations for
|
|
281
283
|
> common patterns are available in `phronomy-examples` (example 06).
|
|
@@ -851,11 +853,11 @@ span attributes by default (`trace_pii: false`). To include full content in trac
|
|
|
851
853
|
Phronomy configuration. Evaluate whether your tracing backend (OTLP collector, Jaeger,
|
|
852
854
|
Honeycomb, etc.) meets your data-retention and privacy requirements.
|
|
853
855
|
|
|
854
|
-
**Prompt injection** — Phronomy provides `
|
|
855
|
-
pattern-based input
|
|
856
|
+
**Prompt injection** — Phronomy provides `PromptInjectionFilter`, a built-in
|
|
857
|
+
pattern-based input filter that detects common injection patterns (ignore/override
|
|
856
858
|
instructions, role-switching phrases, etc.). It is a useful starting point, not a
|
|
857
859
|
comprehensive defence; applications processing untrusted input should layer additional
|
|
858
|
-
custom
|
|
860
|
+
custom filters as needed (see the Filters section above).
|
|
859
861
|
|
|
860
862
|
**Tool and MCP security** — Tools can perform real-world side effects (database
|
|
861
863
|
writes, API calls, file deletion). Treat tool execution as a privileged operation:
|
data/lib/phronomy/agent/base.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
require_relative "checkpoint_store"
|
|
5
5
|
require_relative "concerns/retryable"
|
|
6
|
-
require_relative "concerns/
|
|
6
|
+
require_relative "concerns/filterable"
|
|
7
7
|
require_relative "concerns/before_completion"
|
|
8
8
|
require_relative "concerns/suspendable"
|
|
9
9
|
require_relative "concerns/error_translation"
|
|
@@ -34,7 +34,7 @@ module Phronomy
|
|
|
34
34
|
class Base
|
|
35
35
|
include Phronomy::Runnable
|
|
36
36
|
include Concerns::Retryable
|
|
37
|
-
include Concerns::
|
|
37
|
+
include Concerns::Filterable
|
|
38
38
|
include Concerns::BeforeCompletion
|
|
39
39
|
include Concerns::Suspendable
|
|
40
40
|
include Concerns::ErrorTranslation
|
|
@@ -418,7 +418,7 @@ module Phronomy
|
|
|
418
418
|
|
|
419
419
|
# Invokes the agent with the given input and returns a result Hash.
|
|
420
420
|
# Applies the retry policy configured via {.retry_policy} when transient
|
|
421
|
-
# errors occur. {Phronomy::
|
|
421
|
+
# errors occur. {Phronomy::FilterBlockError} is never retried.
|
|
422
422
|
#
|
|
423
423
|
# @param input [String, Hash] the user message; a Hash may supply
|
|
424
424
|
# +:message+, +:query+, or +:user+ as the text key, plus any template
|
|
@@ -439,7 +439,7 @@ module Phronomy
|
|
|
439
439
|
# @return [Hash] +{ output: String, messages: Array, usage: Phronomy::TokenUsage }+,
|
|
440
440
|
# or +{ output: nil, suspended: true, checkpoint: Phronomy::Agent::Checkpoint,
|
|
441
441
|
# messages: Array }+ when the invocation was suspended awaiting tool approval.
|
|
442
|
-
# @raise [Phronomy::
|
|
442
|
+
# @raise [Phronomy::FilterBlockError] when an input or output filter rejects the value
|
|
443
443
|
# @example Normal invocation
|
|
444
444
|
# result = MyAgent.new.invoke("What is Ruby?")
|
|
445
445
|
# puts result[:output]
|
|
@@ -603,7 +603,7 @@ module Phronomy
|
|
|
603
603
|
# Streaming implementation for #stream.
|
|
604
604
|
def _stream_impl(input, messages: [], thread_id: nil, config: {}, &block)
|
|
605
605
|
trace("agent.invoke", input: input, **_build_caller_meta(config)) do |_span|
|
|
606
|
-
|
|
606
|
+
input = run_input_filters!(input)
|
|
607
607
|
|
|
608
608
|
chat = build_chat
|
|
609
609
|
user_message = extract_message(input)
|
|
@@ -634,7 +634,7 @@ module Phronomy
|
|
|
634
634
|
run_before_completion_hooks!(chat, config)
|
|
635
635
|
|
|
636
636
|
output, usage = _drain_stream(chat, user_message, config, &block)
|
|
637
|
-
|
|
637
|
+
output = run_output_filters!(output)
|
|
638
638
|
|
|
639
639
|
result = {output: output, messages: chat.messages, usage: usage}
|
|
640
640
|
block.call(StreamEvent.new(type: :done, payload: result))
|
|
@@ -813,7 +813,7 @@ module Phronomy
|
|
|
813
813
|
# wrap it in a retry loop without duplicating the LLM interaction logic.
|
|
814
814
|
def invoke_once(input, messages: [], thread_id: nil, config: {})
|
|
815
815
|
trace("agent.invoke", input: input, **_build_caller_meta(config)) do |_span|
|
|
816
|
-
|
|
816
|
+
input = run_input_filters!(input)
|
|
817
817
|
|
|
818
818
|
user_message = extract_message(input)
|
|
819
819
|
chat = build_chat
|
|
@@ -835,7 +835,8 @@ module Phronomy
|
|
|
835
835
|
)
|
|
836
836
|
next [result, usage] if result[:suspended]
|
|
837
837
|
|
|
838
|
-
|
|
838
|
+
filtered_output = run_output_filters!(result[:output])
|
|
839
|
+
result = result.merge(output: filtered_output) unless filtered_output.equal?(result[:output])
|
|
839
840
|
[result, usage]
|
|
840
841
|
end
|
|
841
842
|
end
|
|
@@ -1076,21 +1077,34 @@ module Phronomy
|
|
|
1076
1077
|
end
|
|
1077
1078
|
|
|
1078
1079
|
# Step 3: wrap with approval gate when handler is registered.
|
|
1079
|
-
|
|
1080
|
+
if resolved.requires_approval && @approval_handler
|
|
1081
|
+
handler = @approval_handler
|
|
1082
|
+
# Capture the effective tool name before building the anonymous subclass.
|
|
1083
|
+
# Class-level instance variables (@tool_name) are not inherited through
|
|
1084
|
+
# subclassing, so the wrapper must set it explicitly.
|
|
1085
|
+
effective_name = resolved.new.name
|
|
1086
|
+
resolved = Class.new(resolved) do
|
|
1087
|
+
tool_name effective_name
|
|
1088
|
+
define_method(:call) do |args|
|
|
1089
|
+
if handler.call(name, args)
|
|
1090
|
+
super(args)
|
|
1091
|
+
else
|
|
1092
|
+
"Tool execution denied."
|
|
1093
|
+
end
|
|
1094
|
+
end
|
|
1095
|
+
end
|
|
1096
|
+
end
|
|
1097
|
+
|
|
1098
|
+
# Step 4: wrap with tool result filters when registered.
|
|
1099
|
+
result_filters = _tool_result_filters_for(tool_class)
|
|
1100
|
+
return resolved if result_filters.empty?
|
|
1080
1101
|
|
|
1081
|
-
|
|
1082
|
-
# Capture the effective tool name before building the anonymous subclass.
|
|
1083
|
-
# Class-level instance variables (@tool_name) are not inherited through
|
|
1084
|
-
# subclassing, so the wrapper must set it explicitly.
|
|
1085
|
-
effective_name = resolved.new.name
|
|
1102
|
+
effective_name4 = resolved.new.name
|
|
1086
1103
|
Class.new(resolved) do
|
|
1087
|
-
tool_name
|
|
1104
|
+
tool_name effective_name4
|
|
1088
1105
|
define_method(:call) do |args|
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
else
|
|
1092
|
-
"Tool execution denied."
|
|
1093
|
-
end
|
|
1106
|
+
result = super(args)
|
|
1107
|
+
result_filters.inject(result) { |val, f| f.call(val, tool_name: name, args: args) }
|
|
1094
1108
|
end
|
|
1095
1109
|
end
|
|
1096
1110
|
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
module Concerns
|
|
6
|
+
# Adds input, output, and tool-result filter support to an agent.
|
|
7
|
+
#
|
|
8
|
+
# Filters transform (or block) values at three call sites:
|
|
9
|
+
# - *input* — the raw user input string, before the LLM is called
|
|
10
|
+
# - *output* — the final LLM output string, before it is returned
|
|
11
|
+
# - *tool result* — the return value of each tool call
|
|
12
|
+
#
|
|
13
|
+
# Each filter in the chain receives the value returned by the previous one.
|
|
14
|
+
# A {Phronomy::FilterBlockError} raised inside any filter propagates to the
|
|
15
|
+
# caller.
|
|
16
|
+
#
|
|
17
|
+
# @api private
|
|
18
|
+
module Filterable
|
|
19
|
+
def self.included(base)
|
|
20
|
+
base.extend(ClassMethods)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Class-level DSL mixed into the including agent class.
|
|
24
|
+
module ClassMethods
|
|
25
|
+
# Registers a filter applied to every invocation's user input.
|
|
26
|
+
# Accepts either a {Phronomy::Filter::Base} instance or a subclass;
|
|
27
|
+
# when a class is given it is instantiated with +.new+.
|
|
28
|
+
# @param filter [Phronomy::Filter::Base, Class<Phronomy::Filter::Base>]
|
|
29
|
+
# @return [void]
|
|
30
|
+
# @api public
|
|
31
|
+
def input_filter(filter)
|
|
32
|
+
@_class_input_filters ||= []
|
|
33
|
+
@_class_input_filters << _resolve_filter(filter)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Registers a filter applied to every invocation's final LLM output.
|
|
37
|
+
# Accepts either a {Phronomy::Filter::Base} instance or a subclass;
|
|
38
|
+
# when a class is given it is instantiated with +.new+.
|
|
39
|
+
# @param filter [Phronomy::Filter::Base, Class<Phronomy::Filter::Base>]
|
|
40
|
+
# @return [void]
|
|
41
|
+
# @api public
|
|
42
|
+
def output_filter(filter)
|
|
43
|
+
@_class_output_filters ||= []
|
|
44
|
+
@_class_output_filters << _resolve_filter(filter)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Registers a filter applied to every tool result for all tools.
|
|
48
|
+
# Accepts either a {Phronomy::Filter::Base} instance or a subclass;
|
|
49
|
+
# when a class is given it is instantiated with +.new+.
|
|
50
|
+
# @param filter [Phronomy::Filter::Base, Class<Phronomy::Filter::Base>]
|
|
51
|
+
# @return [void]
|
|
52
|
+
# @api public
|
|
53
|
+
def tool_result_filter(filter)
|
|
54
|
+
@_class_tool_result_filters ||= []
|
|
55
|
+
@_class_tool_result_filters << _resolve_filter(filter)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [Array<Phronomy::Filter::Base>]
|
|
59
|
+
# @api private
|
|
60
|
+
def _class_input_filters
|
|
61
|
+
@_class_input_filters || []
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @return [Array<Phronomy::Filter::Base>]
|
|
65
|
+
# @api private
|
|
66
|
+
def _class_output_filters
|
|
67
|
+
@_class_output_filters || []
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @return [Array<Phronomy::Filter::Base>]
|
|
71
|
+
# @api private
|
|
72
|
+
def _class_tool_result_filters
|
|
73
|
+
@_class_tool_result_filters || []
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# Coerce +filter+ to an instance: if a Class is passed, call +.new+;
|
|
79
|
+
# otherwise return the object as-is.
|
|
80
|
+
# @param filter [Phronomy::Filter::Base, Class<Phronomy::Filter::Base>]
|
|
81
|
+
# @return [Phronomy::Filter::Base]
|
|
82
|
+
# @api private
|
|
83
|
+
def _resolve_filter(filter)
|
|
84
|
+
filter.is_a?(Class) ? filter.new : filter
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Registers an input filter on this instance.
|
|
89
|
+
# Accepts either a {Phronomy::Filter::Base} instance or a subclass;
|
|
90
|
+
# when a class is given it is instantiated with +.new+.
|
|
91
|
+
# Runs in addition to any class-level input filters.
|
|
92
|
+
# @param filter [Phronomy::Filter::Base, Class<Phronomy::Filter::Base>]
|
|
93
|
+
# @return [self]
|
|
94
|
+
# @api public
|
|
95
|
+
def add_input_filter(filter)
|
|
96
|
+
@_instance_input_filters ||= []
|
|
97
|
+
@_instance_input_filters << _resolve_filter(filter)
|
|
98
|
+
self
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Registers an output filter on this instance.
|
|
102
|
+
# Accepts either a {Phronomy::Filter::Base} instance or a subclass;
|
|
103
|
+
# when a class is given it is instantiated with +.new+.
|
|
104
|
+
# Runs in addition to any class-level output filters.
|
|
105
|
+
# @param filter [Phronomy::Filter::Base, Class<Phronomy::Filter::Base>]
|
|
106
|
+
# @return [self]
|
|
107
|
+
# @api public
|
|
108
|
+
def add_output_filter(filter)
|
|
109
|
+
@_instance_output_filters ||= []
|
|
110
|
+
@_instance_output_filters << _resolve_filter(filter)
|
|
111
|
+
self
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Registers a tool result filter on this instance.
|
|
115
|
+
#
|
|
116
|
+
# When called with two arguments, the filter is scoped to the given tool
|
|
117
|
+
# class only. When called with one argument, it applies to all tools.
|
|
118
|
+
# Accepts either a {Phronomy::Filter::Base} instance or a subclass;
|
|
119
|
+
# when a class is given it is instantiated with +.new+.
|
|
120
|
+
#
|
|
121
|
+
# @overload add_tool_result_filter(filter)
|
|
122
|
+
# @param filter [Phronomy::Filter::Base, Class<Phronomy::Filter::Base>] applied to every tool
|
|
123
|
+
# @overload add_tool_result_filter(tool_class, filter)
|
|
124
|
+
# @param tool_class [Class] scope the filter to this tool
|
|
125
|
+
# @param filter [Phronomy::Filter::Base, Class<Phronomy::Filter::Base>]
|
|
126
|
+
# @return [self]
|
|
127
|
+
# @api public
|
|
128
|
+
def add_tool_result_filter(tool_class_or_filter, filter = nil)
|
|
129
|
+
if filter.nil?
|
|
130
|
+
# Single-argument form: apply to all tools.
|
|
131
|
+
@_instance_tool_result_filters ||= []
|
|
132
|
+
@_instance_tool_result_filters << _resolve_filter(tool_class_or_filter)
|
|
133
|
+
else
|
|
134
|
+
# Two-argument form: scoped to one tool class.
|
|
135
|
+
@_scoped_tool_result_filters ||= {}
|
|
136
|
+
(@_scoped_tool_result_filters[tool_class_or_filter] ||= []) << _resolve_filter(filter)
|
|
137
|
+
end
|
|
138
|
+
self
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
# Run input filters (class-level then instance-level).
|
|
144
|
+
# @param input [String, Hash] the raw user input
|
|
145
|
+
# @return [String, Hash] the (possibly transformed) input
|
|
146
|
+
# @api private
|
|
147
|
+
def run_input_filters!(input)
|
|
148
|
+
class_filters = self.class._class_input_filters
|
|
149
|
+
inst_filters = @_instance_input_filters || []
|
|
150
|
+
(class_filters + inst_filters).inject(input) { |val, f| f.call(val) }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Run output filters (class-level then instance-level).
|
|
154
|
+
# @param output [String] the LLM output
|
|
155
|
+
# @return [String] the (possibly transformed) output
|
|
156
|
+
# @api private
|
|
157
|
+
def run_output_filters!(output)
|
|
158
|
+
class_filters = self.class._class_output_filters
|
|
159
|
+
inst_filters = @_instance_output_filters || []
|
|
160
|
+
(class_filters + inst_filters).inject(output) { |val, f| f.call(val) }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Collect all tool-result filters (global + scoped) for a given tool class.
|
|
164
|
+
# @param tool_class [Class]
|
|
165
|
+
# @return [Array<Phronomy::Filter::Base>]
|
|
166
|
+
# @api private
|
|
167
|
+
def _tool_result_filters_for(tool_class)
|
|
168
|
+
global = self.class._class_tool_result_filters + (@_instance_tool_result_filters || [])
|
|
169
|
+
scoped = (@_scoped_tool_result_filters || {})[tool_class] || []
|
|
170
|
+
global + scoped
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Coerce +filter+ to an instance: if a Class is passed, call +.new+;
|
|
174
|
+
# otherwise return the object as-is.
|
|
175
|
+
# @param filter [Phronomy::Filter::Base, Class<Phronomy::Filter::Base>]
|
|
176
|
+
# @return [Phronomy::Filter::Base]
|
|
177
|
+
# @api private
|
|
178
|
+
def _resolve_filter(filter)
|
|
179
|
+
filter.is_a?(Class) ? filter.new : filter
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -6,7 +6,7 @@ module Phronomy
|
|
|
6
6
|
# Adds configurable retry behaviour to an agent.
|
|
7
7
|
#
|
|
8
8
|
# Included in {Phronomy::Agent::Base}. The retry loop wraps the full
|
|
9
|
-
# #invoke_once call; {Phronomy::
|
|
9
|
+
# #invoke_once call; {Phronomy::FilterBlockError} is never retried.
|
|
10
10
|
# @api private
|
|
11
11
|
module Retryable
|
|
12
12
|
def self.included(base)
|
|
@@ -16,7 +16,7 @@ module Phronomy
|
|
|
16
16
|
# Class-level DSL methods mixed into the including agent class.
|
|
17
17
|
module ClassMethods
|
|
18
18
|
# Configures a retry policy that wraps the full #invoke call.
|
|
19
|
-
#
|
|
19
|
+
# FilterBlockError is never retried regardless of this setting.
|
|
20
20
|
#
|
|
21
21
|
# @param times [Integer] maximum retry attempts (default: 0)
|
|
22
22
|
# @param wait [Symbol, Numeric] :exponential, :linear, or a fixed Float
|
|
@@ -60,7 +60,7 @@ module Phronomy
|
|
|
60
60
|
attempt = 0
|
|
61
61
|
begin
|
|
62
62
|
invoke_once(input, messages: messages, thread_id: thread_id, config: config)
|
|
63
|
-
rescue Phronomy::
|
|
63
|
+
rescue Phronomy::FilterBlockError
|
|
64
64
|
raise
|
|
65
65
|
rescue Phronomy::CancellationError
|
|
66
66
|
raise # Never retry after cancellation.
|
|
@@ -80,7 +80,7 @@ module Phronomy
|
|
|
80
80
|
# @return [Hash] +{ output: String, suspended: false, messages: Array, usage: Phronomy::TokenUsage }+
|
|
81
81
|
# or +{ output: nil, suspended: true, checkpoint: Phronomy::Agent::Checkpoint, messages: Array }+
|
|
82
82
|
# when a second approval-required tool is encountered during continuation
|
|
83
|
-
# @raise [Phronomy::
|
|
83
|
+
# @raise [Phronomy::FilterBlockError] when an output filter rejects the value
|
|
84
84
|
# @raise [Phronomy::CheckpointAlreadyResumedError] when the checkpoint has already been consumed
|
|
85
85
|
# @api private
|
|
86
86
|
def resume(checkpoint, approved:, config: {})
|
|
@@ -143,7 +143,7 @@ module Phronomy
|
|
|
143
143
|
output = response.content
|
|
144
144
|
usage = Phronomy::TokenUsage.from_tokens(response.tokens)
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
output = run_output_filters!(output)
|
|
147
147
|
|
|
148
148
|
{output: output, suspended: false, messages: chat.messages, usage: usage}
|
|
149
149
|
end
|
|
@@ -92,7 +92,7 @@ module Phronomy
|
|
|
92
92
|
public
|
|
93
93
|
|
|
94
94
|
# Sets the access scope for this tool (metadata; enforcement is the responsibility of
|
|
95
|
-
# the Workflow/
|
|
95
|
+
# the Workflow/Filter layer).
|
|
96
96
|
# @param value [Symbol] e.g. :read_only, :write, :admin
|
|
97
97
|
# @api public
|
|
98
98
|
# mutant:disable - neutral failure: unparser round-trip produces different source
|
|
@@ -218,7 +218,7 @@ module Phronomy
|
|
|
218
218
|
# retried up to +times+ times with the specified wait strategy.
|
|
219
219
|
# Multiple policies can be registered and are evaluated in order.
|
|
220
220
|
#
|
|
221
|
-
#
|
|
221
|
+
# FilterBlockError is never retried regardless of this configuration.
|
|
222
222
|
#
|
|
223
223
|
# @param exception_classes [Array<Class>] exception classes to retry on
|
|
224
224
|
# @param times [Integer] maximum retry attempts (default: 1)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Filter
|
|
5
|
+
# Abstract base class for value filters.
|
|
6
|
+
#
|
|
7
|
+
# A filter may either transform a value (return the new value) or block it
|
|
8
|
+
# (raise {Phronomy::FilterBlockError}). The same filter instance can be
|
|
9
|
+
# registered at multiple call sites — input, output, and tool result.
|
|
10
|
+
#
|
|
11
|
+
# @example PII masking filter
|
|
12
|
+
# class PiiMaskFilter < Phronomy::Filter::Base
|
|
13
|
+
# def call(value, **_context)
|
|
14
|
+
# value.to_s
|
|
15
|
+
# .gsub(/\b\d{2,4}-\d{2,4}-\d{4}\b/, "[PHONE]")
|
|
16
|
+
# .gsub(/\b(?:\d{4}[- ]?){3}\d{4}\b/, "[CARD]")
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# @example Blocking filter
|
|
21
|
+
# class NoBadWordFilter < Phronomy::Filter::Base
|
|
22
|
+
# def call(value, **_context)
|
|
23
|
+
# block!("Forbidden content detected") if value.to_s.include?("badword")
|
|
24
|
+
# value
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# @api public
|
|
29
|
+
class Base
|
|
30
|
+
# Process +value+ and return the (possibly transformed) result.
|
|
31
|
+
#
|
|
32
|
+
# The +context+ keyword arguments vary by call site:
|
|
33
|
+
# - Tool result: +{ tool_name: String, args: Hash }+
|
|
34
|
+
# - Input / output: +(empty)+
|
|
35
|
+
#
|
|
36
|
+
# @param value [Object] the value being filtered
|
|
37
|
+
# @param context [Hash] optional call-site metadata
|
|
38
|
+
# @return [Object] the transformed value (or the original if unchanged)
|
|
39
|
+
# @raise [Phronomy::FilterBlockError] to reject the value
|
|
40
|
+
# @api public
|
|
41
|
+
def call(value, **_context)
|
|
42
|
+
raise NotImplementedError, "#{self.class}#call is not implemented"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
protected
|
|
46
|
+
|
|
47
|
+
# Reject the value with a human-readable reason.
|
|
48
|
+
# @param reason [String]
|
|
49
|
+
# @raise [Phronomy::FilterBlockError]
|
|
50
|
+
# @api public
|
|
51
|
+
def block!(reason)
|
|
52
|
+
raise Phronomy::FilterBlockError.new(reason, filter: self)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
data/lib/phronomy/{guardrail/prompt_injection_guardrail.rb → filter/prompt_injection_filter.rb}
RENAMED
|
@@ -1,28 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
|
-
module
|
|
4
|
+
module Filter
|
|
5
5
|
# Detects potential prompt injection attempts in the agent input.
|
|
6
6
|
#
|
|
7
7
|
# Prompt injection is an attack where an adversary embeds LLM instructions
|
|
8
8
|
# inside data sources (e.g. RAG chunks, tool results, user input) to override
|
|
9
9
|
# the agent's intended behaviour.
|
|
10
10
|
#
|
|
11
|
-
# This
|
|
12
|
-
# calls {#
|
|
13
|
-
# an input
|
|
11
|
+
# This filter scans the input string for common injection patterns and
|
|
12
|
+
# calls {#block!} when a match is found. It is intended to be registered as
|
|
13
|
+
# an input filter on agents that consume untrusted external content.
|
|
14
14
|
#
|
|
15
15
|
# @example
|
|
16
16
|
# class MyAgent < Phronomy::Agent::Base
|
|
17
17
|
# model "gpt-4o"
|
|
18
|
-
#
|
|
18
|
+
# input_filter Phronomy::Filter::PromptInjectionFilter
|
|
19
19
|
# end
|
|
20
20
|
#
|
|
21
21
|
# @example Custom patterns
|
|
22
|
-
#
|
|
22
|
+
# filter = Phronomy::Filter::PromptInjectionFilter.new(
|
|
23
23
|
# extra_patterns: [/exfiltrate/i]
|
|
24
24
|
# )
|
|
25
|
-
|
|
25
|
+
# agent.add_input_filter(filter)
|
|
26
|
+
#
|
|
27
|
+
# @api public
|
|
28
|
+
class PromptInjectionFilter < Base
|
|
26
29
|
# Common prompt injection / jailbreak patterns.
|
|
27
30
|
DEFAULT_PATTERNS = [
|
|
28
31
|
/ignore\s+(previous|prior|all)\s+instructions?/i,
|
|
@@ -38,20 +41,24 @@ module Phronomy
|
|
|
38
41
|
].freeze
|
|
39
42
|
|
|
40
43
|
# @param extra_patterns [Array<Regexp>] additional patterns to scan for
|
|
41
|
-
# @api
|
|
44
|
+
# @api public
|
|
42
45
|
def initialize(extra_patterns: [])
|
|
43
46
|
super()
|
|
44
47
|
@patterns = DEFAULT_PATTERNS + extra_patterns
|
|
45
48
|
end
|
|
46
49
|
|
|
47
50
|
# Scans the input string for injection patterns.
|
|
48
|
-
# @param
|
|
49
|
-
# @
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
# @param value [String, Hash]
|
|
52
|
+
# @param context [Hash]
|
|
53
|
+
# @return [String, Hash] the original value when no injection is detected
|
|
54
|
+
# @raise [Phronomy::FilterBlockError] when a pattern matches
|
|
55
|
+
# @api public
|
|
56
|
+
def call(value, **_context)
|
|
57
|
+
text = value.is_a?(Hash) ? value.values.join(" ") : value.to_s
|
|
52
58
|
@patterns.each do |pattern|
|
|
53
|
-
|
|
59
|
+
block!("Potential prompt injection detected") if text.match?(pattern)
|
|
54
60
|
end
|
|
61
|
+
value
|
|
55
62
|
end
|
|
56
63
|
end
|
|
57
64
|
end
|
|
@@ -66,8 +66,7 @@ module Phronomy
|
|
|
66
66
|
# @!attribute [r] trusted
|
|
67
67
|
# @return [Boolean] true when confidence >= threshold
|
|
68
68
|
Result = Struct.new(
|
|
69
|
-
:output, :confidence, :citations, :iterations, :review_notes, :trusted
|
|
70
|
-
keyword_init: true
|
|
69
|
+
:output, :confidence, :citations, :iterations, :review_notes, :trusted
|
|
71
70
|
) do
|
|
72
71
|
# @return [Boolean] true when confidence >= threshold
|
|
73
72
|
alias_method :trusted?, :trusted
|
|
@@ -47,8 +47,7 @@ module Phronomy
|
|
|
47
47
|
:index, # Integer — 0-based worker index
|
|
48
48
|
:agent, # Agent::Base instance
|
|
49
49
|
:messages, # Array — accumulated conversation history
|
|
50
|
-
:status
|
|
51
|
-
keyword_init: true
|
|
50
|
+
:status # Symbol — :idle | :available | :done
|
|
52
51
|
) do
|
|
53
52
|
# Returns true when this worker is ready to accept the next task.
|
|
54
53
|
def available? = [:idle, :available].include?(status)
|
data/lib/phronomy/version.rb
CHANGED
data/lib/phronomy.rb
CHANGED
|
@@ -87,12 +87,15 @@ module Phronomy
|
|
|
87
87
|
end
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
# Raised by a {Phronomy::Filter::Base} subclass when the filter rejects a
|
|
91
|
+
# value without transforming it (blocking the pipeline).
|
|
92
|
+
# @api public
|
|
93
|
+
class FilterBlockError < Error
|
|
94
|
+
attr_reader :filter
|
|
92
95
|
|
|
93
|
-
def initialize(message,
|
|
96
|
+
def initialize(message, filter: nil)
|
|
94
97
|
super(message)
|
|
95
|
-
@
|
|
98
|
+
@filter = filter
|
|
96
99
|
end
|
|
97
100
|
end
|
|
98
101
|
|
data/scripts/api_snapshot.rb
CHANGED
|
@@ -32,8 +32,8 @@ PUBLIC_API_ENTRIES = [
|
|
|
32
32
|
# Beta
|
|
33
33
|
Phronomy::MultiAgent::Orchestrator,
|
|
34
34
|
Phronomy::MultiAgent::TeamCoordinator,
|
|
35
|
-
Phronomy::
|
|
36
|
-
Phronomy::
|
|
35
|
+
Phronomy::Filter::Base,
|
|
36
|
+
Phronomy::Filter::PromptInjectionFilter,
|
|
37
37
|
Phronomy::VectorStore::Base,
|
|
38
38
|
Phronomy::VectorStore::InMemory,
|
|
39
39
|
Phronomy::VectorStore::Embeddings::Base,
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: phronomy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Raizo T.C.S
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ruby_llm
|
|
@@ -64,9 +64,9 @@ dependencies:
|
|
|
64
64
|
- - "~>"
|
|
65
65
|
- !ruby/object:Gem::Version
|
|
66
66
|
version: '0.6'
|
|
67
|
-
description: Phronomy
|
|
68
|
-
|
|
69
|
-
LLM abstraction.
|
|
67
|
+
description: Phronomy is a Ruby AI agent framework that provides composable building
|
|
68
|
+
blocks — Agents, Workflows, Tools, Filters, and Tracing — for building AI agents
|
|
69
|
+
in Ruby. Powered by RubyLLM for LLM abstraction.
|
|
70
70
|
email:
|
|
71
71
|
- raizo.tcs@gmail.com
|
|
72
72
|
executables: []
|
|
@@ -109,7 +109,7 @@ files:
|
|
|
109
109
|
- lib/phronomy/agent/checkpoint_store.rb
|
|
110
110
|
- lib/phronomy/agent/concerns/before_completion.rb
|
|
111
111
|
- lib/phronomy/agent/concerns/error_translation.rb
|
|
112
|
-
- lib/phronomy/agent/concerns/
|
|
112
|
+
- lib/phronomy/agent/concerns/filterable.rb
|
|
113
113
|
- lib/phronomy/agent/concerns/retryable.rb
|
|
114
114
|
- lib/phronomy/agent/concerns/suspendable.rb
|
|
115
115
|
- lib/phronomy/agent/context/capability/base.rb
|
|
@@ -147,12 +147,10 @@ files:
|
|
|
147
147
|
- lib/phronomy/eval/scorer/llm_judge.rb
|
|
148
148
|
- lib/phronomy/event.rb
|
|
149
149
|
- lib/phronomy/event_loop.rb
|
|
150
|
+
- lib/phronomy/filter.rb
|
|
151
|
+
- lib/phronomy/filter/base.rb
|
|
152
|
+
- lib/phronomy/filter/prompt_injection_filter.rb
|
|
150
153
|
- lib/phronomy/generator_verifier.rb
|
|
151
|
-
- lib/phronomy/guardrail.rb
|
|
152
|
-
- lib/phronomy/guardrail/base.rb
|
|
153
|
-
- lib/phronomy/guardrail/input_guardrail.rb
|
|
154
|
-
- lib/phronomy/guardrail/output_guardrail.rb
|
|
155
|
-
- lib/phronomy/guardrail/prompt_injection_guardrail.rb
|
|
156
154
|
- lib/phronomy/invocation_context.rb
|
|
157
155
|
- lib/phronomy/knowledge_source.rb
|
|
158
156
|
- lib/phronomy/llm_adapter.rb
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Agent
|
|
5
|
-
module Concerns
|
|
6
|
-
# Adds input and output guardrail support to an agent.
|
|
7
|
-
#
|
|
8
|
-
# Included in {Phronomy::Agent::Base}. Guardrails are run on the raw
|
|
9
|
-
# input string before the LLM is called, and on the raw output string
|
|
10
|
-
# before the result is returned to the caller.
|
|
11
|
-
# @api private
|
|
12
|
-
module Guardrailable
|
|
13
|
-
# Attach a guardrail that validates input before every #invoke call.
|
|
14
|
-
# @param guardrail [Phronomy::Guardrail::InputGuardrail]
|
|
15
|
-
# @return [self]
|
|
16
|
-
# @api private
|
|
17
|
-
def add_input_guardrail(guardrail)
|
|
18
|
-
@input_guardrails ||= []
|
|
19
|
-
@input_guardrails << guardrail
|
|
20
|
-
self
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# Attach a guardrail that validates output before it is returned.
|
|
24
|
-
# @param guardrail [Phronomy::Guardrail::OutputGuardrail]
|
|
25
|
-
# @return [self]
|
|
26
|
-
# @api private
|
|
27
|
-
def add_output_guardrail(guardrail)
|
|
28
|
-
@output_guardrails ||= []
|
|
29
|
-
@output_guardrails << guardrail
|
|
30
|
-
self
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
private
|
|
34
|
-
|
|
35
|
-
def run_input_guardrails!(input)
|
|
36
|
-
(@input_guardrails || []).each { |g| g.run!(input) }
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def run_output_guardrails!(output)
|
|
40
|
-
(@output_guardrails || []).each { |g| g.run!(output) }
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Guardrail
|
|
5
|
-
# Abstract base class for all guardrails.
|
|
6
|
-
#
|
|
7
|
-
# Subclasses override #check to validate input or output.
|
|
8
|
-
# Call #fail! inside #check to reject with a reason.
|
|
9
|
-
#
|
|
10
|
-
# @example
|
|
11
|
-
# class NoPIIGuardrail < Phronomy::Guardrail::InputGuardrail
|
|
12
|
-
# def check(input)
|
|
13
|
-
# fail!("PII detected") if input.to_s.match?(/\d{3}-\d{2}-\d{4}/)
|
|
14
|
-
# end
|
|
15
|
-
# end
|
|
16
|
-
class Base
|
|
17
|
-
# Validate the value. Subclasses must implement this method.
|
|
18
|
-
# @param value [Object] the input or output being checked
|
|
19
|
-
# @raise [Phronomy::GuardrailError] if the guardrail rejects the value
|
|
20
|
-
# @api public
|
|
21
|
-
def check(value)
|
|
22
|
-
raise NotImplementedError, "#{self.class}#check is not implemented"
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Run the check, raising GuardrailError on failure.
|
|
26
|
-
# @param value [Object]
|
|
27
|
-
# @return [Object] the original value (unchanged) when the check passes
|
|
28
|
-
# @api public
|
|
29
|
-
def run!(value)
|
|
30
|
-
check(value)
|
|
31
|
-
value
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
protected
|
|
35
|
-
|
|
36
|
-
# Call inside #check to reject the value.
|
|
37
|
-
# @param reason [String] human-readable rejection reason
|
|
38
|
-
# @raise [Phronomy::GuardrailError]
|
|
39
|
-
# @api public
|
|
40
|
-
def fail!(reason)
|
|
41
|
-
raise Phronomy::GuardrailError.new(reason, guardrail: self)
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Guardrail
|
|
5
|
-
# Guardrail applied to agent/chain input before it reaches the LLM.
|
|
6
|
-
#
|
|
7
|
-
# @example
|
|
8
|
-
# class NoCreditCardGuardrail < Phronomy::Guardrail::InputGuardrail
|
|
9
|
-
# def check(input)
|
|
10
|
-
# fail!("Credit card numbers are not allowed") if input.to_s.match?(/\d{4}[- ]\d{4}[- ]\d{4}[- ]\d{4}/)
|
|
11
|
-
# end
|
|
12
|
-
# end
|
|
13
|
-
#
|
|
14
|
-
# agent = MyAgent.new
|
|
15
|
-
# agent.add_input_guardrail(NoCreditCardGuardrail.new)
|
|
16
|
-
class InputGuardrail < Base
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Guardrail
|
|
5
|
-
# Guardrail applied to agent/chain output before it is returned to the caller.
|
|
6
|
-
#
|
|
7
|
-
# @example
|
|
8
|
-
# class NoSecretsGuardrail < Phronomy::Guardrail::OutputGuardrail
|
|
9
|
-
# def check(output)
|
|
10
|
-
# fail!("Response contains a secret key") if output.to_s.match?(/sk-[A-Za-z0-9]{32,}/)
|
|
11
|
-
# end
|
|
12
|
-
# end
|
|
13
|
-
#
|
|
14
|
-
# agent = MyAgent.new
|
|
15
|
-
# agent.add_output_guardrail(NoSecretsGuardrail.new)
|
|
16
|
-
class OutputGuardrail < Base
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
data/lib/phronomy/guardrail.rb
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Convenience require for Guardrail sub-classes.
|
|
4
|
-
# Zeitwerk auto-loads individual files; this is only needed for explicit requires.
|
|
5
|
-
require_relative "guardrail/base"
|
|
6
|
-
require_relative "guardrail/input_guardrail"
|
|
7
|
-
require_relative "guardrail/output_guardrail"
|