A working starting point for the AI Agent Development Bootcamp. Using your own data (or generating mock data from your schema and samples), add your own tools to build a functioning enterprise AI agent on MongoDB Atlas as the single data layer for retrieval, structured queries, and agent memory.
Out of the box it gives you:
- A tool-calling agent (LangGraph): a chat model that loops over registered tools until it has an answer, with a conditional tool loop and a clean place to add your own tools.
- Three patterns to choose from: document-retrieval (RAG), structured-query, and hybrid.
- Retrieval leg (
knowledge_base_search): Atlas Vector Search with Voyage embeddings and reranking, returning passages with source citations. - Query leg (
structured_query): turns a natural-language question into a read-only MongoDB aggregation, runs it, and explains what it ran. - Hybrid fusion (
assess): one grounded, cited answer over retrieved text and structured records together. - Memory: short-term per conversation (a MongoDB checkpointer) and long-term per user (a MongoDB store), with a
remembertool. - A tool registry to plug in your own two to three business tools, plus a CLI demo and a
verifyscript.
You bring the data and the use case. The scaffold also ships a small synthetic sample scenario (bank operations) and example tools: a runnable reference to copy from, and a fallback if your team wants something to build against before your own data is ready. That sample is not the goal of the repo. One habit worth carrying over from it: keep synthetic data internally consistent so your agent's answers stay verifiable.
These are proof-of-concept agents. Clarity and speed to a working demo are the priorities; production hardening (including query security) is out of scope and handled downstream. See context.md for the full architecture and CLAUDE.md for the invariants.
Pick the one that matches your use case; your data and tools follow from it.
- Document-retrieval (RAG) with
knowledge_base_search: Atlas Vector Search over a Voyage-embedded knowledge base, reranked with Voyage, answers cited. For questions whose answers live in text. - Structured-query with
structured_query: turns a question into a read-only MongoDB aggregation over your structured collection, runs it under a result cap and timeout, and returns the records plus a plain-language explanation. For factual and analytical questions over records. - Hybrid with
assess(the flagship): looks up a structured record and retrieves the relevant text, then fuses both into one grounded, cited judgment. The pattern MongoDB is uniquely built for as a single data layer.
A tool-calling LangGraph: a model node that can call any registered tool, looping until it produces a final answer, with a MongoDBSaver checkpointer persisting state per thread_id.
START -> model -> (tool calls?) --yes--> tools -> model -> ...
--no---> END
src/
index.ts CLI demo
config.ts env loading + zod validation (single source of config)
credentials.ts mints AWS + Voyage keys from the Lambda into memory
llm/model.ts getChatModel(): the ONLY provider SDK import
db/client.ts shared Mongo client singleton
memory/checkpointer.ts MongoDBSaver (short-term, per thread_id)
memory/store.ts MongoDBStore (long-term, per user, reference discipline)
retrieval/ Voyage embeddings + reranker, vector store, retriever tool
query/ collection schema aid + structured_query tool
hybrid/hybridTool.ts assess (fusion)
tools/registry.ts register your tools here (+ exampleBusinessTool.ts)
tools/memoryTools.ts remember (writes long-term memory)
agent/ per-pattern prompts + the LangGraph graph
patterns.ts selects tools + prompt per pattern
data/
sample/kb/ synthetic policy/runbook markdown
sample/activity_events.ts deterministic, self-asserting event generator
mock-input/ fill-in schema + samples; a prompt expands them into the generator
load.ts chunk + embed + index the KB; insert events; ensure indexes
scripts/verify.ts the three-checkpoint acceptance checks
Requires Node 20+ and a MongoDB Atlas cluster with Vector Search.
npm install
mv env.example .env.example # see note below
cp .env.example .env # then fill in MONGODB_URI and set PASSKEYNote: the environment template ships as
env.example. It should be named.env.example; the repo tooling that generated it blocked writing.env*dotfiles, hence the rename step. The name is the only difference.
Then:
npm run typecheck # clean
npm run load # seed KB + events, build the vector index (waits for it)
npm run verify # run the three checkpointsRun the CLI demo against the shipped sample data:
npm run dev -- --pattern rag --thread demo --user analyst_1 "What is the dual-control threshold?"
npm run dev -- --pattern structured --thread demo --user analyst_1 "Who made the largest transfer this month?"
npm run dev -- --pattern hybrid --thread demo --user analyst_1 "Is event evt_0051 consistent with dual control?"Re-run with the same --thread to see short-term memory resume the conversation. Re-run with the same --user but a new --thread to see long-term memory carry over (see Memory below). evt_0051 is the seeded dual-control violation (one operator both initiates and approves a high-value transfer); the ids are deterministic, so this holds after npm run load.
The app never bakes secrets into the image or writes them to disk. On startup, src/credentials.ts calls the DevDay credentials Lambda (get_token task only) using your PASSKEY and mints, into process.env for that process only:
- AWS keys (
provider: "aws") → Bedrock SigV4 auth for the chat model. - Voyage key (
provider: "voyageai") → embeddings and reranking.
The Lambda's completion task is deliberately not used. If a provider's keys are already present in the environment, that provider is skipped, so you can bypass the passkey entirely by exporting your own AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / VOYAGE_API_KEY. Passkeys roll on a ~3-day window; a 401 means "get a fresh passkey." See .assets/LAMBDA_USAGE.md for the Lambda contract.
The chat model is reached through AWS Bedrock via ChatBedrockConverse, implemented only in src/llm/model.ts. No other module imports a model-provider SDK.
Every variable is documented in env.example. The ones you must set: MONGODB_URI and PASSKEY (unless you supply your own keys). Notable defaults:
| Variable | Default | Purpose |
|---|---|---|
MONGODB_DB |
bootcamp |
DB for collections + checkpoints |
KB_COLLECTION / EVENTS_COLLECTION |
kb_documents / activity_events |
demo collections |
VECTOR_INDEX_NAME |
vector_index |
Atlas Vector Search index |
BEDROCK_REGION |
us-west-2 |
Bedrock region (try us-east-1 if invoke fails) |
BEDROCK_MODEL_ID |
global.anthropic.claude-sonnet-4-6 |
chat model |
VOYAGE_EMBEDDING_MODEL / VOYAGE_EMBEDDING_DIMENSIONS |
voyage-3-large / 1024 |
embeddings; dims MUST match the index |
VOYAGE_RERANK_MODEL |
rerank-2.5 |
reranking |
RETRIEVAL_TOP_K / RERANK_TOP_K |
10 / 4 |
candidates fetched / kept |
QUERY_RESULT_CAP / QUERY_MAX_TIME_MS |
50 / 5000 |
query-tool ergonomics (not security) |
REJECT_WRITE_STAGES |
false |
optional $out/$merge guard, off by default |
MEMORY_COLLECTION |
agent_memory |
long-term memory store collection |
MEMORY_TTL_SECONDS |
0 |
long-term memory expiry (0 = never; e.g. 2592000 for 30 days) |
This is the shape of the included sample scenario, not a fixed schema. Your data replaces it; carry over the field conventions and the internal-consistency discipline, not the specific collections.
kb_documents (KB): each row is a chunk of a policy/runbook markdown doc, with text, a Voyage embedding, and source + section metadata for citation. A vectorSearch index (vector_index) over embedding with numDimensions matching the Voyage model.
activity_events (structured): one synthetic operational event per doc.
| Field | Type | Notes |
|---|---|---|
_id |
string | e.g. evt_0001 |
userId / userName |
string | the actor |
action |
string | LOGIN, BALANCE_QUERY, TRANSFER_INITIATED, TRANSFER_APPROVED, USER_CREATED, USER_MODIFIED |
amount |
number | minor units (cents); non-zero only for transfers |
channel |
string | WEB, MOBILE, API, BRANCH |
status |
string | SUCCESS, FAILED, PENDING |
timestamp |
Date | BSON date (UTC) |
The generator is deterministic and asserts its own consistency before returning: the labeled "largest transfer this month" really is the largest, and per-user totals sum to the global total. It also seeds a dual-control violation (one operator initiates and approves a high-value transfer) for the hybrid demo. Indexes on userId, action, timestamp.
The scaffold ships both memory mechanisms LangGraph provides, backed by MongoDB:
- Short-term (checkpointer,
src/memory/checkpointer.ts). AMongoDBSaverpersists the full message state of one conversation thread, keyed bythread_id. Re-invoking with the samethread_idresumes that conversation, even across process runs. This is what gives a single conversation continuity. - Long-term (store,
src/memory/store.ts). AMongoDBStorepersists durable facts about a user, keyed by the namespace[userId, "memories"], independent of any thread. The graph reads it automatically each turn (it injects known user context into the system prompt), and the agent writes to it with theremembertool. Available in every pattern, since memory is cross-cutting.
The two are orthogonal: thread_id scopes a conversation, user_id scopes long-term memory. Same thread_id = resume a chat; same user_id + new thread_id = a fresh chat that still knows the user.
Reference discipline. Long-term memory stores record references (ids) and lightweight context (team, role, preferences), never raw record contents or sensitive personal data. The UserMemory shape and the remember tool's schema are built to encourage this. Set MEMORY_TTL_SECONDS to expire memories on a retention window (for example 30 days); 0 keeps them indefinitely.
structured_query is intentionally simple. The model is prompted to produce a read-only aggregation; execution applies a trailing $limit (clamping larger limits) and a maxTimeMS. These are demo ergonomics, not security controls. There is no validation, allowlist, role separation, or injection defense by design; query security is the adopting organization's responsibility during productization. The only sanctioned optional guard is a one-line $out/$merge rejection (REJECT_WRITE_STAGES=true), off by default.
On the bootcamp doc's "read-only enforcement and query guardrails" (Learning Objective 6, Theory Session 3, and the Checkpoint 3 line "any query-generating tool is read-only and validated per the guardrails"): in this reference scaffold that is a teaching and productization topic, not a built-in layer. The scaffold keeps the query tool minimal on purpose so teams can see the raw mechanism; adding real read-only enforcement and guardrails is part of what teams learn to reason about and what the adopting organization owns downstream. Teams that want to demonstrate it can flip REJECT_WRITE_STAGES=true and build outward from there.
- Set
KB_COLLECTION,EVENTS_COLLECTION, andVECTOR_INDEX_NAMEin.env. - Replace the docs in
data/sample/kb/and/or the generator indata/sample/activity_events.tswith your data. For structured data, the default is to fill indata/mock-input/collection.md(schema + a few hand-authored samples) and let Claude Code expand it into the generator, so no real data leaves its source system; seedata/mock-input/README.md. Load a real export directly only if you already have an approved synthetic one. - If your Voyage embedding model changes, update
VOYAGE_EMBEDDING_MODELandVOYAGE_EMBEDDING_DIMENSIONSso the index dimension matches. - Update the plain-language description in
src/query/schema.tsso the query tool understands your fields. npm run load, thennpm run verify.
- Copy
src/tools/exampleBusinessTool.tsto a new file; give it a clearname,description, and zodschema, and implement the async function (usegetDb(),getChatModel(),retrievePassages(), etc.). - Import it in
src/tools/registry.tsand add it toallTools(or add it to a specific pattern insrc/patterns.ts). - The
descriptionand each field's.describe()are how the model decides when and how to call it. Be concrete.
npm run verify maps to the three bootcamp checkpoints: (1) the skeleton runs and answers a sample question per leg; (2) retrieval cites relevant passages, structured_query returns the correct records with its explanation, and hybrid draws on both legs; (3) at least two tools work, memory resumes on a repeated thread_id, and one demo scenario runs end to end.
The build confirmed current package APIs against the installed versions and official MongoDB docs. Where reality differed from the original build prompt's expectations:
- LangChain JS is on 1.x (
@langchain/core1.x,@langchain/langgraph1.x,@langchain/langgraph-checkpoint-mongodb1.x,@langchain/mongodb1.x,@langchain/aws1.x) and zod is 4.x. Thetool(),StateGraph/MessagesAnnotation,ToolNode(from@langchain/langgraph/prebuilt), andMongoDBSaver({ client, dbName })(needs.setup()) APIs are as used here. - MongoDB Node driver: the newest published driver is 7.5.0 (there is no 8.x on npm yet). The LangChain MongoDB packages depend on
mongodb@^6and carry their own nested copy, so passing our 7.xMongoClient/Collectioninto them needs a small, commented cast at two boundaries (memory/checkpointer.ts,retrieval/vectorStore.ts). Runtime is compatible; only the nominal TS types differ. - Voyage embeddings and reranking are called over the REST API with
fetch(behindsrc/retrieval/embeddings.tsandreranker.ts) rather than via an SDK, to keep dependencies minimal and behavior transparent.VoyageEmbeddingsimplements the LangChainEmbeddingsinterface so it plugs straight into the vector store. - Credentials come from the Lambda's
get_tokentask and live in memory only; thecompletiontask is not used.