+------------------+ WebSocket +------------------+
| | <--------------------> | |
| React Frontend | HTTP REST | FastAPI Backend |
| (Zustand Store)| <--------------------> | (uvicorn) |
| | | |
+------------------+ +--------+---------+
|
+------------------+------------------+
| | |
+------+------+ +------+------+ +------+------+
| LangGraph | | Jupyter | | SQLite |
| Agent | | Kernel | | Database |
| Pipeline | | (subprocess | | |
| | | isolated) | | |
+------+------+ +------+------+ +-------------+
| |
+------+------+ |
| LLM API | execute_code()
| (OpenAI / | |
| Anthropic)| v
+-------------+ Python execution
(pandas, sklearn,
matplotlib, etc.)
The core pipeline is a LangGraph StateGraph compiled without a checkpointer. Session state is managed by our own session_manager (not LangGraph's MemorySaver) to avoid astream_events replaying all prior events on each call.
START
|
v
entry_router (zero-cost, no LLM call)
|
+-- Chat mode --> general_agent --> tools --> general_agent --> END
| |
| +-- <stage> directive in autonomous mode
| |
| v
| start_pipeline (extract problem_statement, init stage_history)
| |
+-- Pipeline mode -----> orchestrator
|
v
orchestrator_should_continue
| |
v v
route END (complete or >10 calls)
|
v
[preprocessing | eda | feature_engineering | modeling | evaluation]
|
v
should_continue_agent
| |
v v
tools hungry_review --> orchestrator (loop)
|
v
route_after_tools --> back to calling agent
Zero-cost routing with no LLM call:
- Chat mode: Always routes to the general agent. The user controls every action.
- Autonomous mode: Routes to the orchestrator if mid-pipeline (current stage is a pipeline stage), otherwise to the general agent.
Decides which pipeline stage to run next based on <stage> directives in its LLM response. Key behaviors:
- Forward navigation: Follows the default order: preprocessing > eda > feature_engineering > modeling > evaluation > complete
- Backward navigation: Can route to any earlier stage when evidence warrants it (poor metrics, data issues discovered later). Unmarking stages from the target onward in the ledger.
- Iterative improvement loop: After evaluation, performance thresholds trigger automatic revisits:
- Good (>85%): Route to complete
- Moderate (65-85%): Try hyperparameter tuning, then different features
- Poor (<65%): Route back to feature engineering, then preprocessing
- Max 2 backward iterations per stage
- "Now I Know Better" restart: After completing the full pipeline, the orchestrator can propose restarting with insights gained. This triggers a HITL gate for user approval.
- Stage visit history: Tracks visit counts and sequence for backward navigation awareness
- Completion guards: Multiple early-exit checks prevent the orchestrator from looping after
<stage>complete</stage>
Each pipeline stage has a dedicated agent with a specialized system prompt. All agents share:
BASE_INSTRUCTIONS: Output format rules (thinking/decision tags), error handling, quantitative output requirements- Dataset info and problem statement injection
- Execution state from the ledger (variables, stage summaries, findings from previous stages)
- Error loop detection with recovery guidance
Stage prompts (in agent/prompts/): preprocessing.py, eda.py, feature.py, modeling.py (A/B debate specialists), evaluation.py
Handles all non-pipeline interaction:
- In Chat mode: Runs code, answers questions, explores data. Never triggers the pipeline.
- In Autonomous mode: Understands data thoroughly, asks clarifying questions about the target variable and problem type, then outputs
<problem_statement>and<stage>to trigger the pipeline via thestart_pipelinenode.
A review-only agent (no tools) that fires between pipeline stages and the orchestrator. Key design:
- Zero-cost pass-through for early stages (preprocessing, eda) — no LLM call
- Active review for
feature_engineering,modeling, andevaluation - Cross-references stage findings from previous stages to catch missed opportunities (e.g., EDA found recommended interactions but Feature Engineering didn't create them)
- Adds tagged review feedback (
[HUNGRY AGENT REVIEW]) to the message stream; the orchestrator can act on it - If the review LLM call fails, the pipeline continues uninterrupted
A LangGraph ToolNode that executes the execute_code tool. After execution, route_after_tools routes back to the calling agent (tracked via current_stage in state).
HITL gates pause the agent pipeline and wait for user input via WebSocket:
- The orchestrator reaches a stage transition in autonomous mode
check_hitl_gate()emits anhitl_requestevent with question and optionswait_for_hitl()blocks on anasyncio.Event(up to 10 minutes)- The frontend shows the HITL prompt; the user responds via
hitl_responseWebSocket message set_hitl_response()stores the response and triggers the event- The orchestrator reads the response: approve (continue), modify (redo with feedback), or reject (redo stage)
| Gate | Triggered After | Question |
|---|---|---|
preprocessing_complete |
Preprocessing | Ready to explore with EDA? |
eda_complete |
EDA | Proceed with feature engineering? |
feature_engineering_complete |
Feature Engineering | Ready to train models? |
modeling_complete |
Modeling | Proceed to evaluation? |
FastAPI server <-- mp.Queue (pipe) --> _kernel_worker (child process)
| ZMQ
v
IPython kernel (grandchild)
The KernelManager and ZMQ client run in a child process so that libzmq C-level crashes only kill the child, not the FastAPI server. Communication uses multiprocessing.Queue (pipe), not ZMQ, between the server and the executor subprocess.
If the child crashes, the server detects it and spawns a new one. The ExecutionLedger replays variable creation code to recover state.
Counted on raw messages before windowing:
- 2 consecutive errors: Recovery guidance injected into the system prompt ("try a fundamentally DIFFERENT approach")
- 3 consecutive errors: Force return to orchestrator with failure summary
- Kernel restarts: Tracked separately. 2 consecutive kernel restarts stop retrying.
- Kernel fatal errors: Immediate stop, suggest new session
Prevents token explosion on long conversations:
- Triggers when message count exceeds 30
- Keeps first 2 messages (original context) + last 20 messages (recent work)
- Inserts a compact summary of dropped messages in between
- Cut point adjusted to never split tool_call/ToolMessage pairs
- After windowing,
_sanitize_messages()fixes orphaned tool calls/results
Tracks execution state outside the kernel:
- ArtifactRecord: variable name, type, stage, creation code, shape
- Stage summaries: Compact text summaries of completed stages
- Completed stages: Set of stages that have been completed
- Stage findings: Structured findings extracted from agent output (see below)
When agents output <stage_findings> tags, the content is extracted and stored in the ledger. These findings are:
- Injected into subsequent agent system prompts (so later stages know what earlier stages discovered)
- Cross-referenced by the Hungry Agent during quality reviews
- Persisted via
ledger_jsonin SQLite
Every agent receives the current execution state in its system prompt:
- Known variables with types and shapes
- Stage summaries from completed stages
- Findings from previous stages (for pipeline agents only)
- Entry router always sends to the general agent
- General agent's
<stage>directives are ignored (no pipeline trigger) - User manually asks for EDA, model training, etc.
- Mode is set from the frontend toggle and sent via the WS
modefield
- Entry router sends to orchestrator if mid-pipeline
- General agent first understands data, asks clarifying questions, then triggers pipeline
- HITL gates active at stage transitions
- Full pipeline automation with human oversight
Holds per-session state:
executor: Jupyter kernel referencegraph: Compiled LangGraph instanceledger: ExecutionLedgermessage_history: Full LangChain message listcurrent_stage,stage_history,problem_statementagent_mode: "chat" or "autonomous"read_only: True when the session's CSV is no longer on disk_emitter: EventEmitter reference for HITL access in graph nodes
- Per-session
asyncio.Lockprevents concurrent agent runs - If a user sends a message while the agent is processing, they get an error message instead of a race condition
When a session is restored from SQLite but its CSV file is not found on disk:
- Session is marked
read_only = True - A lightweight agent (
run_readonly_agent) handles conversation — no tools, no code execution - The agent can discuss previous findings, explain decisions, and suggest next steps
- If the user wants active analysis, they're told to re-upload their dataset
- Sessions persisted to SQLite after each agent run
- Ledger serialized as JSON in the
ledger_jsoncolumn - On backend restart, sessions restored from SQLite with glob-based CSV file lookup
SQLite with WAL mode, context manager with retry:
| Table | Columns | Purpose |
|---|---|---|
sessions |
id, name, file_name, dataset_info, stage, ledger_json, timestamps | Session metadata |
messages |
session_id, role, parts_json, timestamp | Conversation history |
experiments |
session_id, model_name, metrics_json, stage | Model training results |
Real-time streaming of agent events:
- Heartbeat: 30-second ping to keep connections alive during long agent runs
- Concurrency guard: Rejects new messages if the agent is already processing
- Stage reset on reconnect: Prevents auto-resuming a pipeline when switching sessions
- Agent task lifecycle: Background
asyncio.Taskfor agent runs, cancelled on disconnect - HITL coordination:
_hitl_eventsdict maps session IDs toasyncio.Event+ response data
| State | Purpose |
|---|---|
settings |
LLM provider, API key, mode, persona (persisted to localStorage) |
sessions |
Sidebar session list |
sessionId, fileName, datasetInfo |
Current session info |
currentStage, stageHistory |
Pipeline progress |
messages |
Completed message list |
streamingParts, isStreaming |
In-progress agent response |
hitlPending |
Active HITL prompt |
wsStatus |
WebSocket connection state |
- Single connection per session with connection guard (skip if already CONNECTING or OPEN)
- Automatic reconnection on unexpected disconnect (3-second delay)
- Falls back to legacy HTTP chat if WebSocket connection fails
User drops CSV
--> POST /api/upload
--> Validate (extension, size, parseable)
--> Save to disk: {UPLOAD_DIR}/{session_id}_{filename}
--> Create session (new kernel)
--> Load dataset: df = pd.read_csv(...)
--> Build dataset_info summary
--> Persist to SQLite
--> Return session_id to frontend
User types message without session
--> POST /api/sessions (create empty session)
--> WebSocket connects to /ws/{session_id}
--> General agent handles conversation (no kernel, no dataset)
User sends message via WebSocket
--> Save message to DB
--> Build/reuse LangGraph graph
--> Stream events via astream_events(version="v2")
--> entry_router decides: general or orchestrator
--> agent generates reasoning + code
--> execute_code tool runs in subprocess-isolated kernel
--> introspect variables, update ledger
--> emit events (thinking, code, plots, tables)
--> hungry_review checks quality (for late stages)
--> orchestrator routes to next stage or END
--> Save agent response to DB
--> Persist session (including ledger)
Code execution fails (ZMQ error)
--> Kernel restart via restart_kernel()
--> Dataset (df) auto-reloaded
--> KERNEL_RESTART signal returned to tool layer
--> Tool replays artifacts from ledger
--> Reports recovered/lost variables to agent
--> Agent adapts with full awareness of state