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
97 changes: 59 additions & 38 deletions apps/ambient-inventory-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
A stage-ready demo of an inventory monitoring assistant for a regional specialty
coffee roaster. It shows one compressed monitoring cycle:

1. The presenter presses **Start demo**.
1. The presenter presses **Run sweep** on the inventory dashboard.
2. An agent sweeps the catalogue over MongoDB Remote MCP.
3. It finds a shared component below its reorder point and files an alert.
4. The owner asks the agent about suppliers, timing and quantities.
Expand All @@ -12,20 +12,27 @@ coffee roaster. It shows one compressed monitoring cycle:

## Architecture

**One agent, three jobs.** Same model, same MCP tool set, same MongoDB
**One agent, two jobs.** Same model, same MCP tool set, same MongoDB
connection; what differs is the prompt and when it runs.

| Job | When | File |
|---|---|---|
| **Monitor** | On Enter | `investigator.py` — sweeps, diagnoses, files the alert |
| **Monitor** | On **Run sweep** | `investigator.py` — sweeps, diagnoses, files the alert |
| **Assistant** | Owner asks | `agent.py` — answers from the database, streaming |
| **Order clerk** | Owner approves | `order_agent.py` — records the purchase order |

`graph.py` only schedules the monitoring run. Every judgement in the alert — which
component, which supplier, how many, how urgent — is the agent's, reached by
querying MongoDB over Remote MCP. There is no rule-based alternative: if the agent
cannot complete, no alert is raised and the failure is surfaced in the feed rather
than papered over with a fabricated one.
Ordering is not a third agent. When the owner decides in the chat, the assistant
writes the purchase order itself with `insert-many`; when they press the approve
button instead, the app writes it with the driver and labels it as such.

Both jobs are LangGraph agents: `create_agent` compiles a ReAct graph, the app
streams from it with `astream`, and its memory is checkpointed to MongoDB with
`langgraph-checkpoint-mongodb`. `monitor.py` only schedules the sweep — it holds no
orchestration of its own, because the sweep is one agent doing the whole job.

Every judgement in the alert — which component, which supplier, how many, how
urgent — is the agent's, reached by querying MongoDB over Remote MCP. There is no
rule-based alternative: if the agent cannot complete, no alert is raised and the
failure is surfaced in the feed rather than papered over with a fabricated one.

```
Browser ──SSE──► FastAPI ──► LangGraph ReAct agent ──► Claude (Bedrock)
Expand All @@ -44,11 +51,24 @@ The driver is used only for things no model should decide: seeding, the activity
log, the chat transcript, session state, and the UI's state snapshot.

MCP data tools require a `connectionId` from `remote-atlas-connect`. The app
performs that handshake at startup — and again when **Enter** is pressed, since a
laptop left open on a podium may be holding an expired token — then injects
performs that handshake at startup — and again when **Run sweep** is pressed, since
the service-account token lasts an hour and a laptop left open on a podium may be
holding an expired one — then injects
`connectionId` and `database` into every tool call so the model cannot target the
wrong cluster.

### How the agent authenticates

**`RemoteMCPAuth.service_account_token()` in `app/mcp_client.py` is the whole
story** — one function, and the code that actually runs.

The agent holds no database username or password. It holds an Atlas **service
account** (client id + secret, the same credential a CI job would use), sends it as
HTTP Basic in a standard OAuth 2.0 `client_credentials` grant, and gets back a
bearer token valid for one hour. That token authorizes every MCP tool call. Access
is exactly what the service account is granted in the Atlas project — revoke it
there and every tool call stops, with no redeploy.

The agent is scoped to eight of the ~41 tools, which keeps it out of Atlas
administration (`drop-database`, `create-cluster`, …) and keeps tool selection
fast:
Expand Down Expand Up @@ -172,12 +192,12 @@ the first chat message from stalling on stage. Check it worked:
curl -s localhost:8008/api/health | python -m json.tool
```

`mcp.ready` must be `true`. Then open `http://localhost:8008/` and press **Start
demo** when you begin.
`mcp.ready` must be `true`. Then open `http://localhost:8008/` and press **Run
sweep** when you begin.

