Skip to content

Architecture

PhronesisML is built as a graph of cooperating agents — each stage of the ML pipeline is a discrete, testable unit operating on a shared typed state. This page explains how the system is structured and why.


High-Level Architecture

┌─────────────────────────────────────────────────────┐
│                    SDK Layer                         │
│  Phronesis (OOP) │ Simple API │ run_pipeline()      │
└──────────────────────┬──────────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────────┐
│               Workflow Layer (LangGraph)             │
│  StateGraph(WorkflowState) → compiled graph         │
│  Nodes: agent closures │ Router: conditional edges  │
└──────────────────────┬──────────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────────┐
│                 Agent Layer                          │
│  Upload │ ETL │ Validation │ EDA │ Target │ FE │ …  │
│  Each agent: Protocol-based, stateless, returns     │
│  AgentResult(success, data, error)                  │
└──────────────────────┬──────────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────────┐
│              Engine Layer (ABC)                      │
│  PandasEngine │ PolarsEngine │ SparkEngine           │
│  Uniform interface: read, collect, filter, join …   │
└──────────────────────┬──────────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────────┐
│                 Data Layer                           │
│  File loaders │ Validators │ Transformers │ Profiles │
└─────────────────────────────────────────────────────┘

The Agent Pattern

Protocol-Based Design

Agents do not inherit from a base class. They satisfy the BaseAgent protocol via duck typing (structural subtyping). This avoids diamond-inheritance issues and keeps agents independently testable.

from typing import Protocol, Any

class BaseAgent(Protocol):
    """Agents must satisfy this protocol via duck typing."""

    def run(self, state: dict[str, Any]) -> AgentResult: ...

AgentResult — Standardized Returns

Every agent returns an AgentResult with:

Field Type Description
success bool Whether the agent completed successfully
data dict Stage-specific output (merged into WorkflowState)
error Exception \| None The exception if success=False
error_type str \| None Exception class name
error_message str \| None Human-readable error message
error_context dict \| None Structured context for debugging

Key rule: Agents MUST NOT raise exceptions for expected failures. They return AgentResult(success=False, ...) and the workflow handles it gracefully.

Stateless Agents

All mutable state lives in WorkflowState. Agents are pure functions — given the same state, they produce the same result. This makes them independently testable.


LangGraph Integration

PhronesisML uses LangGraph to model the pipeline as a stateful graph.

StateGraph with Pydantic State

from langgraph.graph import StateGraph
from phronesisml.workflow.state import WorkflowState

graph = StateGraph(WorkflowState)  # Pydantic BaseModel, not TypedDict

Using Pydantic for state provides runtime validation and serialization — every field is typed and validated at each stage.

Linear Pipeline Topology

The canonical pipeline order:

upload → etl → validation → eda → target_detection →
feature_engineering → model_selection → evaluation →
explainability → reporting → storage

Conditional Routing

Each stage has a routing function that returns "proceed" or "__end__". If the stage produced its expected output, the pipeline continues. If not, it ends gracefully.

def route_after_validation(state: WorkflowState) -> str:
    """Proceed if validation passed, end if data is invalid."""
    if state.validated_data is not None:
        return "proceed"
    return "__end__"

This "fail-safe" pattern means partial results are always available — if stage 5 fails, you still get results from stages 1–4.

Graph Caching

Compiled graphs are cached by (agent_names, stages, agent_ids). Repeated calls with the same topology skip LangGraph compilation — providing a 20–40% speedup on subsequent runs.


Engine Abstraction

BaseEngine ABC

All engines inherit from BaseEngine(ABC) with @abstractmethod for every core operation. This forces subclassing and ensures every new engine is audited against the full interface.

class BaseEngine(ABC):
    @abstractmethod
    def read(self, path, **kwargs) -> Any: ...

    @abstractmethod
    def collect(self, df) -> pd.DataFrame: ...

    @abstractmethod
    def filter(self, df, conditions) -> Any: ...

    # ... 10+ more abstract methods

Three-Tier Engine Selection

The engine is auto-selected based on data size:

Data Size Engine Rationale
< 2 MB Pandas Fast startup, familiar API
2–500 MB Polars 2-10x faster, lower memory
> 500 MB Spark Distributed computing

Users can override with config.engine.preferred = "polars".

Universal Pandas Output

engine.collect() always returns a pd.DataFrame regardless of backend. This means downstream code (profiling, evaluation, reporting) works uniformly — it doesn't need to know which engine is active.


Composition Root

Agent instantiation happens in exactly two places:

  1. _compose_agents() in __init__.py — for the Simple API
  2. _make_agents() in sdk.py — for the OOP API

All other code depends on abstractions. This "composition root" pattern makes it easy to swap implementations or add dependency injection.


Data Flow

User provides file path
        │
        ▼
    Upload Agent ──────► raw_data (DataFrame)
        │
        ▼
    ETL Agent ──────────► processed_data (DataFrame)
        │
        ▼
    Validation Agent ───► validated_data (DataFrame)
        │
        ▼
    EDA Agent ──────────► data_profile (dict)
        │
        ▼
    Target Detection ───► target_column, task_type, confidence
        │
        ▼
    Feature Engineering ► features (DataFrame), feature_names
        │
        ▼
    Model Selection ────► trained_model, best_pipeline
        │
        ▼
    Evaluation ─────────► evaluation_report (metrics dict)
        │
        ▼
    Explainability ─────► explanation (SHAP values)
        │
        ▼
    Reporting ──────────► final_report (Markdown string)
        │
        ▼
    Storage ────────────► artifacts saved to disk

Each arrow represents a state transition. The WorkflowState accumulates results — by the end, it contains everything from the raw data to the final report.


Error Handling Hierarchy

PhronesisError (base)
├── ConfigurationError
├── DataError
│   ├── DataLoadError
│   ├── DataTransformError
│   └── DataValidationError
├── EngineError
│   └── EngineSelectionError
├── WorkflowError
└── AgentError
    └── AgentNotImplementedError

The system is designed for graceful degradation:

  • If an agent raises AgentNotImplementedError, the node logs a warning and returns {}.
  • If MLflow is unreachable, training continues without logging.
  • Partial results are always available even if later stages fail.

Key Source Files

File Purpose
phronesisml/sdk.py OOP API — Phronesis class
phronesisml/simple.py Simple API — one-liner functions
phronesisml/__init__.py Public exports, composition root
phronesisml/configs/settings.py PhronesisConfig and sub-configs
phronesisml/agents/base.py BaseAgent protocol, AgentResult
phronesisml/workflow/graph.py LangGraph StateGraph construction
phronesisml/workflow/state.py WorkflowState Pydantic model
phronesisml/workflow/router.py Conditional routing functions
phronesisml/engines/base_engine.py BaseEngine ABC
phronesisml/engines/engine_selector.py Auto engine selection
phronesisml/exceptions.py Exception hierarchy