phronomy 0.9.1 → 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 +98 -2
- data/README.md +29 -28
- data/lib/phronomy/agent/base.rb +34 -70
- 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/task/mapped_backend.rb +78 -0
- data/lib/phronomy/task.rb +53 -0
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy.rb +9 -5
- data/scripts/api_snapshot.rb +2 -2
- metadata +10 -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,12 +9,90 @@ 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.
|
|
13
37
|
|
|
14
|
-
|
|
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
|
+
|
|
53
|
+
|
|
54
|
+
## [0.10.0] - 2026-06-08
|
|
15
55
|
|
|
16
56
|
### Added
|
|
17
57
|
|
|
58
|
+
- **`Task#map` — transform a Task's completed value** (#384):
|
|
59
|
+
`task.map { |result| ctx.merge(answer: result[:output]) }` returns a new `Task`
|
|
60
|
+
whose completed value is the block's return value. Primary use-case: wire
|
|
61
|
+
`Agent::Base#invoke_async` into a Workflow entry action so the agent result
|
|
62
|
+
reaches `WorkflowContext` through the standard `:action_completed` path without
|
|
63
|
+
requiring any changes to `FSMSession`. If the source task fails or is cancelled,
|
|
64
|
+
the mapped task propagates the error without calling the block.
|
|
65
|
+
|
|
66
|
+
### Removed
|
|
67
|
+
|
|
68
|
+
- **`Agent::Base#run_as_child` removed** (#384):
|
|
69
|
+
Introduced when agents had their own FSM (`Agent::FSM`). After `Agent::FSM` was
|
|
70
|
+
removed in v0.9.0, the `:child_completed` event payload was silently discarded by
|
|
71
|
+
`FSMSession`, so `ctx.answer` was never populated. Migrate to
|
|
72
|
+
`invoke_async + Task#map`:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
# Before (removed)
|
|
76
|
+
entry :translate, ->(ctx) { TranslationAgent.new.run_as_child(ctx.query, ctx: ctx) }
|
|
77
|
+
transition from: :translate, on: :child_completed, to: :done
|
|
78
|
+
|
|
79
|
+
# After (recommended)
|
|
80
|
+
entry :translate, ->(ctx) {
|
|
81
|
+
TranslationAgent.new.invoke_async(ctx.query).map { |r| ctx.merge(answer: r[:output]) }
|
|
82
|
+
}
|
|
83
|
+
transition from: :translate, to: :done
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Added
|
|
87
|
+
|
|
88
|
+
- **`Task#map` — transform a Task's completed value** (post-v0.9.0):
|
|
89
|
+
`task.map { |result| ctx.merge(answer: result[:output]) }` returns a new `Task`
|
|
90
|
+
whose completed value is the block's return value. The primary use-case is
|
|
91
|
+
connecting `Agent::Base#invoke_async` to a Workflow entry action so the agent
|
|
92
|
+
result populates the `WorkflowContext` through the standard `:action_completed`
|
|
93
|
+
path. If the source task fails or is cancelled, the mapped task propagates the
|
|
94
|
+
error without calling the block.
|
|
95
|
+
|
|
18
96
|
- **`Phronomy::Diagnostics` and `SchedulerReentrancyError`** (#278, #279):
|
|
19
97
|
`Phronomy::Diagnostics` exposes a snapshot of current scheduler state
|
|
20
98
|
(`pending_count`, `active_tasks`, `pool_utilization`, etc.) for debugging and
|
|
@@ -233,6 +311,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
233
311
|
|
|
234
312
|
### Added (post-v0.9.0)
|
|
235
313
|
|
|
314
|
+
- **`Agent::Base#run_as_child` removed** (post-v0.9.0):
|
|
315
|
+
`run_as_child` was introduced when agents had their own FSM. After `Agent::FSM`
|
|
316
|
+
was removed, the `:child_completed` event payload was silently discarded by
|
|
317
|
+
`FSMSession`, meaning `ctx.answer` was never populated. The method has been
|
|
318
|
+
removed. Use `invoke_async + Task#map` instead:
|
|
319
|
+
|
|
320
|
+
```ruby
|
|
321
|
+
# Before (deprecated)
|
|
322
|
+
entry :translate, ->(ctx) { TranslationAgent.new.run_as_child(ctx.query, ctx: ctx) }
|
|
323
|
+
transition from: :translate, on: :child_completed, to: :done
|
|
324
|
+
|
|
325
|
+
# After (recommended)
|
|
326
|
+
entry :translate, ->(ctx) {
|
|
327
|
+
TranslationAgent.new.invoke_async(ctx.query).map { |r| ctx.merge(answer: r[:output]) }
|
|
328
|
+
}
|
|
329
|
+
transition from: :translate, to: :done
|
|
330
|
+
```
|
|
331
|
+
|
|
236
332
|
- **`Phronomy::Agent::CheckpointStore` — idempotency store for HITL resume** (post-v0.9.0):
|
|
237
333
|
New in-memory store tracks consumed checkpoint IDs. Calling `Agent::Base#resume` twice
|
|
238
334
|
with the same checkpoint raises `Phronomy::CheckpointAlreadyResumedError` instead of
|
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 |
|
|
@@ -54,8 +54,9 @@ It provides composable building blocks — Workflows, Agents, Tools, Guardrails,
|
|
|
54
54
|
| Feature | Stability |
|
|
55
55
|
|---|---|
|
|
56
56
|
| **Workflow EventLoop Mode** — Opt-in event-driven execution: `Phronomy.configure { \|c\| c.event_loop = true }` | Experimental |
|
|
57
|
-
| **Agent EventLoop Mode** — `Agent#invoke` (non-blocking via EventLoop), `Agent#
|
|
57
|
+
| **Agent EventLoop Mode** — `Agent#invoke` (non-blocking via EventLoop), `Agent#invoke_async` + `Task#map` (child-agent pattern for Workflow integration), parallel tool dispatch via `ParallelToolChat` | Experimental |
|
|
58
58
|
| **`invoke_async` / `call_async`** — `Agent::Base#invoke_async` and `Workflow#invoke_async` return a `Task`; `Agent::Context::Capability::Base#call_async` similarly; compatible with EventLoop and standalone contexts | Experimental |
|
|
59
|
+
| **`Task#map`** — transforms a `Task`'s completed value via a block; returns a new `Task` whose value is the block's return value; if the source task fails or is cancelled the mapped task propagates the error without calling the block; primary use-case: `invoke_async.map { \|r\| ctx.merge(answer: r[:output]) }` to wire agent results into a `WorkflowContext` | Experimental |
|
|
59
60
|
| **CancellationToken** — Cooperative cancellation via `cancel!`/`cancelled?`/`raise_if_cancelled!`; `timeout_after(seconds)` for monotonic-clock deadlines; optional `deadline:` (wall-clock) for backward compatibility; passed as `config: { cancellation_token: token }` to agents and `dispatch_parallel`; injected into `tool.execute` when the method declares a `cancellation_token:` keyword | Experimental |
|
|
60
61
|
| **`dispatch_parallel` / `fan_out` `force_kill:` option** — `force_kill: false` (default) leaves timed-out workers running and raises `TimeoutError` immediately; `force_kill: true` restores the old `Thread#kill` behaviour with a `logger.warn` | Beta |
|
|
61
62
|
| **`execution_mode` DSL on `Agent::Context::Capability::Base`** — Declares how a tool's `execute` should be dispatched: `:cooperative` (same scheduler thread), `:blocking_io` (default; offloaded to `BlockingAdapterPool`), `:cpu_bound`, `:external_process` | Experimental |
|
|
@@ -77,6 +78,7 @@ It provides composable building blocks — Workflows, Agents, Tools, Guardrails,
|
|
|
77
78
|
| **Agent::SharedState** — Shared state pattern: peer agents collaborate via a shared KnowledgeStore; `member` DSL with per-agent instructions and `coordination` team protocol | Experimental |
|
|
78
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 |
|
|
79
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 |
|
|
80
82
|
|
|
81
83
|
> **Public API boundary**: The tables above are the complete list of classes, modules, and features
|
|
82
84
|
> intended for gem consumers. Every entry has an associated stability label.
|
|
@@ -199,20 +201,18 @@ final = app.send_event(state: state, event: :approve)
|
|
|
199
201
|
puts "Approved: #{final.approved}" # => true
|
|
200
202
|
```
|
|
201
203
|
|
|
202
|
-
In EventLoop mode (`c.event_loop = true`), `
|
|
203
|
-
asynchronously
|
|
204
|
-
|
|
205
|
-
dispatched. Always declare both transitions to avoid a stuck workflow:
|
|
204
|
+
In EventLoop mode (`c.event_loop = true`), use `invoke_async + Task#map` to run an agent
|
|
205
|
+
asynchronously inside a Workflow entry action. The mapped Task returns a `WorkflowContext`,
|
|
206
|
+
which `FSMSession` picks up via the standard `:action_completed` path:
|
|
206
207
|
|
|
207
208
|
```ruby
|
|
208
|
-
# EventLoop mode: workflow that runs an agent
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
209
|
+
# EventLoop mode: workflow that runs an agent and captures the result.
|
|
210
|
+
entry :translate, ->(ctx) {
|
|
211
|
+
TranslationAgent.new.invoke_async(ctx.query).map do |result|
|
|
212
|
+
ctx.merge(answer: result[:output]) # returns WorkflowContext
|
|
213
|
+
end
|
|
213
214
|
}
|
|
214
|
-
transition from: :
|
|
215
|
-
transition from: :run_agent, on: :child_failed, to: :handle_error
|
|
215
|
+
transition from: :translate, to: :done # no on: needed
|
|
216
216
|
```
|
|
217
217
|
|
|
218
218
|
### Multi-Agent — Agent-as-Tool pattern
|
|
@@ -253,30 +253,31 @@ result = OrchestratorAgent.new.invoke("Write a blog post about Ruby 3.4 features
|
|
|
253
253
|
puts result[:output]
|
|
254
254
|
```
|
|
255
255
|
|
|
256
|
-
###
|
|
256
|
+
### Filters — Input/output transformation and blocking
|
|
257
257
|
|
|
258
|
-
|
|
259
|
-
|
|
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`).
|
|
260
260
|
|
|
261
261
|
```ruby
|
|
262
|
-
class
|
|
263
|
-
def
|
|
264
|
-
|
|
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
|
|
265
266
|
end
|
|
266
267
|
end
|
|
267
268
|
|
|
268
269
|
agent = ResearchAgent.new
|
|
269
|
-
agent.
|
|
270
|
+
agent.add_input_filter(NoCreditCardFilter.new)
|
|
270
271
|
|
|
271
272
|
begin
|
|
272
273
|
agent.invoke("Charge 4111-1111-1111-1111")
|
|
273
|
-
rescue Phronomy::
|
|
274
|
+
rescue Phronomy::FilterBlockError => e
|
|
274
275
|
puts e.message # => "Credit card numbers are not allowed"
|
|
275
276
|
end
|
|
276
277
|
```
|
|
277
278
|
|
|
278
|
-
> **Note:** Phronomy includes `
|
|
279
|
-
> input
|
|
279
|
+
> **Note:** Phronomy includes `PromptInjectionFilter`, a built-in pattern-based
|
|
280
|
+
> input filter that detects common injection patterns (see the feature table above).
|
|
280
281
|
> PII scanning and content classification are **not** provided by the framework;
|
|
281
282
|
> that logic must be implemented by the application. Reference implementations for
|
|
282
283
|
> common patterns are available in `phronomy-examples` (example 06).
|
|
@@ -852,11 +853,11 @@ span attributes by default (`trace_pii: false`). To include full content in trac
|
|
|
852
853
|
Phronomy configuration. Evaluate whether your tracing backend (OTLP collector, Jaeger,
|
|
853
854
|
Honeycomb, etc.) meets your data-retention and privacy requirements.
|
|
854
855
|
|
|
855
|
-
**Prompt injection** — Phronomy provides `
|
|
856
|
-
pattern-based input
|
|
856
|
+
**Prompt injection** — Phronomy provides `PromptInjectionFilter`, a built-in
|
|
857
|
+
pattern-based input filter that detects common injection patterns (ignore/override
|
|
857
858
|
instructions, role-switching phrases, etc.). It is a useful starting point, not a
|
|
858
859
|
comprehensive defence; applications processing untrusted input should layer additional
|
|
859
|
-
custom
|
|
860
|
+
custom filters as needed (see the Filters section above).
|
|
860
861
|
|
|
861
862
|
**Tool and MCP security** — Tools can perform real-world side effects (database
|
|
862
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]
|
|
@@ -532,56 +532,6 @@ module Phronomy
|
|
|
532
532
|
end
|
|
533
533
|
end
|
|
534
534
|
|
|
535
|
-
# Registers this agent as a child {AgentFSM} inside the given Workflow context.
|
|
536
|
-
#
|
|
537
|
-
# Use this method from a Workflow entry action (running on the EventLoop thread)
|
|
538
|
-
# instead of {#invoke}, which would raise a deadlock error because +invoke+ blocks
|
|
539
|
-
# on a +Thread::Queue+ when EventLoop mode is active.
|
|
540
|
-
#
|
|
541
|
-
# The agent runs asynchronously in a background IO thread. When it finishes, the
|
|
542
|
-
# parent {FSMSession} receives a +:child_completed+ event whose payload is the
|
|
543
|
-
# result hash +{ output:, messages:, usage: }+. Declare an +on: :child_completed+
|
|
544
|
-
# transition in your Workflow to advance to the next state.
|
|
545
|
-
#
|
|
546
|
-
# The result is delivered exclusively as the +:child_completed+ event payload.
|
|
547
|
-
# The parent Workflow task is the sole owner of the parent +WorkflowContext+ and
|
|
548
|
-
# applies the result after receiving the event — no background thread writes to
|
|
549
|
-
# the parent context directly.
|
|
550
|
-
#
|
|
551
|
-
# @example
|
|
552
|
-
# entry :run_agent, ->(ctx) { MyAgent.new.run_as_child(ctx.query, ctx: ctx) }
|
|
553
|
-
# transition from: :run_agent, on: :child_completed, to: :process_result
|
|
554
|
-
#
|
|
555
|
-
# @param input [String, Hash] user input passed to the agent
|
|
556
|
-
# @param ctx [Object] a WorkflowContext that responds to +#thread_id+
|
|
557
|
-
# @param messages [Array] prior conversation history
|
|
558
|
-
# @param config [Hash] invocation config (forwarded to +_invoke_impl+)
|
|
559
|
-
# @return [nil] the caller must not wait on any return value;
|
|
560
|
-
# the result arrives as a +:child_completed+ event
|
|
561
|
-
# @raise [Phronomy::Error] when EventLoop mode is not enabled
|
|
562
|
-
# @api public
|
|
563
|
-
def run_as_child(input, ctx:, messages: [], config: {})
|
|
564
|
-
unless Phronomy.configuration.event_loop
|
|
565
|
-
raise Phronomy::Error,
|
|
566
|
-
"run_as_child requires EventLoop mode. " \
|
|
567
|
-
"Enable with: Phronomy.configure { |c| c.event_loop = true }"
|
|
568
|
-
end
|
|
569
|
-
|
|
570
|
-
parent_id = ctx.thread_id
|
|
571
|
-
thread_id = "#{parent_id}_agent_#{SecureRandom.uuid}"
|
|
572
|
-
Phronomy::Runtime.instance.spawn(name: "agent-child:#{thread_id}") do
|
|
573
|
-
result = _invoke_impl(input, messages: messages, thread_id: thread_id, config: config)
|
|
574
|
-
Phronomy::EventLoop.instance.post(
|
|
575
|
-
Phronomy::Event.new(type: :child_completed, target_id: parent_id, payload: result)
|
|
576
|
-
)
|
|
577
|
-
rescue => e
|
|
578
|
-
Phronomy::EventLoop.instance.post(
|
|
579
|
-
Phronomy::Event.new(type: :child_failed, target_id: parent_id, payload: e)
|
|
580
|
-
)
|
|
581
|
-
end
|
|
582
|
-
nil
|
|
583
|
-
end
|
|
584
|
-
|
|
585
535
|
# Streaming version of #invoke. Yields {Phronomy::Agent::StreamEvent} objects
|
|
586
536
|
# as they are produced by the underlying LLM.
|
|
587
537
|
#
|
|
@@ -653,7 +603,7 @@ module Phronomy
|
|
|
653
603
|
# Streaming implementation for #stream.
|
|
654
604
|
def _stream_impl(input, messages: [], thread_id: nil, config: {}, &block)
|
|
655
605
|
trace("agent.invoke", input: input, **_build_caller_meta(config)) do |_span|
|
|
656
|
-
|
|
606
|
+
input = run_input_filters!(input)
|
|
657
607
|
|
|
658
608
|
chat = build_chat
|
|
659
609
|
user_message = extract_message(input)
|
|
@@ -684,7 +634,7 @@ module Phronomy
|
|
|
684
634
|
run_before_completion_hooks!(chat, config)
|
|
685
635
|
|
|
686
636
|
output, usage = _drain_stream(chat, user_message, config, &block)
|
|
687
|
-
|
|
637
|
+
output = run_output_filters!(output)
|
|
688
638
|
|
|
689
639
|
result = {output: output, messages: chat.messages, usage: usage}
|
|
690
640
|
block.call(StreamEvent.new(type: :done, payload: result))
|
|
@@ -863,7 +813,7 @@ module Phronomy
|
|
|
863
813
|
# wrap it in a retry loop without duplicating the LLM interaction logic.
|
|
864
814
|
def invoke_once(input, messages: [], thread_id: nil, config: {})
|
|
865
815
|
trace("agent.invoke", input: input, **_build_caller_meta(config)) do |_span|
|
|
866
|
-
|
|
816
|
+
input = run_input_filters!(input)
|
|
867
817
|
|
|
868
818
|
user_message = extract_message(input)
|
|
869
819
|
chat = build_chat
|
|
@@ -885,7 +835,8 @@ module Phronomy
|
|
|
885
835
|
)
|
|
886
836
|
next [result, usage] if result[:suspended]
|
|
887
837
|
|
|
888
|
-
|
|
838
|
+
filtered_output = run_output_filters!(result[:output])
|
|
839
|
+
result = result.merge(output: filtered_output) unless filtered_output.equal?(result[:output])
|
|
889
840
|
[result, usage]
|
|
890
841
|
end
|
|
891
842
|
end
|
|
@@ -1126,21 +1077,34 @@ module Phronomy
|
|
|
1126
1077
|
end
|
|
1127
1078
|
|
|
1128
1079
|
# Step 3: wrap with approval gate when handler is registered.
|
|
1129
|
-
|
|
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
|
|
1130
1097
|
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
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?
|
|
1101
|
+
|
|
1102
|
+
effective_name4 = resolved.new.name
|
|
1136
1103
|
Class.new(resolved) do
|
|
1137
|
-
tool_name
|
|
1104
|
+
tool_name effective_name4
|
|
1138
1105
|
define_method(:call) do |args|
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
else
|
|
1142
|
-
"Tool execution denied."
|
|
1143
|
-
end
|
|
1106
|
+
result = super(args)
|
|
1107
|
+
result_filters.inject(result) { |val, f| f.call(val, tool_name: name, args: args) }
|
|
1144
1108
|
end
|
|
1145
1109
|
end
|
|
1146
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)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "timeout"
|
|
4
|
+
|
|
5
|
+
module Phronomy
|
|
6
|
+
class Task
|
|
7
|
+
# Backend for Tasks created by {Task#map}.
|
|
8
|
+
#
|
|
9
|
+
# A mapped task's lifecycle is driven entirely by the +on_complete+
|
|
10
|
+
# callback of its source task — it never spawns a thread of its own.
|
|
11
|
+
# +MappedBackend+ transitions the owning task to +:running+ immediately
|
|
12
|
+
# on initialization so that +FSMSession+ treats it as an in-progress
|
|
13
|
+
# async action. Completion (or failure) is triggered externally via
|
|
14
|
+
# {Task#transition!} from the +on_complete+ callback registered by
|
|
15
|
+
# {Task#map}.
|
|
16
|
+
#
|
|
17
|
+
# +await+ and +join+ block until {#unblock} is called, which {Task#map}
|
|
18
|
+
# arranges by registering a second +on_complete+ callback on the *mapped*
|
|
19
|
+
# task itself after the transform callback has been registered.
|
|
20
|
+
#
|
|
21
|
+
# @api private
|
|
22
|
+
class MappedBackend < Backend
|
|
23
|
+
def initialize(task:, &)
|
|
24
|
+
super
|
|
25
|
+
@done_queue = Queue.new
|
|
26
|
+
task.transition!(:running)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Unblocks +await+ / +join+. Called by {Task#map} after the mapped task
|
|
30
|
+
# reaches a terminal state.
|
|
31
|
+
# @api private
|
|
32
|
+
def unblock(value, error)
|
|
33
|
+
@done_queue.push([value, error])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Blocks until the mapped task reaches a terminal state.
|
|
37
|
+
# @return [Object] the mapped value
|
|
38
|
+
# @raise [Exception] if the source task or the map block raised an error
|
|
39
|
+
# @api private
|
|
40
|
+
def await
|
|
41
|
+
value, error = @done_queue.pop
|
|
42
|
+
raise error if error
|
|
43
|
+
|
|
44
|
+
value
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns +false+ — a mapped task has no independent thread to kill.
|
|
48
|
+
# @return [Boolean]
|
|
49
|
+
# @api private
|
|
50
|
+
def alive?
|
|
51
|
+
false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# No-op — mapped tasks carry no independent thread to cancel.
|
|
55
|
+
# @return [self]
|
|
56
|
+
# @api private
|
|
57
|
+
def cancel!
|
|
58
|
+
self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Blocks until the mapped task completes, with an optional timeout.
|
|
62
|
+
# @param limit [Numeric, nil]
|
|
63
|
+
# @return [Object, nil] +nil+ on timeout
|
|
64
|
+
# @api private
|
|
65
|
+
def join(limit = nil)
|
|
66
|
+
if limit.nil?
|
|
67
|
+
await
|
|
68
|
+
else
|
|
69
|
+
begin
|
|
70
|
+
Timeout.timeout(limit) { await }
|
|
71
|
+
rescue Timeout::Error
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/phronomy/task.rb
CHANGED
|
@@ -4,6 +4,7 @@ require_relative "task/backend"
|
|
|
4
4
|
require_relative "task/thread_backend"
|
|
5
5
|
require_relative "task/immediate_backend"
|
|
6
6
|
require_relative "task/fiber_backend"
|
|
7
|
+
require_relative "task/mapped_backend"
|
|
7
8
|
|
|
8
9
|
module Phronomy
|
|
9
10
|
# A single unit of concurrent work.
|
|
@@ -195,6 +196,58 @@ module Phronomy
|
|
|
195
196
|
self
|
|
196
197
|
end
|
|
197
198
|
|
|
199
|
+
# Returns a new {Task} whose completed value is the result of applying
|
|
200
|
+
# +block+ to this task's completed value.
|
|
201
|
+
#
|
|
202
|
+
# If this task fails or is cancelled, the mapped task also fails/is
|
|
203
|
+
# cancelled with the same error. The block is never called in error cases.
|
|
204
|
+
#
|
|
205
|
+
# The primary use-case is transforming an agent result into a
|
|
206
|
+
# {WorkflowContext} so that a Workflow entry action can return a Task
|
|
207
|
+
# whose value is picked up by {Workflow::FSMSession} via the existing
|
|
208
|
+
# +:action_completed+ path:
|
|
209
|
+
#
|
|
210
|
+
# @example Returning agent output into a Workflow state field
|
|
211
|
+
# entry :translate, ->(ctx) {
|
|
212
|
+
# TranslationAgent.new.invoke_async(ctx.query).map do |result|
|
|
213
|
+
# ctx.merge(answer: result[:output]) # returns WorkflowContext
|
|
214
|
+
# end
|
|
215
|
+
# }
|
|
216
|
+
#
|
|
217
|
+
# @yield [value] the completed value of this task
|
|
218
|
+
# @yieldreturn [Object] the value for the mapped task
|
|
219
|
+
# @return [Task] a new task that completes when this task does
|
|
220
|
+
# @api public
|
|
221
|
+
def map(&block)
|
|
222
|
+
# MappedBackend drives the task lifecycle entirely via on_complete;
|
|
223
|
+
# it never spawns a thread of its own.
|
|
224
|
+
mapped = self.class.spawn(
|
|
225
|
+
name: "#{@name}-mapped",
|
|
226
|
+
parent: @parent,
|
|
227
|
+
backend_class: MappedBackend
|
|
228
|
+
) {}
|
|
229
|
+
|
|
230
|
+
on_complete do |value, error|
|
|
231
|
+
mapped_value = nil
|
|
232
|
+
mapped_error = error
|
|
233
|
+
unless error
|
|
234
|
+
begin
|
|
235
|
+
mapped_value = block.call(value)
|
|
236
|
+
rescue => e
|
|
237
|
+
mapped_error = e
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
if mapped_error
|
|
241
|
+
mapped.transition!(:failed, error: mapped_error)
|
|
242
|
+
else
|
|
243
|
+
mapped.transition!(:completed, value: mapped_value)
|
|
244
|
+
end
|
|
245
|
+
# Unblock mapped.await / mapped.join after the terminal transition.
|
|
246
|
+
mapped.backend.unblock(mapped_value, mapped_error)
|
|
247
|
+
end
|
|
248
|
+
mapped
|
|
249
|
+
end
|
|
250
|
+
|
|
198
251
|
# Returns +true+ once the task has finished (success, error, or cancellation).
|
|
199
252
|
# @return [Boolean]
|
|
200
253
|
# @api private
|
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
|
|
|
@@ -123,7 +126,8 @@ module Phronomy
|
|
|
123
126
|
# Raised when a {Phronomy::WorkflowContext} field is mutated from a thread
|
|
124
127
|
# that does not own the context (i.e. not the EventLoop dispatch thread).
|
|
125
128
|
# Only raised in EventLoop mode. Use +context.merge(...)+ to produce a new
|
|
126
|
-
# context, or deliver updates as +:
|
|
129
|
+
# context, or deliver updates as +:action_completed+ event payloads
|
|
130
|
+
# via {Agent::Base#invoke_async} + {Task#map}.
|
|
127
131
|
class WorkflowContextOwnershipError < Error; end
|
|
128
132
|
|
|
129
133
|
class << self
|
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
|
|
@@ -189,6 +187,7 @@ files:
|
|
|
189
187
|
- lib/phronomy/task/backend.rb
|
|
190
188
|
- lib/phronomy/task/fiber_backend.rb
|
|
191
189
|
- lib/phronomy/task/immediate_backend.rb
|
|
190
|
+
- lib/phronomy/task/mapped_backend.rb
|
|
192
191
|
- lib/phronomy/task/thread_backend.rb
|
|
193
192
|
- lib/phronomy/task_group.rb
|
|
194
193
|
- lib/phronomy/testing.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"
|