NAME
Chorus::Engine - Perl inference engine with AI-assisted pipeline for deterministic compliance checking against normative corpora.
VERSION
2.0.1
DESCRIPTION
Chorus::Engine implements the inference engine layer of the Chorus platform: a classic recognise-act cycle over typed objects (Chorus::Frame).
Each rule declares a working scope (_SCOPE) — a set of frame combinators — and an action (_APPLY) that fires when a matching combination is found. The engine iterates over the cartesian product of active scopes, applies every matching rule, then repeats until no rule fires or _MAX_CYCLES is reached.
Rules can be added programmatically with addrule() or loaded from a compact YAML DSL with loadRules(); see "YAML DSL" below.
In the Chorus 2 pipeline the YAML rules are generated by an AI agent from a normative corpus and then executed by this class deterministically — no LLM in the hot path, reproducible across machines and runs. See Chorus::Engine::AIAgent for the full corpus-to-compliance workflow.
Chorus::Expert chains several specialised engine instances over a shared working memory for multi-agent pipelines.
SYNOPSIS
use Chorus::Engine;
my $agent = Chorus::Engine->new();
$agent->addrule(
_SCOPE => {
a => sub { [ fmatch(slot => 'color') ] }, # dynamic -- re-evaluated each cycle
b => [1, 2, 3], # static array_ref
},
_APPLY => sub {
my %opts = @_; # $opts{a} and $opts{b} hold one combination of _SCOPE values
return unless $opts{a}->color eq 'blue'; # guard: rule does not apply
$opts{a}->set('tagged', 'y');
return 1; # rule fired (something changed)
},
);
$agent->loop();
METHODS
new
Creates a new engine instance. The instance is a Chorus::Frame that inherits from the internal $ENGINE prototype.
my $agent = Chorus::Engine->new();
my $agent = Chorus::Engine->new(_IDENT => 'MyAgent'); # named agent (used in debug output)
addrule
Adds a rule to the engine.
$agent->addrule(
_ID => 'rule-name', # optional -- used for deduplication; duplicates are skipped
_SCOPE => {
x => sub { [ fmatch(slot => 'slot_name') ] }, # dynamic scope
y => \@static_list, # static scope
},
_APPLY => sub {
my %opts = @_;
return unless <condition>; # rule does not apply
# ... effects ...
return 1; # rule fired
},
);
Additional optional slots on the rule frame:
_TERMINAL 'solved' or 'failed' -- auto-terminates the engine when the rule fires.
_PREMISSES Hashref of slot names used as metadata for reorder().
_TRACE If true, logs a line to STDERR each time this rule fires (even without _LOG).
The _APPLY sub receives one combination of _SCOPE values as a hash. It should return a true value when it has made a change, or false/undef when it has not.
Important -- rules with the same _ID in the same agent are deduplicated: the second definition is silently ignored.
loop
Enters the inference loop. Calls applyrules() repeatedly until no rule fires in a full pass, or until the shared BOARD signals SOLVED or FAILED, or until _MAX_CYCLES (default: 10,000) is reached.
$agent->loop();
The current cycle count is available via $agent->{_CYCLE} during execution (starts at 0, incremented after each pass that fires at least one rule).
applyrules
Runs one pass over all rules. For each rule, evaluates _SCOPE to get candidate arrays, generates all combinations, and calls _APPLY for each. Returns a true value if at least one rule fired.
This method is called internally by loop(); you rarely need to call it directly.
cut
Exits the scope-combination loops of the current rule and moves to the next rule in the same agent.
$agent->cut(); # inside _APPLY
last
Exits the rule loop for the current agent and moves to the next agent. Implies cut().
$agent->last(); # inside _APPLY
replay
Restarts the current agent from its first rule. Implies cut().
$agent->replay(); # inside _APPLY
replay_all
Restarts the whole pipeline from the first agent (propagates up to Chorus::Expert::process()). Implies cut().
$agent->replay_all(); # inside _APPLY
solved
Signals successful termination. Sets BOARD->{SOLVED}, which stops all loops.
$agent->solved(); # inside _APPLY
failed
Signals failed termination. Sets BOARD->{FAILED}, which stops all loops.
$agent->failed(); # inside _APPLY
reorder
Re-sorts the rule list using a comparator function, then calls replay(). Useful for dynamically prioritising rules after a domain event.
sub by_interest {
my ($r1, $r2) = @_;
return 1 if $r1->{_PREMISSES}{CAT_NOUN};
return -1 if $r2->{_PREMISSES}{CAT_NOUN};
return 0;
}
$agent->reorder(\&by_interest);
_LOG — rule-firing log
Set _LOG on an agent before calling loadRules() or addrule() to get a trace of every rule that fires during loop().
$agent->set('_LOG', 1); # default handler → STDERR
$agent->set('_LOG', \&my_handler); # custom handler
The custom handler receives ($engine, $rule_id, \%scope_opts):
sub my_handler {
my ($engine, $rule_id, $opts) = @_;
printf "fired: %s cycle=%s\n", $rule_id, $engine->{_CYCLE};
}
The default handler prints one line per firing:
[cycle 1] MyAgent / rule-name fired
scope: x=frame-id
_LOG is checked at fire time — setting it to undef or 0 after addrule() silently disables logging for subsequent cycles.
The callback is the natural integration point for any external logger. Example with Log::Log4perl:
use Log::Log4perl qw(:easy);
Log::Log4perl->easy_init($DEBUG);
$agent->set('_LOG', sub {
my ($engine, $rule_id, $opts) = @_;
DEBUG sprintf "[cycle %s] %s / %s fired",
$engine->{_CYCLE}, $engine->{_IDENT} // '?', $rule_id;
});
Log::Log4perl is not a dependency of Chorus::Engine — the module stays lightweight; logging infrastructure is the caller's responsibility.
To trace a single rule without enabling logging on the whole agent, set _TRACE on that rule (or add TRACE: 1 to the YAML file):
$agent->addrule( _ID => 'my-rule', _TRACE => 1, _SCOPE => ..., _APPLY => ... );
pause
Disables the engine until wakeup() is called. While paused, loop() has no effect. Use this to skip agents that have nothing to do in the current context.
$agent->pause();
wakeup
Re-enables a paused engine.
$agent->wakeup();
loadRules
Loads all *.yml files from a directory in alphabetical order, compiles each one to a Perl addrule() call, and evaluates it.
$agent->loadRules('/path/to/rules/dir');
$agent->loadRules('/path/to/rules/dir', debug => ['rule-name']);
Files are loaded sorted alphabetically; prefix filenames with R01-, R02-... to control order. Multiple calls accumulate rules.
Compilation errors are printed to STDERR with the generated code for inspection.
setScope
$agent->setScope( \%desc )
Compiles one FIND / CHERCHER entry into the Perl string used inside _SCOPE. %desc has two optional keys:
attribut-
The slot name passed to
fmatch(). filtre-
An optional Perl expression (string) evaluated as
grep { ... }before the scope array is passed to_APPLY. The expression receives$_bound to each candidate frame.
Override this slot on a per-agent basis to customise scope compilation:
$agent->set('setScope', sub { ... });
setFilter
$agent->setFilter( $filter_expr | \@filter_exprs )
Compiles one or more filter expressions (from filtre: in YAML) into a grep { ... } string. Multiple expressions are joined with and (implicit AND). Returns an empty string when $filter_expr is false.
setCondition
$agent->setCondition( $condition | \@conditions )
Compiles the CONDITION / CONDITION value into a Perl guard expression (return unless ...). Multiple conditions are joined with or (implicit OR). Returns an empty string when no condition is present.
setException
$agent->setException( $exception | \@exceptions )
Compiles the EXCEPTION / EXCEPTION value into a Perl guard expression (return if ...). Multiple exceptions are joined with or. Returns an empty string when no exception is present.
setEffect
$agent->setEffect( $effect | \@effects )
Compiles the ACTION / EFFET value into the body of _APPLY. A list of effects is joined sequentially (implicit AND/sequence). Returns an empty string when $effect is false.
codeRule
Chorus::Engine::codeRule( $engine, \%yaml_rule, %opts )
Internal function called by loadRules() to compile one parsed YAML hash into the Perl argument string for addrule(). Exposed here for completeness; you rarely need to call it directly.
%opts may contain debug => \@rule_names to print generated code for the named rules to STDOUT.
CUSTOMISATION HOOKS
The following slots are defined on the engine prototype with a default identity implementation (sub { shift }). Override them on a per-agent instance to implement a custom DSL on top of the YAML keys — for example, to wrap every effect expression in a domain-specific decorator.
codeEffect
Called by setEffect() on each effect string extracted from ACTION / EFFET. The default passes the string through unchanged.
$agent->set('codeEffect', sub {
my ($expr) = @_;
return "do { local \$_ = $_; $expr }"; # custom wrapper
});
codeCondition
Called by setCondition() on each condition string from CONDITION. Default: identity.
codeException
Called by setException() on each exception string from EXCEPTION. Default: identity.
codeTest
Called by setFilter() on each filter expression from filtre:. Default: identity.
YAML DSL
Rules can be written in YAML instead of Perl. Each file defines one rule.
Keywords are available in both English and French — use whichever fits your project. When both are present in the same file, the French keyword takes precedence.
English French Meaning
------- ------ -------
RULE REGLE Rule name — becomes _ID (mandatory)
FIND CHERCHER Scope definition — defines _SCOPE (mandatory)
ACTION EFFET Effect body — body of _APPLY (mandatory)
TERMINAL TERMINAL Optional: 'solved' or 'failed'
PREMISES PREMISSES Optional: metadata for reorder()
CONDITION CONDITION Optional: guard — return unless CONDITION
EXCEPTION EXCEPTION Optional: guard — return if EXCEPTION
TRACE TRACE Optional: 1 — log to STDERR each time this rule fires
Example using English keywords:
RULE: rule-name # mandatory -- becomes _ID
TERMINAL: solved # optional -- 'solved' or 'failed'
PREMISES: # optional -- metadata for reorder()
- slot-name
FIND: # mandatory -- defines _SCOPE
var:
attribut: slot-name # fmatch(slot => 'slot-name')
filtre: '$_->score > 0' # optional grep filter applied before _APPLY
CONDITION: '$var->ok' # optional -- return unless CONDITION
EXCEPTION: 'defined $var->r' # optional -- return if EXCEPTION
ACTION: | # mandatory -- body of _APPLY (must return true when fired)
$var->set('result', 42);
1
Example using French keywords (equivalent):
REGLE: rule-name
CHERCHER:
var:
attribut: slot-name
EFFET: |
$var->set('result', 42);
1
Important -- the last instruction of ACTION / EFFET must return a true value when the rule has made a change. If a conditional block may leave nothing modified, return 0 rather than 1:
ACTION: |
if ($var->score > 5) { $var->set('flag', 'KO'); return 1 }
0
The codeEffect, codeCondition, codeException and codeTest slots on the engine frame are called during compilation and default to the identity function. Override them on a per-agent basis to implement a custom DSL on top of the YAML keys.
AUTHOR
Christophe Ivorra
BUGS
Please report bugs via the CPAN request tracker: http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Chorus
SUPPORT
perldoc Chorus::Engine
MetaCPAN -- https://metacpan.org/dist/Chorus
GitHub -- https://github.com/civorra/Chorus
SEE ALSO
LICENSE AND COPYRIGHT
Copyright 2013 Christophe Ivorra.
This program is free software; you can redistribute it and/or modify it under the terms of either: the GNU General Public License as published by the Free Software Foundation; or the Artistic License.
See http://dev.perl.org/licenses/ for more information.