Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 87 additions & 30 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
## What this is

Multi-agent workplace automation: email triage, meeting management, task
prioritization, follow-up tracking, wellness monitoring, and reporting — six
ReAct-pattern agents orchestrated through LangGraph, with vector memory,
a governance layer, and cost tracking.
prioritization, follow-up tracking, wellness monitoring, and reporting —
six specialized subagents orchestrated by a single `deepagents`-based agent
(built on LangGraph), with vector memory, a governance layer, and cost
tracking.

The governance + cost-tracking layer is the differentiator. Most multi-agent
demos skip exactly that part.
Expand All @@ -15,26 +16,40 @@ Mail and calendar are **live Microsoft Graph data**, not mock JSON. See

## Stack

Python · LangGraph 1.x (Redis-backed checkpointing) · Microsoft Graph (mail,
calendar) · Generic OpenAI-API-compatible LLM gateway · ChromaDB (vector
memory) · FastAPI · Next.js
Python · LangGraph 1.x + `deepagents` (Redis-backed checkpointing) ·
Microsoft Graph (mail, calendar) · Generic OpenAI-API-compatible LLM gateway
(bridged to `deepagents` via `langchain-openai`) · ChromaDB (vector memory) ·
FastAPI · Next.js

`streamlit` and `nicegui` were pinned in requirements.txt as a supposed
"second UI stack" but were never imported anywhere in the codebase --
removed. Next.js (`frontend/`, 9 pages) is the only real UI and is required.

## Layout

- `agents/` — the six specialized agents (11 modules)
- `orchestration/` — LangGraph workflows and routing (12 modules, including
`checkpointer.py`, the shared Redis checkpointer factory)
- `agents/` — the six domain agents (`email_agent.py`, `meeting_agent.py`,
`tasks_agent.py`, `followup_agent.py`, `wellness_agent.py`,
`reporting_agent.py` — single-shot LLM-call classes used directly by
simpler `backend/routes_ai.py` endpoints), plus `tools.py` (the tool
registry + `ToolExecutor` the deep agent's tools wrap), `llm_model.py`
(the `SETTINGS["llm"]` -> `BaseChatModel` bridge), `autonomous_inbox.py`
(the background email-processing loop), `smart_chat.py` (the chat
adapter), `schemas.py`, `prompts.py`
- `orchestration/` — `deep_agent.py` (the `deepagents`-based agent: six
subagents, `interrupt_on` approval config, memory write-back tool),
`checkpointer.py` (the shared Redis checkpointer factory),
`chat_workflow.py` (multi-turn conversation layer on top of the deep
agent), `proactive_scheduler.py` (scheduled briefings/wellness/deadline
checks)
- `backend/` — FastAPI server and routes (21 modules)
- `frontend/` — Next.js dashboard. **Has its own static mock data**
(`frontend/public/data/*.json`) and its own "uses mock data as fallback"
behavior, untouched by this pass — see "What's still open" below.
- `memory/` — vector store + episodic memory
- `frontend/` — Next.js dashboard, backed entirely by real backend data (no
static mock data or fallback JSON remains)
- `memory/` — vector store + episodic memory. Recall is exposed to agents
via tools; write-back happens through the deep agent's `record_episode`
tool (email/meeting subagents)
- `governance/` — policy enforcement, audit logging, cost management, the LLM
gateway
gateway, the human-in-the-loop approval queue (now driven by LangGraph's
`interrupt()` via `deepagents`' `interrupt_on`, not ad hoc flags)
- `repos/` — `data_repo.py` (mail/calendar via Graph + Opspilot's own
generated-state JSON), `graph_auth.py`, `graph_client.py`
- `data/` — Opspilot's own generated state (tasks, follow-ups, reports,
Expand All @@ -47,9 +62,10 @@ removed. Next.js (`frontend/`, 9 pages) is the only real UI and is required.
only you can do this)
- `scripts/graph_login.py` — run this once to complete the Graph device-code
login
- `ARCHITECTURE.md`, `CODEBASE_INDEX.md`, `super_graph.md` — good existing docs;
read `ARCHITECTURE.md` first
- `print_graph.py` — renders the LangGraph topology
- `ARCHITECTURE.md`, `CODEBASE_INDEX.md` — existing docs; read
`ARCHITECTURE.md` first (predates the deepagents migration, so its
orchestration-layer description is stale)
- `print_graph.py` — renders the deep agent's graph topology

## Running

Expand Down Expand Up @@ -199,18 +215,55 @@ themselves when no Redis is reachable on 6379). `.github/workflows/ci.yml`
runs the suite on every push/PR — no live Graph or Redis credentials needed;
Graph failure paths are tested via stubs, not live calls.

## ✅ deepagents migration — done

The premise going in ("six hand-rolled ReAct agents to swap for deepagents")
didn't match reality: there wasn't one ReAct pattern, there were three
parallel execution styles (six single-shot LLM-call classes, a barely-used
ReAct loop that degraded to a hardcoded heuristic, and the real production
path -- a hand-rolled LangGraph state machine), plus real duplication (two
`EmailAgent` classes, two independent "process an email" pipelines,
`super_graph.py`'s email/meeting routing was stubbed and never actually
called those subgraphs) and three inconsistent definitions of "does this
action need approval." Delivered as three PRs:

1. **Dead code removal**: `agents/react_agent.py`, `orchestration/
email_graph.py`, `orchestration/meeting_graph.py`, the unused
`ApprovalPolicy` class, a broken dead convenience wrapper -- all deleted,
not migrated.
2. **Introduce deepagents, additively**: `agents/llm_model.py` bridges
`SETTINGS["llm"]` to the `BaseChatModel` `deepagents` needs via
`langchain-openai`'s `ChatOpenAI` (works against any OpenAI-compatible
`base_url`). `orchestration/deep_agent.py` wraps `create_deep_agent()`
with six subagents, tools generically wrapped from the existing
`agents/tools.py` registry (delegating to the same `ToolExecutor`, not
reimplemented), and `interrupt_on` as the single reconciled approval
source of truth.
3. **Cutover**: chat (`orchestration/chat_workflow.py`), autonomous email
processing (`agents/autonomous_inbox.py`), `/agent/process-email`
(replacing `backend/worker.py`'s separate naive pipeline, now deleted),
and governance approve/reject (`backend/routes_governance.py`, which now
resumes the paused agent thread via `Command(resume=...)` instead of
re-implementing the write) all point at the new agent. The previously
dead-end memory recall (`recall_memory_context` was never wired into the
old compiled graph, and nothing ever called its write-back methods) is
closed via a `record_episode` tool on the email/meeting subagents.
`orchestration/super_graph.py`, `autonomous_graph.py`, the four other
subgraph files, and `common_state.py` (confirmed unused -- every subgraph
rolled its own local state instead) were deleted once the new path was
verified.

`ApprovalQueue`'s pending-actions storage/audit trail (`governance/
approval.py`) is unchanged and still the human-facing "what's pending" list
-- only the enforcement mechanism moved from ad hoc flags to LangGraph's
real `interrupt()`.

## What's still open

- **`SmartChatAgent` is unimplemented.** Building it is a real feature-design
task (no spec exists), not something to reverse-engineer.
- **The frontend has its own static mock data**
(`frontend/public/data/*.json`) and its own "mock data as fallback" comment
in `frontend/.env.example` — untouched by this pass. Fixing that is Next.js/
TypeScript work, a separate chunk from the backend changes here.
- **deepagents migration** — discussed, not started. The six hand-rolled
ReAct agents (`agents/react_agent.py`) are a strong candidate to replace
with deepagents' subagent harness, now that the underlying LangGraph/Redis/
Graph-API/LLM-gateway foundation is solid. That's the natural next phase.
Nothing from the original punch list. `SmartChatAgent`, the frontend mock
data, and the deepagents migration are all done; LICENSE and "two UI stacks"
were resolved (see below). If you're picking this back up, look for newer
gaps rather than assuming this list is current.

Resolved, not open: LICENSE already exists (MIT, root of repo) -- the earlier
note that it was missing was wrong. "Two UI stacks" resolved to one real
Expand All @@ -219,6 +272,10 @@ stack (Next.js) plus two unused pip packages, not a decision to make.
## If you're scaling this further

The foundation (deps, real data, gateway, checkpointing, boot verification,
tests, CI) is done. Order from here: deepagents migration for the six agents →
`SmartChatAgent` (if the conversational feature is still wanted) → drop the
second UI → frontend off its own static mock data → LICENSE.
tests, CI, the deepagents-based agent, real approval enforcement) is done.
Good next candidates: give the deep agent a `response_format`/structured
output where callers need one (the chat layer currently reports `None`/`[]`
for confidence/reasoning-trace rather than fabricating them, since
`deepagents` doesn't expose those natively); async-native Graph calls
instead of the `_run_sync()` thread-hop bridge; multi-user/multi-tenant
memory scoping if this ever serves more than one mailbox at a time.
189 changes: 92 additions & 97 deletions agents/autonomous_inbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

from repos.data_repo import DataRepo
from governance.approval import get_approval_queue
from orchestration.autonomous_graph import process_email_with_graph
from orchestration.deep_agent import create_opspilot_agent


# ============================================================
Expand Down Expand Up @@ -208,7 +208,7 @@ def _process_single_email(self, email: Dict[str, Any]) -> None:
))

try:
self._process_with_langgraph(email)
self._process_with_deep_agent(email)

self.state.processed_count += 1

Expand All @@ -230,87 +230,63 @@ def _process_single_email(self, email: Dict[str, Any]) -> None:

self.state.current_email_id = None

def _process_with_langgraph(self, email: Dict[str, Any]) -> None:
"""Process email using LangGraph workflow"""
def _process_with_deep_agent(self, email: Dict[str, Any]) -> None:
"""Process email using the deepagents-based agent (replaces the
hand-rolled classify/gather/plan/execute/check LangGraph loop in
orchestration/autonomous_graph.py -- deepagents' own agent loop
already does gather -> act -> observe internally, and interrupt_on
replaces that graph's ad hoc pending_approvals state)."""
email_id = email.get("email_id")

# Run the graph
final_state = None
for event in process_email_with_graph(email, self.user_email):
# Each event is a dict with node name as key
for node_name, node_state in event.items():
thought = node_state.get("current_thought", "")
status = node_state.get("status", "")

# Map to event types
if node_name == "classify":
event_type = "thinking"
elif node_name == "gather_context":
event_type = "action"
elif node_name == "plan_actions":
event_type = "thinking"
elif node_name == "execute_action":
event_type = "action"
else:
event_type = "observation"

if thought:
self._emit_event(AgentEvent(
event_type=event_type,
email_id=email_id,
content=thought,
metadata={
"node": node_name,
"status": status,
"iteration": node_state.get("iteration", 0)
}
))

# Check for pending approvals
pending = node_state.get("pending_approvals", [])
for pa in pending:
if pa.get("status") == "pending":
# Queue for approval
approval_queue = get_approval_queue()
approval_queue.add_pending_action(
action_type=pa.get("action_type"),
payload=pa.get("payload", {}),
reason=pa.get("reason", "Agent recommended action"),
source_email_id=email_id,
agent_reasoning=pa.get("description", "")
)

self._emit_event(AgentEvent(
event_type="approval_needed",
email_id=email_id,
content=f"⏸️ Action queued for approval: {pa.get('action_type')}",
metadata={"action": pa}
))

final_state = node_state

# Mark email as processed
if final_state:
executed = final_state.get("executed_actions", [])
category = final_state.get("email_analysis", {}).get("category", "unknown")

self.repo.mark_email_processed(
email_id,
actions_taken=executed,
category=category
)

thread_id = f"autonomous_email_{email_id}"

agent = create_opspilot_agent(repo=self.repo, user_email=self.user_email, gateway=self.gateway)
config = {"configurable": {"thread_id": thread_id}}
instruction = (
f"Use the email subagent to process email {email_id} "
f"(subject: {email.get('subject', '')!r}, from: {email.get('from_email', '')!r}). "
"Read it, take whatever action (if any) is warranted, and mark it "
"processed when you're done."
)
result = agent.invoke(
{"messages": [{"role": "user", "content": instruction}]},
config=config,
version="v2",
)

for message in result.value.get("messages", []):
content = getattr(message, "content", "") or ""
if content:
self._emit_event(AgentEvent(
event_type="observation",
email_id=email_id,
content=content,
))

for interrupt in result.interrupts or []:
for action in interrupt.value.get("action_requests", []):
approval_queue = get_approval_queue()
approval_queue.add_pending_action(
action_type=action.get("name", ""),
payload=action.get("args", {}),
reason="Agent recommended action while processing this email",
source_email_id=email_id,
agent_reasoning="",
session_id=thread_id,
)
self._emit_event(AgentEvent(
event_type="approval_needed",
email_id=email_id,
content=f"⏸️ Action queued for approval: {action.get('name')}",
metadata={"action": action},
))

if not result.interrupts:
self._emit_event(AgentEvent(
event_type="completed",
email_id=email_id,
content=final_state.get("final_summary", "Processing complete"),
metadata={
"actions_executed": len(executed),
"category": category,
"pending_approvals": len(final_state.get("pending_approvals", []))
}
content="Processing complete",
))

def process_email_now(self, email_id: str) -> Generator[AgentEvent, None, None]:
"""
Process a specific email immediately (for manual triggering).
Expand All @@ -337,26 +313,45 @@ def process_email_now(self, email_id: str) -> Generator[AgentEvent, None, None]:
"subject": email.get("subject")
}
)

# Process with LangGraph
for event in process_email_with_graph(email, self.user_email):
for node_name, node_state in event.items():
thought = node_state.get("current_thought", "")
if thought:
yield AgentEvent(
event_type="thinking" if "Think" in thought else "observation",
email_id=email_id,
content=thought,
metadata={"node": node_name}
)

# Mark processed
self.repo.mark_email_processed(
email_id,
actions_taken=["manual_processed"],
category="processed"

thread_id = f"autonomous_email_{email_id}"
agent = create_opspilot_agent(repo=self.repo, user_email=self.user_email, gateway=self.gateway)
config = {"configurable": {"thread_id": thread_id}}
instruction = (
f"Use the email subagent to process email {email_id} "
f"(subject: {email.get('subject', '')!r}, from: {email.get('from_email', '')!r}). "
"Read it, take whatever action (if any) is warranted, and mark it "
"processed when you're done."
)

result = agent.invoke(
{"messages": [{"role": "user", "content": instruction}]},
config=config,
version="v2",
)

for message in result.value.get("messages", []):
content = getattr(message, "content", "") or ""
if content:
yield AgentEvent(event_type="observation", email_id=email_id, content=content)

for interrupt in result.interrupts or []:
for action in interrupt.value.get("action_requests", []):
approval_queue = get_approval_queue()
approval_queue.add_pending_action(
action_type=action.get("name", ""),
payload=action.get("args", {}),
reason="Agent recommended action while processing this email",
source_email_id=email_id,
agent_reasoning="",
session_id=thread_id,
)
yield AgentEvent(
event_type="approval_needed",
email_id=email_id,
content=f"⏸️ Action queued for approval: {action.get('name')}",
metadata={"action": action},
)

yield AgentEvent(
event_type="completed",
email_id=email_id,
Expand Down
Loading
Loading