Skip to main content
Go from zero to a production AI assistant that answers “why was this built this way?” in four phases. Start with one file and a real search query. End with a FastAPI backend that ingests GitHub, PRs, Slack, and RFCs, then generates GPT-4o answers grounded in your codebase.
How this guide is structured. Each phase ends with something that works. Phase 0 is a complete minimal system in under 10 minutes. Phases 1–3 are progressive upgrades. You never need to redo what came before.
All code in this cookbook is real. Base URL is https://api.hydradb.com. Get your API key at app.hydradb.com.

Prerequisites

Required knowledge: Python basics, REST APIs, environment variables
Required tools:
  • HydraDB API key
  • Python 3.11 or 3.12 (python --version)
  • OpenAI API key (for generating answers in Phase 3)
  • pip install hydradb-sdk openai

What You’ll Build

By the end of this cookbook, you’ll be able to:
  • Upload any codebase file into HydraDB and get grounded answers to “why was this built this way?”
  • Progressively ingest GitHub files, PRs, Slack threads, and RFCs across four phases
  • Build a FastAPI backend that answers developer questions using search_docs + GPT-4o
  • Add a VS Code extension trigger to query HydraDB from inside your editor

Phase 0 - Minimal Working System · 5–10 minutes

The only goal is to see a real search response from a real file you uploaded. No multi-source ingestion, no backend server, no VS Code extension - just the four API calls that prove the pipeline works end-to-end. Goal: You will run one search query and see actual results.
⚠️ Do Phase 0 first, even if you plan to skip ahead. Every later phase assumes the database exists and indexing works. Running Phase 0 takes under 10 minutes and eliminates the most common failure modes.

Prerequisites

You need three things before writing any code:
  1. A HydraDB API key. Email [email protected] or book a demo at hydradb.com. You’ll receive a key that starts with hdb_.
  2. Python 3.10+. Run python3 --version to check.
  3. The HydraDB SDK. Run pip install hydradb-sdk python-dotenv.
Create config.py in the project root. This is the canonical configuration used by every script in this guide:
Authorization header must be exact. Authorization: Bearer hdb_abc123 - capital B, one space, no quotes. Anything else returns 401.

Step 1 - Create a Database

What: Registers your top-level namespace. Database creation is asynchronous - infrastructure provisions in the background. Do not upload files until polling confirms it is ready. Endpoint: POST /databases - Async, poll /databases/status before ingesting
Expected output:
If it fails: 401 - API key wrong or missing Bearer prefix. Confirm .env has HYDRA_DB_API_KEY=hdb_abc123 with no surrounding quotes.

Step 2 - Upload One File

What: Uploads a single file using multipart form-data to /context/ingest - the recommended beginner path. HydraDB handles chunking, embedding, and graph-node creation automatically. The returned id is what you use in Step 3 to verify indexing.
💡 Two ingestion modes exist. This step uses multipart file upload via /context/ingest - the tested beginner path. An advanced JSON body mode (used in Phases 1–2) supports structured IDs and explicit graph relations. Use file upload here first.
First, create a sample document:
Expected output:
The exact value of id depends on HydraDB’s internal file registration. Copy the value returned and use it in Step 3. If it fails:
  • 404 Database does not exist - Step 1 not complete, or database mismatch.
  • 400 / missing files - Confirm the field name is documents (not file), and that Content-Type is NOT manually set in headers.

Step 3 - Verify Indexing

What: Polls /context/status using the id returned in Step 2. HydraDB returns a statuses array; you read statuses[0].indexing_status. Querying before this reaches "completed" returns empty results with no error - the most common beginner confusion. Replace YOUR_ID_HERE with the id from Step 2:
Expected output:

Step 4 - Run Your First Search Query

What: Sends a POST to /query and receives a chunks array. Each chunk has a chunk_content field - this is the text you will later pass to GPT-4o as context. This is the end-to-end proof that database, ingestion, indexing, and retrieval all work. Validated first search - minimal working request:
Expected output:
If it fails: chunks: [] - Indexing is not yet complete. Wait 30 seconds and retry. Re-run Step 3 to confirm indexing_status: completed.
Phase 0 complete. The same /query endpoint - with the same minimal three-field body - is what every later phase uses. You’ll only add parameters, not change the structure. The chunks array and chunk_content field are now your canonical search response objects.
What you just built: You now have a working retrieval system. HydraDB can store your content, index it, and return the most relevant chunks - each with a chunk_content text field and a relevancy_score - for a question. The missing layer is the reasoning backend that takes those chunks and turns them into a readable, cited answer.

Phase 1 - Improve Retrieval · 15–20 minutes

Ingest multiple files with structured metadata, use collections to organise content, and tune search parameters. At the end of this phase, search queries return more relevant chunks across many documents. Goal: 50+ files indexed with metadata, scoped search working.

How Retrieval Works

Before writing more ingestion code, it’s worth understanding what HydraDB actually returns and how to interpret it. This mental model applies to every phase. Chunks vs. sources: When HydraDB indexes a document, it splits the content into overlapping chunks. Each chunk is embedded independently and stored as a node in the context graph. When you call /query, you get back a chunks array. Iterate this array and read chunk_content from each item - that is the text you pass directly to your LLM as context. The response also includes a sources array - a deduplicated list of the original documents that contributed chunks. Use sources for citation labels; use chunks for the actual LLM context. Anatomy of a search chunk - field reference: How to interpret the relevancy_score: Scores are relative within a single response - they indicate ranked relevance for your specific query, not absolute confidence. How graph context changes search: When you add "graph_context": true to your search request, HydraDB walks the explicit relations.ids edges you set at ingestion time for every high-scoring chunk. A source file chunk can pull in the PR that last changed it. That PR can pull in the RFC it referenced. This multi-hop traversal is what makes “why” questions answerable.
💡 Always safe to add to Phase 0. "graph_context": true has no downside with a single file and no relations - it returns the same result. Turn it on now.

P1 · Step 1 - Batch Upload with Explicit IDs (JSON Ingestion)

What: Switches to JSON body ingestion via /context/ingest with app_knowledge field - the advanced ingestion path. This gives you full control over IDs, timestamps, collections, metadata, and (in Phase 2) explicit graph relations. The {repo_name}/{relative_path} ID convention is required for Phase 2 graph linking.
⚠️ Advanced path - stricter validation. JSON ingestion requires well-formed payloads with id, type, and content.text on every item. Malformed requests are rejected outright. Use the file upload path (Phase 0 Step 2) if you just want to get content indexed quickly. Use JSON ingestion when you need stable IDs and graph relations.
Expected output:

P1 · Step 2 - Metadata and Collections

What: collections are labels you define for scoping search. additional_metadata fields are arbitrary key-value pairs used for filtering and citation labels. No extra API calls needed - these fields go in the ingestion payload.

P1 · Step 3 - Tuned Search with Graph Context


Phase 2 - Multi-source Context · 20–30 minutes

Connect GitHub source files to their pull requests, Slack decision threads, and internal wikis. Use explicit relations.ids to guarantee graph edges. When a developer asks “why?”, the answer now travels across all four source types in a single query. Goal: A “why” question returns code + PR + Slack + RFC in one response. Pipeline: Source files → Pull requests → Slack threads → Wikis & RFCs → Graph built
💡 Explicit vs. automatic graph edges. Setting relations.ids guarantees a graph link every time. HydraDB may also try to extract relationships from content, but this is best-effort. Always use explicit relations for links that matter.

Why Each Source Type Matters

A developer asking “why does the auth middleware skip token validation for internal IPs?” cannot be answered by any single context. Here is what each source type contributes: A normal code search tool answers what a function does. It is weak at answering why it exists because the rationale almost never lives in the code file alone. HydraDB becomes powerful when you index all the places where engineering intent is recorded:
  • Code tells you what is running now.
  • Pull requests tell you why the code changed and what alternatives were discussed.
  • Slack threads capture informal trade-offs, approvals, and urgency.
  • RFCs and ADRs record the formal decision and long-term architectural reasoning.
When those sources share stable IDs and explicit relations, the assistant can move from a source file to the PR that introduced it, to the Slack thread that approved it, to the ADR that documented it.
💡 Adaptation checklist. Pick a stable ID scheme first, choose the minimum set of high-signal sources second, then expand. Good IDs and a small number of trustworthy sources beat a huge but messy index every time.

P2 · Step 1 - Ingest GitHub Source Files with Consistent IDs

What: Ingests source files using the {repo_name}/{relative_path} ID convention. These IDs are what PR ingestion will reference in relations.ids. The IDs must match exactly - myrepo/auth/middleware.py, not auth/middleware.py. Use ingest_directory() from Phase 1 Step 1. If you already ran Phase 1, your files are already indexed.

P2 · Step 2 - Ingest Pull Requests with Explicit Graph Relations

What: Fetches merged PRs and turns each into one document: title + description + review comments + changed-file list. The relations.ids field guarantees graph edges form between the PR and every source file it changed. Before: A GitHub token with repo scope. Create at github.com/settings/tokens → New classic token → check repo → add to .env as GITHUB_TOKEN, GITHUB_OWNER, GITHUB_REPO.

P2 · Step 3 - Ingest Slack Threads

What: Groups messages by thread_ts so each conversation thread becomes one document. Architecture channels, post-mortems, and decision threads are the highest-value sources - these capture approvals that never get written up anywhere else. Before: A Slack export ZIP. Go to your-workspace.slack.com/services/export (requires admin), export, and unzip. Structure: {export_dir}/{channel_name}/{YYYY-MM-DD}.json.

P2 · Step 4 - Ingest Wikis & RFCs

What: Ingests Architecture Decision Records, RFCs, Confluence pages, and Notion wikis. Tag each with doc_type so search can be scoped to decisions-only when that is what a question requires.

P2 · Step 5 - Run a Multi-source Graph Query

Expected output:
Phase 2 complete. A “why” question now returns multi-hop retrieved context. Phase 3 wraps this in a production FastAPI server with your own GPT-4o answer generation layer.

Phase 3 - Backend & Answer Generation · 25–35 minutes

Build a production FastAPI server. HydraDB is the memory layer - it retrieves ranked chunks via search_docs(). GPT-4o is the reasoning layer - it reads those chunks and writes a grounded, cited answer. The two layers are kept strictly separate so they can be debugged and improved independently. Goal: POST /chat and POST /ask → HydraDB chunks → GPT-4o → answer.

Backend Architecture

The flow through the backend is:
  1. Question - POST /chat or /ask
  2. Search - search_docs() → chunks
  3. Context block - chunk_content assembled
  4. GPT-4o - writes grounded answer
  5. Response - answer + citations
Why split retrieval and reasoning? Because they fail differently. Weak search means you improve ingestion, metadata, or graph links. Weak answers mean you improve prompt construction or model behavior. Keeping the layers separate means you can isolate which side is the problem. File structure:
Architecture summary:
  • config.py - Loads .env, exposes HYDRA_DB_API_KEY, DATABASE_ID, client (HydraDB SDK instance), OPENAI_API_KEY, and search defaults. Every other file imports from here only.
  • hydra_client.py - search_docs() is the HydraDB retrieval path. It returns chunks, sources, and graph context; prompt formatting and OpenAI calls stay in answer.py. Imported by → search.py, app.py.
  • search.py - recall_context() calls search_docs() and returns the raw payload. build_context_block() extracts chunk_content from each chunk and assembles a formatted context string for the LLM. Imported by → app.py.
  • answer.py - Takes the context block from search.py, formats the system + user prompt, and calls the OpenAI streaming API. Contains all anti-hallucination rules. Never calls HydraDB. Imported by → app.py.
  • app.py - Two endpoints: POST /chat streams NDJSON token-by-token - ideal for a web or IDE frontend. POST /ask returns a complete JSON response with answer, sources, and chunks - easier for Postman testing. Both use the same recall_contextbuild_context_blockstream_answer pipeline.
Install dependencies for Phase 3:
Add to .env:
Create the empty package init file:

backend/config.py

Centralises all credentials. Use HYDRA_DB_API_KEY as the variable name for your HydraDB key. All other backend files import from here.

backend/hydra_client.py

search_docs() is the HydraDB retrieval path. It returns ranked chunks and source metadata; it does not format prompts or decide routing.

backend/search.py

recall_context() calls search_docs() and returns the raw HydraDB payload. build_context_block() reads the chunks array, extracts each chunk_content field, and assembles them into a single formatted string that is passed to the LLM. The sources array from the payload is used separately by app.py for citation output.
💡 Production upgrade: score filtering. build_context_block is intentionally simple for the first backend. For production, add a relevancy_score filter to drop low-quality chunks before building the context string - the same pattern used in Phase 1’s build_context() helper.

backend/answer.py

Takes the context block string from search.py and turns it into a grounded GPT-4o answer. This is the only file that touches OpenAI. If the context string is empty, it returns a safe fallback without calling the model at all - this is the primary hallucination guard.

backend/app.py

The FastAPI server exposes two endpoints. POST /chat streams NDJSON - best for a web frontend or VS Code extension. POST /ask returns a complete JSON response - easier for Postman testing and best for API consumers that don’t stream. Both use the same recall_contextbuild_context_blockstream_answer pipeline.
💡 Want WHY_SIGNALS routing? Once this basic backend is working, add a question classifier that adjusts search_docs() parameters for “why” questions, such as lower alpha, higher max_results, and graph_context: true. Keep final answer generation in your app layer.

Answer Generation Flow

Understanding the exact data path prevents debugging confusion. build_context_block() does the following in order:
  1. Read the chunks array from the search payload. /query returns {"chunks": [...], "sources": [...]} - always iterate chunks, never results.
  2. Extract chunk_content from each chunk object. This is the raw text of the chunk - a few hundred tokens of the original document. It is the only required field. Skip any chunk where chunk_content is absent or empty.
  3. Join all chunk_content values with double newlines to form the context body.
  4. Append the sources list from recall_payload.sources in - title (id) format so the model can cite them.
  5. Return the assembled string to stream_answer(). This becomes the user message body inside the OpenAI prompt.
The assembled context block that GPT-4o receives looks like this:

Avoiding Hallucination

LLMs will confidently invent details about your codebase if you don’t actively prevent it. The system prompt in answer.py contains specific rules for each failure mode:
⚠️ Do not raise temperature above 0.2 for this use case. Higher temperature increases creativity, which is the opposite of what you want for accurate, cited answers about specific code.

Running the Server

Expected startup output:
Test the streaming /chat endpoint:
Test the synchronous /ask endpoint (easier for Postman):
Phase 3 complete. You have a production FastAPI backend. HydraDB is the memory layer - search_docs() retrieves ranked chunks. GPT-4o is the reasoning layer - it reads those chunks via stream_answer() and writes a grounded answer. The two layers are intentionally separate so you can debug and improve each independently.

Phase 4 - Productization · Coming next

Ship the assistant to your team. Build the VS Code extension sidebar, set up a daily incremental sync cron job, wire a GitHub Actions workflow for on-push indexing, and monitor search quality over time. Goal: Team using it daily with a fresh index on every merge. Detailed implementation coming in the next revision. Everything in Phase 4 is packaging around a working Phase 3 server. Planned components: VS Code extension (TypeScript), Webview chat UI, Active-file context injection, Daily cron sync, GitHub Actions on push to main, Incremental re-index, Search quality monitoring.

If Your Retrieval Quality Is Weak

  • Fix your IDs and metadata. Weak retrieval often starts upstream. Use stable IDs, clear titles, accurate timestamps, and useful metadata like doc_type, repo, and channel.
  • Add explicit relations. If “why” answers feel shallow, it usually means the assistant sees the code but not the documents around it. Add relations.ids between code, PRs, Slack, and RFCs.
  • Improve source formatting. A PR with only a title is weak. A PR with title, description, changed files, reviews, and inline comments is strong. Rich documents produce better chunks.
  • Add relevancy_score filtering. The production upgrade to build_context_block is to drop chunks where relevancy_score is below your threshold before assembling the context string.

How to Extend Beyond the First Version

A good progression: Phase 0 search in the terminal → FastAPI /ask in Postman → FastAPI /chat with a simple web frontend → VS Code sidebar → streaming, incremental sync, caching, usage analytics.

Troubleshooting

401 - Not Authenticated

Cause: The Authorization header is missing or malformed. Most common: the Bearer prefix is absent, quotes surround the key in .env, or there is a leading/trailing space in the key value. Fix: Run python3 -c "from config import HYDRA_DB_API_KEY; print(repr(HYDRA_DB_API_KEY))". The output should be 'hdb_abc123' - no surrounding spaces. Your header must be exactly Authorization: Bearer hdb_abc123.

404 - Database Does Not Exist

Cause: You are attempting to upload or query before the database was created, or the database in your request does not match the one you created. Database IDs are case-sensitive. Fix: Run python3 phase0/create_tenant.py and wait for ”✓ Database ready.” before proceeding. Confirm DATABASE_ID in config.py exactly matches what you passed to create_tenant.

404 - Not Found (on search or ingestion endpoint)

Cause: The endpoint path has a typo, extra slash, or missing segment. Also occurs when querying a collection that has never had data written to it. Fix: Correct paths: /context/ingest (file upload or JSON batch with app_knowledge), /query, /context/status. If using collection, confirm at least one batch was successfully uploaded to that collection first.

400 - Provide at least one of: documents or app_knowledge

Cause: For file upload: the field name is wrong (file instead of documents), or Content-Type: application/json was manually set (which breaks multipart). For JSON batch: the array is empty or objects are missing required fields. Fix: For file upload: use files={"documents": (filename, f, "text/markdown")} and data={"database": DATABASE_ID}. Do NOT set Content-Type manually. For JSON batch: confirm each item has id, type, and content.text.

Empty Search - chunks: []

Cause: Indexing is still in progress, or relevancy_score filtering in build_context_block is too aggressive, or the query does not semantically match any ingested content. Fix: Step 1 - Run verify_file() and confirm "indexing_status": "completed" in the statuses array. Step 2 - Run phase0/search.py directly and print the raw response - check that the chunks key is present and non-empty before any filtering. Step 3 - Check the relevancy_score values on returned chunks.

429 - OpenAI insufficient_quota

Cause: Your OpenAI account has run out of credits or hit its usage limit. This is an OpenAI-side error - HydraDB retrieval may be working perfectly. The error appears in stream_answer() inside answer.py, not in the HydraDB search step. Fix: Add billing or upgrade your OpenAI plan at platform.openai.com. While debugging, replace stream_answer in answer.py with this temporary debug fallback - it returns the raw retrieved context without calling OpenAI:
💡 General debugging pattern. Add print("STATUS CODE:", resp.status_code) and print("RESPONSE:", resp.text[:500]) immediately after every requests.post() call. See the full error body before doing anything else.

Production Notes

  • Batch size and rate limits. The JSON batch upload endpoint accepts a maximum of 20 source objects per request. Always sleep 1 second between batches. For large repos (1,000+ files), expect ingestion to take 10–30 minutes total.
  • Indexing delays. Indexing is async. Never rely on a fixed sleep; always poll /context/status until the statuses array shows "indexing_status": "completed".
  • Consistency and upserts. HydraDB upserts by id - re-uploading replaces the existing document. There is a brief window where search may return stale chunks. Run verify on new IDs before marking a deployment complete.
  • LLM context window. The basic build_context_block assembles all chunk text. For production, add a relevancy_score filter and a character cap (e.g. 12,000 chars) to prevent exceeding GPT-4o’s context window.
  • Graph edge consistency. Graph edges only form if both sides exist and are indexed. Always ingest source files first, then PRs. Re-ingest affected PRs if you add new files after initial ingestion.
  • API key security. The HydraDB API key grants full access to all database data. Never commit it to git. Use environment secrets in production. Rotate immediately if you suspect exposure.
  • OpenAI token costs. Each question sends context + system prompt + question to GPT-4o. For a 50-engineer team asking 200 questions/day, budget $2–4/day. Use gpt-4o-mini for factual lookups to reduce cost.
  • Search quality monitoring. Log the relevancy_score value of each chunk included in a response. If median scores are falling over time, new content may be diluting the index - re-ingest with better metadata and collections tagging. Also confirm the chunks array is non-empty before sending context to GPT-4o.

API Reference

All endpoints used in this cookbook. Base URL: https://api.hydradb.com. Header: Authorization: Bearer YOUR_API_KEY

Create Database

POST /databases - Async, poll /databases/status before ingesting

Upload File - Beginner Path

POST /context/ingest - Multipart form-data · field name "files" · database as form field

Upload Sources - JSON Body (Advanced)

POST /context/ingest - Max 20 items · body field app_knowledge (JSON string) · supports relations

Verify Indexing

GET /context/status - Returns statuses[] array · poll until "completed"

Search - Validated Minimal Request

POST /query - Primary verified path · collection optional · returns chunks[] and sources[]
POST /query - Use for multi-hop retrieval, then generate the answer in your app layer

Benchmarks

1,200 developer questions across three engineering teams (18–80 engineers, codebases 150k–2.2M lines) compared against naive vector search using identical ingested content.
ℹ️ The 22% accuracy on “why” questions isn’t a tuning problem - it’s structural. Embedding a source file and the RFC that motivated it produces similar-but-unlinked vectors. Without explicit graph edges, the RFC never surfaces when the source file matches. HydraDB makes the connection explicit: every node linked to your query gets traversed, not just nodes that look semantically similar.