Version: 8.0
Module: factory.core
Inherits from: ObjData.ObjData
Copyright: Proprietary to TechnoCore Automate
ObjDecisionSwitch is the core decision tree engine for the Axion platform. It provides a database-driven, rule-based decision-making system that evaluates input data against hierarchical decision trees to produce deterministic outcomes.
The class implements the foundational decision tree framework:
Simulation, visualization, export, and SQL generation capabilities are provided by child classes (see Related Modules).
ObjData.ObjData
└── ObjDecisionSwitch (factory.core)
├── ObjDecisionSwitchEdit (factory.core/extend.edit)
├── ObjDecisionSwitchVisual (factory.core/extend.visual)
└── ObjServiceApi (factory.service)
Decision trees are stored in two main tables:
def_decision_tree
Guid: Unique identifierDecisionName: Tree identifierPackage: Package scopeModule: Owning module name (e.g. ObjDecisionSwitch)Version: Version numberDescription: Tree descriptionDataTable: Input data table for simulationsSimDataTable: Optional override of the sim sample sourceDefaultOutcome: Default outcome when no rules match (also used as the live evaluator's fallback so live and bulk SQL agree)simlimit: Sample size for run --sim (defaults to 2000 when NULL)ConstantsTables: Comma-separated lookup tables resolved into _constants_cacheThreeWords, VersionDate, RaciSimulation, RaciProduction, RaciOutcome, RaciDevelopment: governance / audit metadataSampleColumns, PreviousDistribution: feature-level context used by reportsdef_decision_tree_history — append-only audit row written on every import-tree: Guid, DecisionName, Package, Module, Version, ThreeWords, VersionDate, NodeCount, ChangedBy, ChangeType (IMPORT), ChangeNotes, CreatedAt.
def_decision_bulk_cache — generated CASE SQL per (decision, version, table). Invalidated when def_decision_tree.VersionDate is newer than the cache's GeneratedAt. Bypass with run --rebuild.
data_decision_sim_analysis — narrative blocks rendered in the sim email. Rows keyed by (DecisionName, Version, BlockType, SimGuid) with BlockTitle and BlockContent. Block types include summary, outcomes, anomalies (AI), and reconcile (bulk-vs-live diff).
def_decision_treenodes
Guid: Node identifier (N1, N2, O1, O2, START, etc.)DecisionName: Parent tree namePackage: Package scopeVersion: Version numberrank: Execution ordertarget_node: Next node to evaluatecondition_field: Field to evaluatecondition_operator: Comparison operator (=, <, >, <=, >=, !=, IN)condition_value: Value to compare againstterminal_id: Output field name (for outcome nodes)terminal_value: Output value (for outcome nodes)Decision trees are cached in-memory per package:decision_name combination for performance. Cache is stored in the module-level DECISION_TREE_CACHE dictionary.
read(decision_name: str) -> NoneLoads a decision tree from the database into memory.
Example:
tree = ObjDecisionSwitch()
tree.read("credit_scoring_v1")
compute_decision(context: SimulationContext) -> str | NoneEvaluates a single record through the decision tree.
Parameters:
context: Dictionary containing input field valuesReturns:
Example:
tree = ObjDecisionSwitch()
tree.read("credit_scoring_v1")
context = {
"CreditScore": 750,
"Income": 50000,
"DebtRatio": 0.3
}
outcome = tree.compute_decision(context)
print(f"Decision: {outcome}")
print(f"Calculated values: {context}")
evaluate(value1: str | int | float, operator: str, value2: str | int | float) -> boolEvaluates a single condition with flexible type handling.
Supported Operators:
| Operator | Aliases | Description | Example | With Constants |
|---|---|---|---|---|
= |
EQ, MATCH |
Equality | Status = Active |
Status = active_status |
< |
LT |
Less than | Age < 18 |
Age < min_age |
> |
GT |
Greater than | Income > 50000 |
Income > threshold |
<= |
LE, LTE |
Less or equal | Dpd <= 30 |
Dpd <= max_dpd |
>= |
GE, GTE |
Greater or equal | Score >= 600 |
Score >= min_score |
!= |
<> |
Not equal | Status != Closed |
Status != blocked_status |
@ |
IN |
Value in list | day @ (1,3,5,7) |
day @ (weekdays) |
~= |
RANGE |
Anchor + offset | dom ~= (15,3) |
dom ~= (debit_date,friday_adjust) |
>< |
BETWEEN |
Absolute bounds | age >< (18,65) |
age >< (min_age,max_age) |
* |
ELSE, DEFAULT |
Catch-all | * |
Constants Resolution:
All operators resolve condition_value from the
constants cache at runtime. Constants are key/value pairs
stored in database tables referenced by
def_decision_tree.ConstantsTables. Anywhere you would
put a literal value, you can put a constants key instead.
Example constants table (data_sms_strategy_constants):
| ConstantKey | ConstantValue |
|---|---|
| friday_adjust | 3 |
| min_score | 600 |
| weekdays | 2,3,4,5,6 |
| debit_date | 15 |
This allows changing business rules (thresholds, offsets,
lists) by updating a constants table row — without
modifying the decision tree definition.
For list operators (@ / IN), constants that contain
comma-separated values are expanded. For example,
day @ (weekdays) where weekdays = 2,3,4,5,6 expands
to checking against (2,3,4,5,6). Mixed literal and
constant values work: day @ (weekdays,7).
Range vs Between:
~= / RANGE: format (anchor, offset) — the offsetdom ~= (15, 3) → checks [15, 18]dom ~= (15, -3) → checks [12, 15]dom ~= (debit_date, friday_adjust) → resolves>< / BETWEEN: format (low, high) — both areage >< (18, 65) → checks [18, 65]age >< (min_age, max_age) → resolves bothType Handling:
clean_value(value: str | int | float) -> strRemoves quotes and whitespace from values.
is_valid_float(s: str | int | float) -> boolChecks if value can be converted to float.
get_matched_value(context, field: str) -> strExtracts field value from context with flexible matching:
The normalisation ensures that field names from the
decision tree CSV match data table columns regardless of
naming convention differences. For example, a tree field
DO_Failed_Reason will match a table column
DO Failed reason because both normalise to
dofailedreason. This applies to all three lookup
sources: constants cache, run_context, and the input
context dictionary.
get_sim_key(input_table: str = "") -> strIdentifies simulation key column in input table.
get_prompts(decision_name: str) -> listReturns LLM prompt definitions for decision tree analysis.
from ObjDecisionSwitch import ObjDecisionSwitch
# Initialize and load tree
tree = ObjDecisionSwitch()
tree.read("loan_approval")
# Evaluate single record
context = {
"CreditScore": 720,
"Income": 75000,
"LoanAmount": 250000,
"EmploymentYears": 5
}
outcome = tree.compute_decision(context)
print(f"Loan Decision: {outcome}")
print(f"Decision Details: {context}")
Decision trees require entries in ObjDecisionSwitch.yaml:
queries:
read_decision_tree: |
SELECT * FROM def_decision_tree
WHERE package = '{package}' AND DecisionName = '{decision_name}'
read_decision_tree_nodes: |
SELECT * FROM def_decision_treenodes
WHERE package = '{package}' AND DecisionName = '{decision_name}'
ORDER BY COALESCE(Rank, 100), Guid
Common errors and solutions:
read() before operationsLocation: factory.core/extend.edit/ObjDecisionSwitchEdit.py
Inherits from: ObjDecisionSwitch
Provides CRUD, simulation, and export operations:
simulate() — Batch simulation with multiprocessingverify_outcomes() — Validate results against expected outcomescreate_simulation_data_table() — Generate input table schemaLocation: factory.core/extend.visual/ObjDecisionSwitchVisual.py
Inherits from: ObjDecisionSwitch
Provides visualization, reporting, and interchange formats:
diagram() — Mermaid flowchart generationdiagram_terminal() — Terminal-based tree renderingdiagram_sankey() — Outcome distribution chartsgenerate_sql_statement() — SQL CASE statement generationsave_pmml() / load_pmml() — PMML import/exportsave_csv() / load_csv() — CSV import/exportsave_excel() — Excel exportsend_email() — Email reportingcompute_outcome_distribution() — Outcome statisticsLocation: factory.service/package.core/ObjServiceDecisionSwitch.py
Service wrapper providing API endpoints, CLI commands, and workflow integration for decision tree operations.
All CLI commands use typer. Run from the project
root. --help is rich-formatted with examples per
command, and help-detail prints a long-form guide.
# Import a decision tree from CSV
python factory.core/ObjDecisionSwitch.py \
import-tree TREE_NAME path/to/file.csv \
--three-words "short label" \
--change-notes "v2 threshold update" \
--diff # show added/removed/changed rules vs prior version
# --strict (default) aborts on silent SQL failures during the
# def_decision_tree row INSERT/UPDATE; pass --no-strict to fall back
# to legacy swallow-and-continue behaviour.
# Bulk run (default): single SQL CASE UPDATE on the whole DataTable
python factory.core/ObjDecisionSwitch.py \
run TREE_NAME
python factory.core/ObjDecisionSwitch.py \
run TREE_NAME --rebuild # ignore cached CASE SQL
# Sim run: sample-based, AI analysis, and bulk-vs-live reconcile
python factory.core/ObjDecisionSwitch.py \
run TREE_NAME --sim
# --sim also runs the bulk CASE on the full DataTable and writes a
# 'reconcile' block to data_decision_sim_analysis.
# Email the latest sim report (rendered via SIMULATION_OVERVIEW)
python factory.core/ObjDecisionSwitch.py \
email TREE_NAME --to a@x.com[,b@x.com] \
[--subject "..."] [--from notify@technocore.co.za]
# List trees / set source table
python factory.core/ObjDecisionSwitch.py list
python factory.core/ObjDecisionSwitch.py list --package CORE
python factory.core/ObjDecisionSwitch.py \
set-table TREE_NAME my_data_table
# Long-form guide
python factory.core/ObjDecisionSwitch.py help-detail
| Command | Purpose |
|---|---|
import-tree |
Import CSV as a new versioned tree (--diff, --strict/--no-strict) |
run |
Bulk SQL CASE UPDATE; --sim switches to sample sim + AI + reconcile |
email |
Render the latest sim report and send via WebMail (SMTP from config.yaml smtp:) |
list |
Show all trees with version, label, DataTable, and package |
set-table |
Update def_decision_tree.DataTable for the named tree |
help-detail |
Print the full guide (workflow, storage layout, gotchas) |
run --sim exercises both code paths (live row-by-row evaluator and the bulk SQL CASE) and persists a reconcile block summarising matches / mismatches / mismatch rate / sample mismatched rows under data_decision_sim_analysis. The block is rendered automatically in the sim email. Live and bulk are kept in sync by having compute_decision fall back to the tree's DefaultOutcome (e.g. UNMATCHED) at "no rule matched" exit paths — without that fallback, gap rules silently produce None in live but the configured default in bulk.
The AI prompts (summary, outcomes, anomalies) run concurrently via a ThreadPoolExecutor; each worker only calls the model HTTP endpoint and returns its text. All sql_execute writes happen on the main thread because the DB connection is not thread-safe.
The model is read from config.yaml:
homechoice:
ai:
sim_analysis_model: mcp:ollama:qwen3:8b # default fallback
Override per-package by editing the value; falls back to the bundled hardcoded default.
ObjDecisionSwitchVisual.build_outcome_paths_html()
traces backwards from every O-node to START and
renders the condition chain as an HTML table.
This is included in the send_email() report
(see extend.visual/ObjDecisionSwitchVisual.md).
factory.core/extend.edit/ObjDecisionSwitchEdit.md — Edit module documentationfactory.core/extend.visual/ObjDecisionSwitchVisual.md — Visual module documentationfactory.service/package.core/ObjServiceDecisionSwitch.md — CLI command documentationObjData.md — Database connectivity patternsObjTypes.py — Type definitions used throughout8.x (2026-04-28):
import-tree gained --diff (read-only added/removed/changed report) and --strict/--no-strict (default strict — aborts on silent def_decision_tree row INSERT/UPDATE failures, e.g. unknown columns).ObjDecisionSwitchVisual.get_three_words(decision_name) — the import flow was calling it without its required argument and silently corrupting the version sequence.--sim now runs the bulk CASE alongside the live evaluator and persists a reconcile block with match/mismatch counts.compute_decision falls back to the tree's DefaultOutcome at "no rule matched" exits so live and bulk SQL agree.config.yaml ai.sim_analysis_model (default mcp:ollama:qwen3:8b).email (sends the latest sim report through SIMULATION_OVERVIEW email template + WebMail) and help-detail.ObjDecisionSwitch.yaml queries: reconcile_select_bulk_outcomes, reconcile_insert_block.8.0: Core extraction and refactoring
_clear_output_tables and _move_to_done to support CSV import via the service layerself.get_time() with time.time() for simulation latency tracking