LLM-in-the-loop rule based expert system
Project description
theow
þēow - Old English for "servant" or "bondman."
Theow is a rule engine designed for auto-failover. It captures failure context automatically, semantically matches it against existing rules, and executes bound actions for recovery. Rules are deterministic. Same context, same fix, every time.
When no rule matches, Theow's internal agent uses an LLM to investigate and write a new rule. This agent is leashed and programmatic by design. It restricts the LLM to a defined set of tools. You specify what the LLM can do, nothing more, providing full but secure automation. As rules accumulate, LLM calls decrease. Since failure modes are finite, they may reach zero over time.
TL;DR
- Mark functions for recovery with
@theow.mark()- failures trigger the rule engine - Rules match context and execute deterministic fixes
- No match? LLM explores, writes a new rule, rule handles it next time
- You define the tools the LLM can use - nothing more, nothing less
- Rules accumulate, LLM calls decrease, may hit zero over time
Example
A simplified pipeline with Theow-managed failure recovery:
from theow import Theow
pipeline_agent = Theow(
theow_dir="./.theow",
name="pipeline",
llm="gemini/gemini-2.0-flash",
)
@pipeline_agent.mark(
context_from=lambda task, exc: {
"stage": "process",
"task_id": task.id,
"error": str(exc),
},
explorable=True,
)
def process(task):
# do work that might fail
...
def run_pipeline(task):
prepare(task)
process(task) # failures here trigger theow
post_process(task)
complete(task)
When process() fails, Theow captures the context, matches it against rules, and attempts recovery. If no rule exists and exploration is enabled, the LLM investigates and writes one. This pattern extrapolates to any pipeline with a sequential flow.
Components
Initialization
from theow import Theow
pipeline_agent = Theow(
theow_dir="./.theow", # rules, actions, vector store
name="pipeline", # for logging
llm="gemini/gemini-2.0-flash", # primary LLM (provider/model)
llm_secondary="anthropic/claude-sonnet-4-20250514", # fallback LLM
session_limit=20, # max explorations per session
max_tool_calls=30, # max tool calls per conversation
max_tokens=8192, # max output tokens
)
Initialization creates the theow directory structure, loads existing rules and actions, and sets up the vector store. The name parameter identifies this instance in logs, making it easy to trace which agent handled which failure in a multi-agent setup.
.theow/
├── rules/ # Rule YAML files
├── actions/ # Action Python files
├── prompts/ # Prompt templates
└── chroma/ # Vector DB (auto-managed)
Marker
The @mark decorator wraps functions for automatic recovery. When the function raises an exception, Theow captures context, matches against rules, and attempts recovery.
@pipeline_agent.mark(
context_from=lambda task, exc: { # (args, exception) -> dict
"stage": "process",
"error": str(exc),
"task_id": task.id,
},
max_retries=3, # max rules to try
rules=["specific_rule"], # try these first, by name
tags=["pipeline"], # then try rules with these tags
fallback=True, # fall back to vector search
explorable=True, # allow LLM exploration
collection="pipeline", # chroma collection for indexing
)
def process(task):
...
Theow adds tracebacks automatically. The context_from callable builds the context dict that rules match against. Include whatever information is relevant for diagnosing failures: error messages, identifiers, state. This dict can be extended with any keys your rules need.
Rules
Rules are YAML files in .theow/rules/ that define conditions and responses. The when block matches against the context dict populated by context_from in the marker.
name: config_missing # unique identifier
description: Required config file not found # used for vector search
tags: [config, setup] # for filtering
when: # all facts must match
- fact: error
contains: "FileNotFoundError" # substring match
- fact: error
regex: 'config/(?P<filename>\w+\.yaml)' # regex with named captures
examples: # improves vector search recall
- "FileNotFoundError: config/database.yaml not found"
then: # actions to execute
- action: create_default_config
params:
filename: "{filename}" # captured from regex
Fact operators: equals (exact), contains (substring), regex (with named captures). Multiple actions can be chained in the then block and run sequentially.
Rule Engine
The engine does not brute-force match facts against all available rules. Instead, it uses semantic search to find rules similar to the failure context, then validates facts against those candidates. This keeps matching fast as the rule set grows.
1. Explicit Filtering
If the @mark decorator specifies rules=["name"] or tags=["tag"], the engine filters the rule set to only those rules. Rules specified by name are tried first, then rules matching the tags. Each candidate rule's when block is validated against the context. If a rule matches, its actions are executed and the engine stops. This allows you to scope recovery to known, trusted rules for specific failure points.
2. Semantic Search
When no explicit match is found, the engine uses vector search via ChromaDB:
- Metadata pre-filter: Facts with
equalsconstraints filter candidates to rules where exact-match facts align with the context - Vector similarity: Rule descriptions and examples are embedded and searched against the error context. This lets the engine apply existing solutions to similar known problems, even if the exact error message differs
- Fact validation: All
whenconditions must match. Similarity alone is not enough to trigger an action. The engine enforces that facts match to avoid running fixes for the wrong problem
3. Execution
The engine retrieves up to N candidates (N = max_retries) and tries each in order. If an action fails, the next candidate is tried.
Actions
Actions are Python functions that rules execute. When a rule matches, its then block specifies which actions to run. Parameters come from regex captures in the rule or from the context built by context_from in the marker.
Actions live in .theow/actions/ and are auto-discovered on startup.
from theow import action
@action("create_default_config")
def create_default_config(filename: str) -> dict:
"""Create a default config file."""
Path(f"config/{filename}").write_text("# default\n")
return {"status": "ok", "created": filename}
The action name in the decorator must match the action referenced in the rule's then block.
Exploration
When no rule matches and explorable=True in the marker, Theow brings in the LLM. The configured LLM receives the failure context (traceback, exception, and everything from context_from) and investigates using the registered tools.
Exploration requires the THEOW_EXPLORE environment variable to be set. This lets you programmatically enable or disable LLM exploration without changing code. Useful for running with exploration in dev/CI but disabling it in production where you only want deterministic rule matching.
THEOW_EXPLORE=1 python my_script.py
Semantic Rule Search
The LLM has access to an internal search_rules tool to query the vector database for semantically similar rules. This helps in three ways:
- Smarter retries: The LLM can reason about intent and find rules the engine missed because the error message was slightly different but the underlying problem is the same
- Informed rule writing: When writing a new rule, the LLM can look at how similar problems were solved before instead of starting from scratch
- Efficient context: Instead of dumping every rule into the prompt, the LLM searches for what's relevant, keeping context focused
Ephemeral Rules
When the LLM writes a fix, the new rule and action go to .theow/rules/ephemeral/. These are unproven until they actually fix the failure. Ephemeral rules persist across multiple LLM conversations within the same exploration session, so each subsequent attempt knows what was tried and why it failed.
Once an ephemeral rule successfully fixes the issue, it gets promoted to the main rules folder and indexed into the vector database for future use.
Incomplete Rules
If the LLM hits its session budget before finishing, it can tag the rule as incomplete with notes about progress. The next exploration session can pick up where the previous one left off using list_ephemeral_rules and read_ephemeral_rule tools.
LLM Configuration
Set the API key for your provider via environment variable. The agent picks the provider based on the llm parameter format in initialization.
| Provider | Format | Environment Variable |
|---|---|---|
| Gemini | gemini/gemini-2.0-flash |
GEMINI_API_KEY |
| Anthropic | anthropic/claude-sonnet-4-20250514 |
ANTHROPIC_API_KEY |
| Copilot | copilot/gpt-4o |
GITHUB_TOKEN |
Tools
Tools are how you control what the LLM can do during exploration. They define the boundaries of LLM actions, effectively putting the LLM on a leash. The LLM cannot act outside the tools you register.
Theow provides common tools out of the box:
from theow.tools import read_file, write_file, run_command
pipeline_agent.tool()(read_file)
pipeline_agent.tool()(write_file)
pipeline_agent.tool()(run_command)
For tighter control, write custom tools with constraints:
@pipeline_agent.tool()
def read_config(path: str) -> str:
"""Read config files only."""
if not path.endswith((".yaml", ".json")):
raise ValueError("Only config files allowed")
return Path(path).read_text()
@pipeline_agent.tool()
def run_safe_command(cmd: str) -> dict:
"""Run whitelisted commands only."""
allowed = ["ls", "cat", "grep"]
if not any(cmd.startswith(a) for a in allowed):
raise ValueError(f"Command not allowed: {cmd}")
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
return {"stdout": result.stdout, "stderr": result.stderr}
This is the key to secure automation. You define the blast radius. The LLM operates within those boundaries.
LLM Based Actions
Rules can invoke the LLM directly on match instead of running a deterministic action. Useful for failures that need dynamic investigation rather than a fixed fix.
name: investigate_unknown
description: Unknown failure, use LLM to investigate
when:
- fact: error_type
equals: unknown
llm_config:
prompt_template: file://prompts/investigate.md # file path or inline string
tools: [read_file, run_command] # tools the LLM can use
constraints:
max_tool_calls: 20
Unlike exploration, this does not create new rules. The LLM acts directly on the failure each time the rule matches. Requires an API key for the configured provider.
Known Limitations
These are known and planned to be addressed:
- Scale: Rule matching has not been tested at scale with large rule sets
- Action chaining: No intelligent forward or backward chaining. Actions run sequentially in the order defined in the rule
- Multi-agent: No multi-agent routing yet. Each Theow instance operates independently
- Local only: Rules, actions, vector store must be co-located with the process. No remote rule storage or distributed access yet
- Vector store: ChromaDB is the only supported vector store
- Sync only: No async support. Execution is synchronous
- LLM dependency: Exploration quality depends heavily on the LLM's reasoning ability. Stronger models produce better rules
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file theow-0.0.3.tar.gz.
File metadata
- Download URL: theow-0.0.3.tar.gz
- Upload date:
- Size: 1.0 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
672935f78213fba4d47867ddc73aead0e107a378de89b76e201a9bb95355f03d
|
|
| MD5 |
31260bf397b3a2d25539e64af5c0499b
|
|
| BLAKE2b-256 |
dc7f73efc11b1db92535c8747acb7d9af758ab7106ede8687cadcfe6732fcfab
|
Provenance
The following attestation bundles were made for theow-0.0.3.tar.gz:
Publisher:
release.yml on adhityaravi/theow
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
theow-0.0.3.tar.gz -
Subject digest:
672935f78213fba4d47867ddc73aead0e107a378de89b76e201a9bb95355f03d - Sigstore transparency entry: 955895123
- Sigstore integration time:
-
Permalink:
adhityaravi/theow@7d5d100a1d072dc04e14e97869ccfcd75469333a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/adhityaravi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7d5d100a1d072dc04e14e97869ccfcd75469333a -
Trigger Event:
push
-
Statement type:
File details
Details for the file theow-0.0.3-py3-none-any.whl.
File metadata
- Download URL: theow-0.0.3-py3-none-any.whl
- Upload date:
- Size: 49.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
075383ab7e8c3cba1e495ee20a0aeba12b178db14c8795202182c2fba6fd8796
|
|
| MD5 |
f72b53b0816ce7d1d05e7fd58040a227
|
|
| BLAKE2b-256 |
486ba10f83d778fbb84a568cee01439c28df7eb809d461cf893df7ec8cc8def3
|
Provenance
The following attestation bundles were made for theow-0.0.3-py3-none-any.whl:
Publisher:
release.yml on adhityaravi/theow
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
theow-0.0.3-py3-none-any.whl -
Subject digest:
075383ab7e8c3cba1e495ee20a0aeba12b178db14c8795202182c2fba6fd8796 - Sigstore transparency entry: 955895138
- Sigstore integration time:
-
Permalink:
adhityaravi/theow@7d5d100a1d072dc04e14e97869ccfcd75469333a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/adhityaravi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7d5d100a1d072dc04e14e97869ccfcd75469333a -
Trigger Event:
push
-
Statement type: