Extensible Doctor Rules

Ship your own diagnostic and repair rules — praisonai doctor discovers them automatically

Doctor rules are a plugin surface — any package can register a diagnostic-and-repair rule via the praisonai.doctor_contracts entry point and praisonai doctor picks it up alongside the built-in checks.

Diagram

Doctor Rules

apply

🩺 doctor run

📇 registry

🔧 built-in rules

🔌 plugin rules

📋 collect findings

–fix?

✅ RepairPlan + residual

Quick Start

  1. Write a rule

    Implement DoctorContractProtocol: a rule_id, a collect_findings() that returns Findings, and an apply_fix() that returns a repaired config.

    python
    # my_package/rules.py
    from typing import Any, Dict, List
    from praisonaiagents.runtime import Finding
    
    class MyServiceRule:
        @property
        def rule_id(self) -> str:
            return "my_service_endpoint"
    
        def collect_findings(self, config: Dict[str, Any]) -> List[Finding]:
            if config.get("my_service_url", "").startswith("http://"):
                return [Finding(
                    rule_id=self.rule_id,
                    severity="warning",
                    message="my_service_url uses insecure http://",
                    fix_description="Rewrite to https://",
                )]
            return []
    
        def apply_fix(self, config: Dict[str, Any]) -> Dict[str, Any]:
            url = config.get("my_service_url", "")
            if url.startswith("http://"):
                config["my_service_url"] = "https://" + url[len("http://"):]
            return config
  2. Register it via entry point

    Expose the rule under praisonai.doctor_contracts in your pyproject.toml.

    toml
    # pyproject.toml
    [project.entry-points."praisonai.doctor_contracts"]
    my_service = "my_package.rules:MyServiceRule"
  3. Run the doctor

    Your rule now runs alongside the built-ins — collect findings, or preview and apply repairs.

    python
    from praisonaiagents.runtime import collect_findings, plan_fixes
    
    config = {"my_service_url": "http://api.local"}
    
    for finding in collect_findings(config):
        print(f"[{finding.severity}] {finding.rule_id}: {finding.message}")
    
    plan = plan_fixes(config, dry_run=True)   # preview
    print(plan.render_diffs())
    print("residual:", plan.residual_findings)

How It Works

The registry loads built-in rules plus every rule discovered on the praisonai.doctor_contracts entry point, then runs each in isolation — a rule that raises is recorded in refused instead of aborting the whole run.

Diagram
RuleRegistryDoctorUserRuleRegistryDoctorUserloop[each rule, isolated]plan_fixes(config)get_rules() (built-in + plugins)collect_findings(config)findingsapply_fix(config)repaired configRepairPlan (diffs, residual, refused)preview + residual findings

A repair is a three-phase lifecycle: collect findings → apply fix → re-collect residual findings so callers can report “repair left N finding(s)”. In dry_run mode nothing is written; the plan’s config holds the proposed result.


Reference

Finding — one diagnostic result:

FieldTypeDefaultDescription
rule_idstr—The rule that produced this finding
severitystr—"warning", "error", or "info"
messagestr—Human-readable description shown to operators
fix_descriptionstrNoneWhat the repair would do
contextdictNoneExtra structured data (field, value, location)

RepairPlan — the result of plan_fixes():

FieldTypeDefaultDescription
configdict—The repaired (or proposed, in dry-run) config
diffslist[ConfigDiff]—Per-rule before/after changes
backup_pathstrNoneWhere the original was snapshotted (apply mode + backup=True)
residual_findingslist[Finding][]Findings still present after repair
refusedlist[Finding][]Rules that raised and were skipped
appliedboolFalseTrue only when a real change was written

Rule contract (DoctorContractProtocol):

MemberSignaturePurpose
rule_idproperty -> strUnique id; duplicate ids replace the earlier rule
collect_findings(config) -> list[Finding]Analyze config, return findings
apply_fix(config) -> dictReturn a repaired config

Common Patterns

Preview before writing — plan_fixes defaults to dry_run=True:

python
from praisonaiagents.runtime import plan_fixes

plan = plan_fixes(config)
if plan.has_changes:
    print(plan.render_diffs())

Apply with a backup and re-validate:

python
plan = plan_fixes(config, dry_run=False, backup=True)
if plan.residual_findings:
    print(f"repair left {len(plan.residual_findings)} finding(s)")

Register a rule at runtime instead of an entry point:

python
from praisonaiagents.runtime import register_rule
from my_package.rules import MyServiceRule

register_rule(MyServiceRule())

Best Practices

Keep collect_findings fast and side-effect-free

It runs on every doctor invocation. Inspect the config only — no network calls, no writes.

Make apply_fix idempotent and re-checkable

Running the fix twice must produce the same config, and a follow-up collect_findings should return no findings for what you repaired.

Return actionable message strings

message and fix_description show up verbatim in operator output — say what is wrong and what the fix does.

Use a stable rule_id prefix

Prefix ids per package (my_channel_, my_tool_) so operators can filter and duplicate-id replacement is predictable.


Related