sarvam-tools
by Apurv428
README.md
# Setu — Multilingual Voice Agent on Sarvam AI
> *Setu* (सेतु) means **bridge**. Speak a question in any major Indian language; an AI agent reasons over Sarvam's speech, translation, and chat tools and speaks the answer back in your language.
**Live demo: [setu-agent.onrender.com](https://setu-agent.onrender.com)**
<br>
## Demo
Try the live web app at [setu-agent.onrender.com](https://setu-agent.onrender.com) — no setup needed.
Or run the scripted CLI demo without a microphone:
```bash
python app.py --demo
```
This runs 3 scripted turns (each a follow-up on the last, to show memory works),
prints every tool call the agent makes, and saves the spoken replies as WAV files.
*(add demo.gif here after recording)*
<br>
## What this project demonstrates
| Capability | How |
|---|---|
| Hands-on use of Sarvam models | Saaras v3 (STT), Bulbul v3 (TTS), Sarvam-Translate, sarvam-30b (chat) |
| Building an MCP server from scratch | FastMCP server with 6 tools, 2 resources, 3 prompts — testable in the MCP Inspector |
| Authoring an agent without a framework | `scratch_agent.py` — a hand-written JSON tool-call loop, no LangChain/LangGraph |
| Authoring the same agent with a framework | `graph_agent.py` — LangGraph ReAct consuming the same MCP server |
| Retrieval-augmented answers | `retrieval.py` + `search_knowledge` MCP tool — local embeddings over a knowledge base |
| Measuring quality | `eval/run_eval.py` — 14-case eval suite with LLM-as-judge scoring |
| Web interface + cloud deployment | `streamlit_app.py` deployed on Render with mic and text input |
<br>
## Architecture
```
User (browser or CLI)
│
├── streamlit_app.py (web — text input + mic recording)
│
└── app.py (CLI — mic, --chat REPL, --text, --demo)
│
│ audio path / text query
▼
┌──────────────────────────┐ tool calls over MCP / JSON protocol
│ Agent orchestrator │ ─────────────────────────────────────────┐
│ │ │
│ scratch_agent.py │ │
│ (no framework) OR │ ◄────────────────────────────────────────┘
│ graph_agent.py │ tool results
│ (LangGraph) │
└──────────────────────────┘
│
▼
┌──────────────────────────────┐
│ mcp_server.py │
│ "sarvam-tools" (FastMCP) │
│ │
│ transcribe_audio ────────► Saaras v3
│ detect_language ────────► sarvam-30b
│ translate_text ────────► Sarvam-Translate
│ answer_question ────────► sarvam-30b
│ synthesize_speech ────────► Bulbul v3
│ search_knowledge ────────► retrieval.py
└──────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ sarvam_client.py │
│ single source of truth │
│ for every Sarvam API call │
└──────────────────────────────┘
```
The agent **decides** which tools to call and in what order. A typical turn looks like:
1. `search_knowledge` — check the local knowledge base first (for questions about Indian languages/scripts)
2. `transcribe_audio` — WAV → text + detected language (e.g. `hi-IN`)
3. `translate_text` — translate question to English for better reasoning accuracy
4. `answer_question` — get the answer from `sarvam-30b`
5. `translate_text` — translate answer back to the user's language
6. `synthesize_speech` — text → WAV via Bulbul v3
The agent may skip steps (e.g. answer directly in Hindi without translation hops when the model handles it natively). That decision is the agent's, not hard-coded logic.
<br>
## Tech stack
| Layer | Library / Model |
|---|---|
| Speech-to-text | Sarvam **Saaras v3** |
| Translation | Sarvam **Sarvam-Translate / Mayura** |
| Chat / reasoning | Sarvam **sarvam-30b** (64 K context, native tool calling) |
| Text-to-speech | Sarvam **Bulbul v3** |
| MCP server | **FastMCP** (`mcp` Python SDK) |
| Framework agent | **LangGraph** + `langchain-mcp-adapters` + `langchain-openai` |
| Embeddings (RAG) | `sentence-transformers` `paraphrase-multilingual-MiniLM-L12-v2` (local, no API) |
| Web interface | **Streamlit** — text input + `st.audio_input` mic widget |
| Audio I/O (CLI) | `sounddevice` + `scipy` |
| Deployment | **Render** (web service, auto-deploy from GitHub) |
| Config | `python-dotenv` |
<br>
## Project structure
```
setu/
├── sarvam_client.py # Thin wrapper — only file that calls Sarvam APIs
├── mcp_server.py # FastMCP server: 6 tools + 2 resources + 3 prompts
├── retrieval.py # Local RAG: embedding index over knowledge/ docs
├── scratch_agent.py # Agent loop with NO framework (the differentiator)
├── graph_agent.py # Same agent built with LangGraph
├── app.py # CLI voice entrypoint: mic → agent → speaker
├── streamlit_app.py # Web interface: text + mic → agent → chat UI
├── render.yaml # Render deployment config
├── run_inspector.ps1 # One-click MCP Inspector launcher (Windows)
├── knowledge/ # Markdown docs the agent can retrieve
│ ├── indian_languages_overview.md
│ ├── hindi_language.md
│ ├── tamil_language.md
│ ├── indic_scripts.md
│ ├── language_families.md
│ └── sarvam_ai.md
├── eval/
│ ├── dataset.json # 14 labeled test cases
│ ├── run_eval.py # Eval runner with LLM-as-judge
│ └── results.json # Last run results
├── tests/
│ ├── test_retrieval.py # Unit tests for retrieval.py (mocked, fast)
│ ├── test_tools.py # Unit tests for MCP tools (mocked, fast)
│ ├── test_scratch_agent.py # Unit tests for the scratch agent loop
│ ├── test_eval_scoring.py # Unit tests for judge/scoring logic
│ └── test_mcp_live.py # Live integration test — real API calls via MCP stdio
├── requirements.txt
└── .env.example # Copy to .env and add your key
```
<br>
## Quickstart
### 1. Get a free Sarvam API key
Sign up at [dashboard.sarvam.ai](https://dashboard.sarvam.ai) — it's free.
### 2. Clone and set up
```bash
git clone https://github.com/Apurv428/setu-agent.git
cd setu-agent
python -m venv .venv
# Windows:
.venv\Scripts\activate
# macOS / Linux:
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
# Open .env and set: SARVAM_API_KEY=your_key_here
```
### 3. Verify the Sarvam client
```bash
python sarvam_client.py
```
Expected: four `PASS` lines — translate → chat → synthesize → transcribe.
### 4. Run the Streamlit web app
```bash
streamlit run streamlit_app.py
```
Opens at `http://localhost:8501`. Type a question or use the mic tab to record. Each reply shows the tool call trace in a collapsible expander, and the synthesized audio plays inline.
### 5. Inspect the MCP server (Windows)
```powershell
.\run_inspector.ps1
```
Opens the MCP Inspector at `localhost:6274` with everything pre-configured. Click **Connect** — you'll see all 6 tools listed immediately. Select any tool, fill in the input, and click **Run Tool** to call the live API.
> On macOS/Linux: `mcp dev mcp_server.py` and add `SARVAM_API_KEY` in the Environment Variables panel.
### 6. Run the unit tests
```bash
pytest -q
```
42 tests, zero network calls, runs in about 3 seconds.
### 7. Run an agent from the CLI
```bash
# Framework-free scratch agent
python scratch_agent.py "Which script is Marathi written in?"
# LangGraph agent (same MCP server)
python graph_agent.py "Which script is Marathi written in?"
```
### 8. Full voice loop (CLI)
```bash
# Text input (no mic required)
python app.py --text "भारत की राजधानी क्या है?"
# Mic input — records 5 seconds
python app.py
# Multi-turn chat with memory (keeps context across follow-up questions)
python app.py --chat
# Scripted 3-turn demo, no mic required
python app.py --demo
```
### 9. Query the knowledge base directly
```bash
python retrieval.py "Devanagari"
python retrieval.py "Which languages use the same script as Hindi?"
```
Prints the top-3 relevant chunks with similarity scores. The index is built on first run and cached for fast subsequent queries.
### 10. Run the eval
```bash
python eval/run_eval.py
python eval/run_eval.py --category qa
python eval/run_eval.py --verbose
```
<br>
## Web interface
The Streamlit app (`streamlit_app.py`) is the recommended way to try Setu without setting up a local environment.
**Live:** [setu-agent.onrender.com](https://setu-agent.onrender.com)
**Two input modes:**
- **Text tab** — type any question, press Send
- **Voice tab** — click the mic, speak, click Stop — the agent runs automatically
**What you see per turn:**
- The assistant's text answer in a chat bubble
- A collapsible **Tool calls** expander showing every step the agent took (e.g. `step 1: search_knowledge(...)`)
- The synthesized Bulbul v3 audio playing inline below the answer
**Memory** is kept across turns within the browser session. Use the **Clear conversation** button in the sidebar to start fresh.
**Good questions to try:**
- "Which script is Marathi written in?" — triggers `search_knowledge` before answering
- "How many characters does the Tamil script have?" — retrieval + precise answer
- "Translate 'good morning' to Tamil"
- "What are the four language families of India?"
- Then follow up with "Which one has the most speakers?" — tests memory
<br>
## MCP Server — verified results
The server exposes **6 tools**, **2 resources**, and **3 prompt templates**, all verified live against the Sarvam API in the MCP Inspector.
---
### Tools
#### `detect_language`
Returns the BCP-47 language code of a text string.
```
Input : "kashi aahe"
Output: mr-IN ← Marathi detected correctly
Input : "नमस्ते, आप कैसे हैं?"
Output: hi-IN
Input : "வணக்கம், நீங்கள் எப்படி இருக்கிறீர்கள்?"
Output: ta-IN
```
#### `answer_question`
Answers a question using `sarvam-30b` (64K context, native tool calling).
```
Input : "What is the capital of Maharashtra?"
Output: "The capital of Maharashtra is Mumbai."
Input : "भारत की सबसे लंबी नदी कौन सी है?"
Output: "भारत की सबसे लंबी नदी गंगा है।"
```
#### `translate_text`
Translates between Indic languages and English. Pass `"auto"` as `source_language_code` to detect automatically.
```
Input : text="i am apurv" source=en-IN target=mr-IN
Output: "मी अपूर्व आहे"
Input : text="Good morning" source=auto target=mr-IN
Output: "शुभ सकाळ"
```
#### `synthesize_speech`
Converts text to speech using Bulbul v3. Returns the path to the saved WAV file.
#### `transcribe_audio`
Transcribes an Indian-language WAV file using Saaras v3. Returns transcript + detected language.
#### `search_knowledge`
Searches the local knowledge base (six markdown documents about Indian languages and scripts). Returns the top-3 relevant passages with source file and similarity score, or `NO_RELEVANT_KNOWLEDGE_FOUND` if no passage scores above 0.35.
```
Input : "Which script is Marathi written in?"
Output: [indic_scripts.md | score 0.70]
Devanagari is the most widely used Indic script. It is used to write
Hindi, Marathi, Sanskrit, Nepali, Konkani...
```
---
### Resources
| URI | Content |
|-----|---------|
| `sarvam://languages` | All 11 supported BCP-47 language codes with names |
| `sarvam://models` | Available Sarvam models (STT, TTS, Chat, Translate) with context limits |
---
### Prompts
| Prompt | Arguments | Use case |
|--------|-----------|----------|
| `answer_in_language` | `question`, `language_code` | Ask a question and get a reply in a specific Indic language |
| `translate_prompt` | `text`, `target_language_code`, `source_language_code` | Ready-to-use translation prompt with source/target |
| `voice_agent_turn` | `user_utterance`, `detected_language` | Full voice-agent turn: transcription → reasoning → synthesised reply |
<br>
## Live agent trace
Real output from `scratch_agent.py` on a Hinglish (code-mixed) query — the agent detects the language, answers, and replies in Hindi:
```
Query: Maharashtra ki rajdhani kya hai?
step 1: detect_language({'text': 'Maharashtra ki rajdhani kya hai?'})
-> hi-IN
step 2: answer_question({'question': 'What is the capital of Maharashtra?'})
-> The capital of Maharashtra is Mumbai. ...
Final: आप सही कह रहे हैं। महाराष्ट्र की आर्थिक राजधानी मुंबई है,
जबकि नागपुर आधिकारिक राजधानी है।
```
<br>
## Retrieval-augmented answers
The `knowledge/` directory contains six markdown documents covering Indian languages and scripts: an overview of India's 22 scheduled languages, deep dives into Hindi and Tamil, a survey of Indic scripts (Devanagari, Tamil, Bengali, Telugu, Kannada, Malayalam, Gurmukhi), a guide to India's four language families, and a note on Sarvam AI's model lineup.
The agent uses `search_knowledge` automatically when a question is about Indian languages or scripts. The tool embeds the query using `paraphrase-multilingual-MiniLM-L12-v2` (a local sentence-transformers model that handles Hindi, Marathi, Tamil, and other Indic queries against English documents) and returns the top-3 passages by cosine similarity. If all scores fall below 0.35, it returns `NO_RELEVANT_KNOWLEDGE_FOUND` and the agent falls back to `answer_question`.
To verify retrieval standalone:
```bash
python retrieval.py "Devanagari"
python retrieval.py "Tamil classical language"
```
The embedding index is built on first run (~10 seconds) and cached to `knowledge/.index.npz`. It is rebuilt automatically if any document is newer than the cache file.
<br>
## Measuring quality
The eval suite lives in `eval/`. Run it with:
```bash
python eval/run_eval.py
```
### Categories
| Category | Cases | What it tests |
|---|---|---|
| `qa` | 4 | Questions answered via the scratch agent (exercises both RAG and the agent loop), scored by LLM-as-judge |
| `translation` | 4 | Sarvam-Translate across en-IN, hi-IN, ta-IN, mr-IN, bn-IN pairs, scored by judge |
| `lang_detect` | 3 | Language detection in Hindi, Tamil, Bengali scripts, scored by exact BCP-47 match |
| `round_trip` | 3 | TTS then STT on the same phrase, scored by string similarity (PASS at >= 0.80) |
### Results (run 2026-06-11)
| Category | Passed | Total | Accuracy |
|---|---|---|---|
| qa | 3 | 4 | 75.0% |
| translation | 4 | 4 | 100.0% |
| lang_detect | 3 | 3 | 100.0% |
| round_trip | 3 | 3 | 100.0% |
| **Overall** | **13** | **14** | **92.9%** |
The one qa miss was a judge disagreement on a question whose retrieved passage contained the correct answer (Devanagari for Marathi); the underlying retrieval and reasoning were correct.
<br>
## Tests
The test suite (`pytest -q`) covers retrieval chunking and cosine ordering, cache freshness/invalidation, every MCP tool with mocked sarvam_client, the scratch agent loop (happy path, malformed JSON recovery, unknown tool error, max_steps cap, multi-turn history), and the eval judge (valid JSON, malformed-then-valid re-ask, double failure, API errors). All 42 tests run without network access or model downloads in about 3 seconds.
```bash
pytest -q
```
<br>
## The two agents — what's different
### `scratch_agent.py` — framework-free
The entire mechanism is visible. The model replies with JSON; we parse it, dispatch to a tool, append the observation, and repeat. This is the loop that LangGraph runs for you — building it once by hand is how you understand what a framework actually does.
```json
{"tool": "search_knowledge", "args": {"query": "Marathi script"}}
{"tool": "answer_question", "args": {"question": "..."}}
{"final": "Marathi is written in Devanagari.", "audio_path": "reply.wav"}
```
Handles: malformed JSON (re-prompts with the contract), unknown tools (reports available tools), `max_steps` cap.
### `graph_agent.py` — LangGraph
The same behaviour, but LangGraph manages the state machine, the tool-call loop, and retries. `sarvam-30b` via its OpenAI-compatible endpoint supports native tool calling — no hand-written JSON protocol needed. MemorySaver provides conversation memory keyed by `thread_id`.
Both agents connect to the **same `mcp_server.py`** over stdio.
<br>
## Design decisions
**Why MCP instead of calling the functions directly?**
Clean, reusable boundary. The same server backs the scratch loop, the LangGraph agent, and anything else — tested in isolation with the inspector before any agent touches it.
**Why two agents?**
To make the contrast explicit. The scratch loop shows the mechanism; LangGraph shows what the framework automates. Building it by hand earns the right to say you can author agents without a framework.
**Why a single `sarvam_client.py`?**
All Sarvam-specific request shapes, model IDs, and response fields live in one file. If Sarvam changes a field name, exactly one file changes.
**Why local embeddings for RAG?**
No additional API key, no cost per query, no latency beyond the first index build. The multilingual MiniLM model handles Hindi, Tamil, and Marathi queries against English documents without translation.
**Why Streamlit for the web interface?**
Minimal code on top of the existing agent. `st.audio_input` gives a real mic widget with no JavaScript, `st.cache_resource` keeps the embedding model loaded across requests, and Render auto-deploys from GitHub on every push.
<br>
## Failure modes handled
| Failure | Handling |
|---|---|
| Malformed JSON from the model | Re-prompt with the JSON contract; retry up to `max_steps` |
| Unknown tool name in model output | Return available tool names as the observation |
| No relevant knowledge found | `search_knowledge` returns `NO_RELEVANT_KNOWLEDGE_FOUND`; agent falls back to `answer_question` |
| Wrong language detection | STT-detected language is preferred; `translate(auto)` as fallback |
| API errors | Surfaced as tool-call errors; step cap prevents runaway loops |
<br>
## Language codes supported
`hi-IN` Hindi · `mr-IN` Marathi · `ta-IN` Tamil · `te-IN` Telugu · `bn-IN` Bengali · `gu-IN` Gujarati · `kn-IN` Kannada · `ml-IN` Malayalam · `pa-IN` Punjabi · `od-IN` Odia · `en-IN` English (Indian)
<br>
## Limitations
- **Streaming** — TTS and STT are request/response, not streamed. Both Saaras and Bulbul support WebSocket streaming for lower latency; not wired up here.
- **Observability** — tool calls print to stdout but are not traced to any structured logging system. LangSmith or a simple spans table would make debugging easier at scale.
<br>
---
Built with [Sarvam AI](https://sarvam.ai) APIs · [dashboard.sarvam.ai](https://dashboard.sarvam.ai) for your free key
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues