Inspiration
Email is where my context lives, and it’s also where it gets lost. Answers like “what did the recruiter say about the start date?” or “which invoices are still unpaid?” are buried in threads I’ve already read once. Search boxes match keywords, not questions. I wanted to ask my inbox a question in plain English and get a grounded answer back. I once missed an interview because it was buried deep and you can't keep making multiple email accounts to stay at the top of every news.
The NVIDIA x Nebius hackathon was a good excuse to test a real idea: can a small, fast open model (Nemotron Nano on Nebius Token Factory) act as the “brain” of a personal assistant by choosing which tools to call, instead of relying on a giant model for everything?
What it does
Bella is a personal assistant currently only for emails. You ask a question in a browser or via curl, and the system:
Recalls relevant past context from a vector memory. Lets a Nemotron model decide which Gmail tools to call (search, read thread, and so on). Runs those tools through an MCP server. Returns an answer along with the tool calls made and the memory hits used, so you can see how it got there.
It also remembers. Tool results and past Q&A are indexed into ChromaDB, so later questions benefit from what the assistant has already seen.
How we built it
We used Claude to vibe code the project but the underlining structure and tools were researched and collaborated together and what other functionalities that needs to be there.
Challenges we ran into
Is tool calling reliable on a small model? This was the biggest unknown, so I built verification into the setup: list_models.py to find the exact model id, and check_tool_calling.py to prove the model returns well-formed tool calls before anything else runs. Safety with untrusted input. Emails are attacker-controlled text. A prompt-injected message could try to steer the agent. I limited the blast radius by using the gmail.readonly scope, so even a successful injection can’t send or delete mail. Dependency churn. MCP v2 renamed FastMCP to MCPServer and changed other APIs, so I pinned mcp<2. Gmail OAuth for a POC. Setting up a Desktop-app OAuth client with a test-user consent screen has several fiddly steps, so I wrote a one-time auth_gmail.py helper to keep it to a single command. Memory design. For now, memory is only a prompt hint and doesn’t skip the LLM call. I chose that deliberately to keep behavior easy to debug before adding caching.
What we learned
MCP cleanly separates what the agent can do from how the agent reasons, which made the Gmail server testable and swappable. An OpenAI-compatible endpoint makes it very easy to try open models in an existing agent loop. Exposing tool_calls and memory_hits in every response made debugging much faster than reading logs. Security decisions (read-only scope, localhost-only) are far cheaper to make on day one than to retrofit.
What's next for Bella the personal assistant
Model routing: Nano for fast tool-calling turns, a larger Nemotron (Super/Ultra) for heavier reasoning like “summarize my week.” Semantic cache: answer directly from memory when a hit is fresh enough. With a similarity threshold, a query is served from memory and otherwise falls through to the model. Daily digest as a Nebius Serverless Job writing into the same ChromaDB. Streaming responses over SSE so the UI doesn’t sit on “…”. Deployment as a Nebius Serverless Endpoint, with a Dockerfile, a persistent volume for data/, and real authentication. and integrating Langfuse to monitor the working on the model's properties that affects the cost and resources.
Built With
- api
- chromadb
- claude
- fastapi
- google-gmail-oauth
- langfuse
- mcp
- nebius
- python
Log in or sign up for Devpost to join the conversation.