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

SEE ALSO

Chorus::Frame, Chorus::Expert

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.