Reseeding is a pre-flight step, not something the page does, which is why
`setup_demo.sh` does it before starting the server. Neither loading the page nor
pressing **Start demo** reseeds — `/api/demo/start` mints a new `session_id`, which
pressing **Run sweep** reseeds — `/api/demo/start` mints a new `session_id`, which
leaves the previous run's alert and transcript behind but does *not* clear
`purchase_orders`. That matters, because a leftover order for the shared component
makes the next sweep decide no alert is needed, so run `./setup_demo.sh` again
Expand All @@ -195,11 +215,13 @@ Start it, then open the app and walk away:
open http://localhost:8008/
```

1. A **Start demo** screen appears and nothing runs behind it, so the laptop can
sit on the podium indefinitely. There is no URL parameter to remember.
2. Press **Start demo** when you begin. It re-mints the Remote MCP session — a
long-idle laptop may be holding an expired OAuth token — and starts the sweep.
About 6 seconds, then the dashboard.
1. The app opens straight into the inventory portal, showing the shop's real data
with the agent idle. Nothing runs until you press play, so the laptop can sit on
the podium indefinitely and there is no URL parameter to remember.
2. Press **Run sweep** in the Agent activity panel when you begin. It re-mints the
service-account token, rebinds the cluster connection, and starts the sweep —
about 6 seconds, narrated on the button itself. The control then becomes a live
**Monitoring** indicator.
3. The Agent activity panel fills as it works: `Agent · plan`, then a live stream
of `Agent · MCP` queries. This is the part to narrate; the badge pulses when
the agent files its diagnosis, roughly 45 seconds in.
Expand All @@ -209,7 +231,7 @@ open http://localhost:8008/
answer streams in.
6. Approve the order. The agent writes the purchase order over MCP (~17s).

Timing after **Enter**: ~6s to reconnect MCP, then the alert lands 40–65s later.
Timing after **Run sweep**: ~6s to reconnect MCP, then the alert lands 40–65s later.
The activity feed populates throughout, so the wait is the demo rather than dead
air. A chat answer takes 15–45s depending on how many queries the model runs.

Expand All @@ -226,7 +248,7 @@ restarting.
| `suppliers` | Lead times, reliability, `unit_costs`, `minimum_order` |
| `purchase_orders` | Seeded inbound POs plus agent-submitted orders |
| `alerts` | Inbox alerts with the decision inputs that produced them |
| `session_history` | One timeline per session: owner questions, agent answers, and every tool call the agent made (TTL 24h) |
| `session_history` | One timeline per session: owner questions, agent answers, and every tool call the agent made |
| `checkpoints`, `checkpoint_writes` | LangGraph short-term memory, one thread per sweep |
| `demo_sessions` | Session state and the seed marker |

Expand All @@ -242,8 +264,9 @@ Notes on the schema, following MongoDB's modeling guidance:
session-scoped list views, plus a unique `(session_id, dedupe_key)` backing the
alert upsert.
- **Approval is idempotent in the database.** A unique partial index on
`{alert_id}` where `status: "submitted"` means a double-click cannot place two
supplier orders.
`{alert_id}` where `status: "ordered"` means a double-click cannot place two
supplier orders — and neither can the agent, so it does not spend a query
checking before it writes.
- **Seeded documents carry `session_id: "seed"`** so session queries stay
indexable equality matches instead of `{$exists: false}`.
- **`$jsonSchema` validators** are attached at `warn` level: drift shows up in the
Expand All @@ -263,17 +286,13 @@ monitoring run — not on the browser session or the alert:
- The alert stores its `sweep_id`, so opening it resumes that thread. Nothing is
held in the browser.

The order write runs nested inside a chat turn, so it deliberately has no
checkpointer — sharing the conversation's thread would resume it mid-tool-call.
Writing one document needs no memory.

### Overriding the recommendation

If the owner wants a different supplier, the agent records that as an `override`
beside the untouched `recommendation`. The alert keeps showing what the agent
advised — that is the record — and the override shows what will actually be
ordered. Nothing is written to `purchase_orders` until the owner says to place it,
either in words or with the button.
If the owner wants a different supplier, the agent orders from theirs. The alert
goes on showing the original `recommendation` — that is the record of what the
agent advised — and the purchase order is the record of what was actually bought.
Nothing reaches `purchase_orders` until the owner says to place it, either in words
or with the button.

This is also why a phone would work in a real deployment: state is in MongoDB, so
a push notification deep-linking to an alert would resume the same conversation.
Expand All @@ -287,10 +306,12 @@ otherwise.
This is a demo, and a few things would change in production:

- The monitor runs once per browser session via an in-process `asyncio` task. A
real deployment would run the same graph on a schedule (cron, worker, or
managed LangGraph) independent of anyone viewing the page, and the alert would
also go out as a push notification.
real deployment would run the same sweep on a schedule (cron, worker, or managed
LangGraph) independent of anyone viewing the page — `InventoryMonitor.run()` is
the entry point that would be called — and the alert would also go out as a push
notification.
- Purchase orders are simulated — no supplier API is called.
- Alert detection is deliberately rule-based. Keeping the LLM out of detection
is a reasonable production choice too: thresholds stay auditable and cheap,
and the model is reserved for explanation and decision support.
- Detection here is the model's own work, which is what makes the demo, but a
production system would likely compute the threshold crossing deterministically
and reserve the model for diagnosis and decision support: cheaper per sweep, and
the trigger stays auditable.
Loading
Loading