Most AI agents retrieve text, stuff it into a prompt, and forget everything after the response. That works fine until someone asks a question that spans systems: "what breaks if we deprecate the v1 payments API, who owns those services, and is there already a migration plan?"
A vector search finds related chunks, but it can't connect the architecture decision record (ADR) to the Slack thread to the postmortem to the team that owns the fix.
You'll build an internal engineering assistant that can. You'll ingest engineering artifacts into HydraDB, compare retrieval with and without graph context, and see fragments become a company brain for your agent.
The same question, answered two ways:
Without graph context: four chunks from different documents. Relevant, but disconnected. The reader (or LLM) has to mentally assemble the dependency chain.
With graph context: the same chunks plus relationship evidence. For example, manifest paths linking affected services to the API, and extracted document relations from the ADR, postmortem, and Slack thread when they match the query. The LLM gets structure, not a reading list.
Prerequisites
- Python 3.10+ (HydraDB's SDK requires 3.10. We tested on 3.12)
- A HydraDB account and API key from app.hydradb.com
- An OpenAI API key (optional, used only for the final generation step. The retrieval comparison works without it)
- ~30 minutes
Install the HydraDB Python SDK
Create a working directory and install dependencies:
mkdir hydra-agent-tutorial && cd hydra-agent-tutorial
python -m venv .venv && source .venv/bin/activate
pip install "hydradb-sdk>=2.1.1,<3" openai python-dotenv
Create a .env file:
HYDRA_DB_API_KEY=your_hydradb_api_key
OPENAI_API_KEY=your_openai_api_key # optional
OPENAI_MODEL=gpt-4o # optional
And the top of your script:
import io
import os
import time
import json
from dotenv import load_dotenv
from hydra_db import HydraDB
from hydra_db.errors import ConflictError
from hydra_db.helpers import build_string
load_dotenv()
client = HydraDB(token=os.environ["HYDRA_DB_API_KEY"])
DATABASE = "eng_assistant_tutorial"
SHARED_COLLECTION = "engineering_knowledge"
def text_file(filename, content):
return (filename, io.BytesIO(content.encode("utf-8")), "text/plain")
def field(obj, key, default=""):
if obj is None:
return default
if isinstance(obj, dict):
return obj.get(key, default)
return getattr(obj, key, default)
def score_text(score):
return f"{score:.3f}" if isinstance(score, (int, float)) else "n/a"
HydraDB v2 uses database and collection as its conceptual names. In the current Python SDK, client.databases.* and client.query() expose those canonical names.
Define the engineering knowledge base
The knowledge base uses five synthetic engineering artifacts, the kind of documents that already exist in any mid-size engineering org. The shapes matter more than the content. Substitute your own once the pipeline works.
| Document | Type | Why it's here |
|---|---|---|
| ADR-007: Payments API v1 → v2 Migration | Architecture decision record | The decision to deprecate, with rationale and rollout plan |
| Architecture Review Meeting Notes | Meeting transcript | Cross-team discussion capturing concerns and owners |
| Slack Thread: v1 Deprecation Timeline | Chat messages | The informal timeline and blockers that never made it into the ADR |
| Payments Latency Incident Postmortem | Postmortem | A production incident caused by v1's connection pooling, which motivates the migration |
| Service Dependency Manifest | Structured data | Which services depend on which APIs, with team ownership |
The ADR is ingested as a text document. The meeting notes, Slack thread, and postmortem are ingested as App Sources with source-specific metadata. The fifth artifact is a structured dependency graph, which requires a different ingestion method.
Create a HydraDB database
# Create a dedicated database for this tutorial.
try:
client.databases.create(database=DATABASE)
except ConflictError:
print(f"Database {DATABASE!r} already exists; reusing it.")
# Poll until the infrastructure is ready
while True:
status = client.databases.status(database=DATABASE)
if status.data.infra.ready_for_ingestion:
print("Database ready.")
break
print("Waiting for database provisioning...")
time.sleep(3)
Database creation is asynchronous. Don't skip the readiness check. Ingestion calls fail if the database isn't fully provisioned.
Ingest documents and app sources as shared knowledge
Shared context goes in as type="knowledge", so any user of this assistant can retrieve it. The ADR is a plain text document. The meeting notes, Slack thread, and incident postmortem use HydraDB's App Sources so HydraDB receives the source-specific provider, kind, and fields that help it structure the graph. We'll write everything to a named shared collection so we can later query shared knowledge and persona-specific memories together.
HydraDB's current App Sources API uses singular kind values. In the examples below, meeting notes use kind="knowledge_base" with provider="some_internal_notes_provider", Slack threads use kind="message" with provider="slack", and incident/postmortem records use kind="ticket" with provider="jira".
# -- Synthetic source content (in production, these are disk files. ) --
adr_007 = """# ADR-007: Migrate from Payments API v1 to v2
**Status:** Accepted | **Date:** 2025-03-15 | **Team:** Payments
## Context
v1 payments API uses synchronous Stripe calls with per-request connection setup.
p99 latency exceeds 1200ms at 500 RPS. v2 introduces connection pooling, async
webhook handling, and batch settlement.
## Decision
Deprecate v1 by Q3 2025. All consuming services must migrate to v2.
Highest-priority consumers: billing-service, invoice-generator, checkout-service.
## Consequences
- checkout-service: update payment initiation calls (owner: frontend-platform)
- billing-service: migrate recurring charge logic (owner: payments)
- invoice-generator: switch settlement endpoints (owner: payments)
"""
meeting_notes = """# Architecture Review: Payments v1 Deprecation
**Date:** 2025-04-02 | **Attendees:** Sarah Chen (Payments Lead), Mike Torres (Platform),
Priya Patel (Billing), James Wright (SRE)
Sarah: ADR-007 is accepted. v1 sunset target is September 1. Billing-service is the
critical path - Priya, your team owns that migration.
Priya: We can start in May. Recurring charge logic is tightly coupled to v1's
synchronous response format. Estimate: 3-4 sprints.
Mike: checkout-service has a thinner integration. We can parallelize.
James: The March 12 incident (INC-2025-0312) was v1 connection exhaustion under load.
That postmortem recommended this migration. Link the timeline so on-call knows.
"""
postmortem = """# Incident Postmortem: INC-2025-0312
**Severity:** SEV-2 | **Duration:** 47 minutes | **Date:** 2025-03-12
**Service:** payments-api-v1 | **On-call:** James Wright (SRE)
## Summary
payments-api-v1 exhausted its Stripe connection pool under sustained load (>600 RPS),
causing cascading timeouts in billing-service and checkout-service. Customer-facing
payment failures lasted 47 minutes.
## Root Cause
v1 creates a new Stripe connection per request. At 600+ RPS, the connection pool
ceiling (default: 500) is exceeded. No circuit breaker existed.
## Resolution
Temporarily increased pool ceiling to 1000. Permanent fix: migrate to v2 API
which uses persistent connection pooling (see ADR-007).
"""
source_ids = []
# Ingest the ADR as a plain knowledge document
adr_result = client.context.ingest(
database=DATABASE,
collection=SHARED_COLLECTION,
type="knowledge",
documents=text_file("adr_007.md", adr_007),
document_metadata=json.dumps([
{
"id": "adr_007",
"title": "ADR-007: Payments API v1 to v2 Migration",
"additional_metadata": {"document_type": "adr", "service": "payments"},
}
]),
upsert="true",
)
source_ids.extend([r.id for r in adr_result.data.results])
print(f"Ingested ADR: {adr_result.data.results[0].id}")
# Model the Slack thread as individual message sources with a shared thread_id
slack_messages = [
{
"id": "slack_payments_eng_20250410_0914",
"external_id": "1744276440.000100",
"body": "Migration tracking spreadsheet is live. billing-service target: June 30. checkout-service target: July 15. invoice-generator target: August 1.",
"author": "sarah.chen",
"created_at": "2025-04-10T09:14:00Z",
},
{
"id": "slack_payments_eng_20250410_0932",
"external_id": "1744277520.000200",
"body": "Billing will need a feature flag for the cutover. We're calling it `use_payments_v2`. Can't do a hard switch mid-billing-cycle.",
"author": "priya.patel",
"created_at": "2025-04-10T09:32:00Z",
"parent_id": "1744276440.000100",
},
{
"id": "slack_payments_eng_20250410_1005",
"external_id": "1744279500.000300",
"body": "SRE will keep v1 monitoring active until all services confirm migration. Dashboard: go/payments-v1-deprecation",
"author": "james.wright",
"created_at": "2025-04-10T10:05:00Z",
"parent_id": "1744276440.000100",
},
{
"id": "slack_payments_eng_20250415_1422",
"external_id": "1744726920.000400",
"body": "checkout-service PR is up - only 3 call sites. Targeting merge by April 25. After that, billing is the only hard blocker.",
"author": "mike.torres",
"created_at": "2025-04-15T14:22:00Z",
"parent_id": "1744276440.000100",
},
]
thread_id = slack_messages[0]["external_id"]
# Ingest source-shaped records with App Sources
app_sources = [
{
"id": "meeting_notes",
"database": DATABASE,
"collection": SHARED_COLLECTION,
"title": "Architecture Review: Payments v1 Deprecation",
"type": "internal_notes",
"kind": "knowledge_base",
"provider": "some_internal_notes_provider",
"external_id": "arch-review-payments-v1-2025-04-02",
"timestamp": "2025-04-02T00:00:00Z",
"fields": {
"kind": "knowledge_base",
"title": "Architecture Review: Payments v1 Deprecation",
"body": meeting_notes,
"created_by": "sarah.chen",
"created_at": "2025-04-02T00:00:00Z",
},
"metadata": {"document_type": "meeting_notes", "service": "payments"},
},
*[
{
"id": message["id"],
"database": DATABASE,
"collection": SHARED_COLLECTION,
"title": "#payments-eng - v1 Deprecation Timeline",
"type": "slack",
"kind": "message",
"provider": "slack",
"external_id": message["external_id"],
"timestamp": message["created_at"],
"fields": {
"kind": "message",
"body": message["body"],
"author": message["author"],
"thread_id": thread_id,
"created_at": message["created_at"],
**({"parent_id": message["parent_id"]} if "parent_id" in message else {}),
},
"metadata": {"document_type": "slack_thread", "service": "payments", "channel": "payments-eng"},
}
for message in slack_messages
],
{
"id": "postmortem",
"database": DATABASE,
"collection": SHARED_COLLECTION,
"title": "Incident Postmortem: INC-2025-0312",
"type": "jira",
"kind": "ticket",
"provider": "jira",
"external_id": "INC-2025-0312",
"timestamp": "2025-03-12T00:00:00Z",
"fields": {
"kind": "ticket",
"title": "Incident Postmortem: INC-2025-0312",
"description": postmortem,
"status": "resolved",
"priority": "sev-2",
"assignee": "james.wright",
"created_at": "2025-03-12T00:00:00Z",
},
"metadata": {"document_type": "postmortem", "service": "payments", "incident_id": "INC-2025-0312"},
},
]
app_result = client.context.ingest(
database=DATABASE,
collection=SHARED_COLLECTION,
type="knowledge",
app_knowledge=json.dumps(app_sources),
upsert="true",
)
source_ids.extend([r.id for r in app_result.data.results])
print(f"Ingested {len(app_sources)} app sources")
Each source gets a stable id. The ADR uses document_metadata; App Sources use the top-level id, external_id, provider, and kind fields. We'll use the returned IDs to poll indexing status.
Add a service dependency graph with bring-your-own-graph
The service manifest describes a known, structured topology: which service depends on which API, and who owns it. Auto-extraction is model-driven and may not produce the exact dependency chain we need, so we use HydraDB's bring-your-own-graph (graph_payload) to declare the edges explicitly:
manifest_text = """# Service Dependency Manifest
- checkout-service (owner: frontend-platform) → payments-api-v1, payments-api-v2
- billing-service (owner: payments) → payments-api-v1, stripe-api
- invoice-generator (owner: payments) → payments-api-v1, billing-service
- auth-service (owner: platform) → rate-limit-config
- rate-limit-config → references payments-api-v1 routes
"""
MANIFEST_ID = "service_manifest"
manifest_graph = {
"entities": {
"checkout": {"name": "checkout-service", "type": "SERVICE", "namespace": "services"},
"billing": {"name": "billing-service", "type": "SERVICE", "namespace": "services"},
"invoice": {"name": "invoice-generator", "type": "SERVICE", "namespace": "services"},
"auth": {"name": "auth-service", "type": "SERVICE", "namespace": "services"},
"payments_v1": {"name": "payments-api-v1", "type": "API", "namespace": "apis"},
"payments_v2": {"name": "payments-api-v2", "type": "API", "namespace": "apis"},
"stripe_api": {"name": "stripe-api", "type": "API", "namespace": "apis"},
"rate_limit_config": {"name": "rate-limit-config", "type": "CONFIG", "namespace": "platform"},
"team_frontend": {"name": "frontend-platform", "type": "TEAM", "namespace": "teams"},
"team_payments": {"name": "payments", "type": "TEAM", "namespace": "teams"},
"team_platform": {"name": "platform", "type": "TEAM", "namespace": "teams"},
},
"relations": [
{"source": "checkout", "target": "payments_v1", "predicate": "DEPENDS_ON", "context": "checkout-service still calls payments-api-v1."},
{"source": "checkout", "target": "payments_v2", "predicate": "DEPENDS_ON", "context": "checkout-service has started integrating payments-api-v2."},
{"source": "billing", "target": "payments_v1", "predicate": "DEPENDS_ON", "context": "billing-service depends on payments-api-v1 for recurring charges."},
{"source": "billing", "target": "stripe_api", "predicate": "DEPENDS_ON", "context": "billing-service also calls stripe-api."},
{"source": "invoice", "target": "payments_v1", "predicate": "DEPENDS_ON", "context": "invoice-generator depends on payments-api-v1 settlement endpoints."},
{"source": "invoice", "target": "billing", "predicate": "DEPENDS_ON", "context": "invoice-generator depends on billing-service."},
{"source": "auth", "target": "rate_limit_config", "predicate": "DEPENDS_ON", "context": "auth-service references payment route rate-limit configuration."},
{"source": "rate_limit_config", "target": "payments_v1", "predicate": "REFERENCES", "context": "rate-limit-config references payments-api-v1 routes."},
{"source": "payments_v2", "target": "payments_v1", "predicate": "REPLACES", "context": "payments-api-v2 is the migration target for payments-api-v1 consumers."},
{"source": "checkout", "target": "team_frontend", "predicate": "OWNED_BY"},
{"source": "billing", "target": "team_payments", "predicate": "OWNED_BY"},
{"source": "invoice", "target": "team_payments", "predicate": "OWNED_BY"},
{"source": "auth", "target": "team_platform", "predicate": "OWNED_BY"},
],
}
graph_payload = {MANIFEST_ID: manifest_graph}
manifest_result = client.context.ingest(
database=DATABASE,
collection=SHARED_COLLECTION,
type="knowledge",
documents=text_file("service_manifest.md", manifest_text),
document_metadata=json.dumps([
{
"id": MANIFEST_ID,
"title": "service_manifest.md",
"additional_metadata": {"document_type": "manifest", "service": "payments"},
}
]),
graph_payload=json.dumps(graph_payload),
upsert="true",
)
source_ids.extend([r.id for r in manifest_result.data.results])
print(f"Ingested manifest with {len(manifest_graph['entities'])} entities, "
f"{len(manifest_graph['relations'])} relations")
When you provide a graph_payload, HydraDB uses your declared entities and relations for that source instead of relying on LLM extraction. The top-level graph_payload key must match the source id in document_metadata.
Limits are generous for service topology examples: 5,000 entities, 10,000 relations, 500 relations per entity, 2,000 characters per relation context, and 256 characters for names and predicates.
Poll indexing status before querying
Ingestion is asynchronous. Documents become searchable once they reach graph_creation status, but graph queries require completed status, which means the full context graph is built.
def wait_for_indexing(client, database, ids, collection=None, timeout=300):
start = time.time()
while time.time() - start < timeout:
status_kwargs = {"database": database, "ids": ids}
if collection is not None:
status_kwargs["collection"] = collection
status = client.context.status(**status_kwargs)
items = status.data.statuses
states = [s.indexing_status for s in items]
if all(s == "completed" for s in states):
print("All sources indexed and graph-ready.")
return
if any(s in ("errored", "failed") for s in states):
details = {
id_: getattr(item, "error_message", None) or getattr(item, "message", "")
for id_, item in zip(ids, items)
}
raise RuntimeError(f"Indexing failed: {dict(zip(ids, states))}; {details}")
print(f" Status: {dict(zip(ids, states))}")
time.sleep(5)
raise TimeoutError("Indexing did not complete within timeout.")
wait_for_indexing(client, DATABASE, source_ids, collection=SHARED_COLLECTION)
Small sources typically finish in a few minutes. You'll see statuses progress through queued → processing → graph_creation → completed.
Query with graph context off
With graph context disabled, vanilla hybrid retrieval returns:
query = "What would break if we deprecated the v1 payments API?"
result_no_graph = client.query(
database=DATABASE,
collection=SHARED_COLLECTION,
query=query,
type="knowledge",
query_by="hybrid",
mode="fast",
max_results=10,
graph_context=False,
query_apps=True,
)
chunks = result_no_graph.data.chunks or []
print(f"Chunks returned: {len(chunks)}\n")
for chunk in chunks[:5]:
title = chunk.source_title or chunk.id or "unknown source"
content = chunk.chunk_content or ""
print(f"[{title}] (score: {score_text(chunk.relevancy_score)})")
print(f" {content[:120]}...")
print()
You'll get relevant chunks: pieces of the ADR, the postmortem, maybe the meeting notes. But there's no connective tissue.
The LLM has to figure out on its own that the postmortem's root cause is the same API the ADR deprecates, that billing-service is both the highest-risk dependent and on the critical path, and that a timeline already exists in a Slack thread.
Query with graph context on
Same query, one flag changed:
result_with_graph = client.query(
database=DATABASE,
collection=SHARED_COLLECTION,
query=query,
type="knowledge",
query_by="hybrid",
graph_context=True,
query_apps=True,
mode="thinking", # multi-query expansion + richer graph traversal
max_results=10,
)
graph = result_with_graph.data.graph_context
chunks = result_with_graph.data.chunks or []
query_paths = graph.query_paths or [] if graph else []
chunk_relations = graph.chunk_relations or [] if graph else []
print(f"Chunks returned: {len(chunks)}")
print(f"Query paths: {len(query_paths)}")
print(f"Chunk relations: {len(chunk_relations)}")
The chunks are still ranked by relevance. But now you may also get query_paths (multi-hop relationship chains from the query to retrieved chunks) and chunk_relations (relationships between the returned chunks). Empty graph arrays are expected when no graph evidence matches the query.
Print the traversal evidence:
print("\n--- Query Paths ---")
for path in query_paths:
for triplet in path.triplets or []:
src = field(triplet.source, "name", "unknown")
rel = field(triplet.relation, "canonical_predicate", "RELATED_TO")
tgt = field(triplet.target, "name", "unknown")
print(f" {src} --[{rel}]--> {tgt}")
print(f" (relevancy: {score_text(path.relevancy_score)})\n")
Instead of disconnected passages, the output shows edges like billing-service → DEPENDS_ON → payments-api-v1, billing-service → OWNED_BY → payments, and payments-api-v2 → REPLACES → payments-api-v1.
The explicit graph we ingested with the manifest makes the service topology deterministic. Relationships extracted from the ADR, Slack thread, and postmortem are still model-derived.
Format retrieval results for LLM generation
The build_string helper from hydra_db.helpers flattens chunks, graph paths, chunk relations, and additional context into a single prompt-ready string:
context_string = build_string(result_with_graph)
# Optional: pass to OpenAI
if os.environ.get("OPENAI_API_KEY"):
from openai import OpenAI
llm = OpenAI()
response = llm.chat.completions.create(
model=os.getenv("OPENAI_MODEL", "gpt-4o"), # swap for your preferred model
messages=[
{
"role": "system",
"content": (
"You are an internal engineering assistant. Answer based only "
"on the provided context. If the context doesn't contain enough "
"information, say so. Cite the source document for each claim."
),
},
{
"role": "user",
"content": f"Context:\n{context_string}\n\nQuestion: {query}",
},
],
)
print(response.choices[0].message.content)
The LLM now has both the relevant text and the relationship structure. It can trace that billing-service depends on payments-api-v1, is owned by the payments team, has a 3-4 sprint migration estimate, and was affected by the incident that motivated the deprecation, all cited from specific sources. See the Chat Completions API reference for the full set of generation parameters.
Add per-user memory for personalized retrieval
Different roles need different answers to the same question. A backend engineer wants the migration mechanics: API contracts, feature flags, rollout sequence. An engineering manager wants the timeline, risk surface, and who's on point.
HydraDB handles this through memories, which are per-user context stored in persona-specific collections and retrieved alongside shared knowledge via type="all".
# Ingest persona-specific memories
engineer_memories = [
{"id": "eng_mem_1", "text": "I work on billing-service. My current sprint focuses on migrating the recurring charge logic from payments-api-v1 to v2.", "infer": False},
{"id": "eng_mem_2", "text": "The feature flag for our v2 cutover is `use_payments_v2`. We can't do a hard switch mid-billing-cycle.", "infer": False},
{"id": "eng_mem_3", "text": "I prefer seeing API request/response examples and code-level migration steps over high-level timelines.", "infer": False},
]
manager_memories = [
{"id": "mgr_mem_1", "text": "I manage the payments team. Priya Patel reports to me and owns the billing-service migration.", "infer": False},
{"id": "mgr_mem_2", "text": "My priorities are: shipping the v1 deprecation by September 1, managing risk to billing SLAs, and keeping stakeholders updated.", "infer": False},
{"id": "mgr_mem_3", "text": "I prefer timeline views, ownership maps, and risk assessments over implementation details.", "infer": False},
]
# Each persona gets its own collection
memory_ids_by_collection = {}
for collection, memories in [("backend_engineer", engineer_memories), ("eng_manager", manager_memories)]:
result = client.context.ingest(
database=DATABASE,
collection=collection,
type="memory",
memories=json.dumps(memories),
upsert="true",
)
memory_ids_by_collection[collection] = [r.id for r in result.data.results]
print(f"Ingested {len(memories)} memories for {collection}")
# Wait for memory indexing (usually seconds)
for collection, ids in memory_ids_by_collection.items():
wait_for_indexing(client, DATABASE, ids, collection=collection, timeout=120)
Now query shared knowledge and persona memory together:
for persona in ["backend_engineer", "eng_manager"]:
result = client.query(
database=DATABASE,
query=query,
type="all", # knowledge + memory, merged by relevancy
collections={SHARED_COLLECTION: 1.0, persona: 1.0},
graph_context=True,
query_apps=True,
query_by="hybrid",
mode="thinking",
max_results=10,
)
graph = result.data.graph_context
chunks = result.data.chunks or []
query_paths = graph.query_paths or [] if graph else []
context = build_string(result)
print(f"\n{'='*60}")
print(f"PERSONA: {persona}")
print(f"Chunks: {len(chunks)} | "
f"Paths: {len(query_paths)}")
print(f"{'='*60}")
# If you have an OpenAI key, generate the persona-specific answer:
if os.environ.get("OPENAI_API_KEY"):
from openai import OpenAI
llm = OpenAI()
response = llm.chat.completions.create(
model=os.getenv("OPENAI_MODEL", "gpt-4o"),
messages=[
{"role": "system", "content": "You are an internal engineering assistant. Answer based only on the provided context. Tailor the depth and focus to what's most relevant given the user's context."},
{"role": "user", "content": f"Context:\n{context}\n\nQuestion: {query}"},
],
)
print(response.choices[0].message.content)
type="all" queries knowledge and memory in parallel, merging everything by relevancy score into one ranked set. The collections object fans out across the shared knowledge collection and the persona's memory collection.
The backend engineer's result includes the same shared knowledge (the ADR, the manifest, the postmortem), but their memory about working on billing-service and the use_payments_v2 feature flag can surface alongside it. The engineering manager sees the same shared facts, but their memory about owning the timeline and managing risk shifts the emphasis.
The mechanism is scoped retrieval: shared knowledge plus relevant per-user context, ranked together. Irrelevant memories score low and drop out.
Cleanup
# Delete the tutorial database when you're done
client.databases.delete(database=DATABASE)
print("Tutorial database deleted.")
This is permanent. If you want to keep experimenting, skip this step.
From document fragments to a company brain
You started with five disconnected engineering documents and turned them into a company brain that maps relationships between services, teams, decisions, and incidents. The same question, "what breaks if we deprecate v1?", went from returning relevant fragments to returning connected evidence with traversal paths.
From here, swap in your actual ADRs, runbooks, and service catalogs. Use graph_payload for any structured topology you already maintain. Add more persona collections, since memories accumulate over sessions and HydraDB merges them at query time. For a production example of this pattern with real company docs, see the AI Onboarding Agent cookbook. To wrap the query-and-generate pattern in a tool call with a FastAPI backend, see Cursor for Docs.
Vector search gets you relevant text. Graph context lets your agent follow the thread from an incident to the API it exposed to the team that owns the migration.
Sign up for HydraDB to start building, or grab the full working code for this tutorial and run it end to end.
Frequently asked questions
What is the difference between vector search and graph context for AI agents?
Vector search finds text chunks that are semantically similar to a query. Graph context adds relationship evidence on top: which services depend on which APIs, who owns them, and how documents relate to each other. Vector search returns a reading list. Graph context returns a connected map the LLM can reason over.
Why does my RAG pipeline return disconnected results?
Standard retrieval-augmented generation (RAG) ranks chunks by similarity to the query, but it has no way to link them. A postmortem and an ADR might both score high for the same question without any signal that they describe the same API, the same incident, or the same team. Adding graph context surfaces those connections automatically.
What is bring-your-own-graph in HydraDB?
Bring-your-own-graph lets you declare entities and relationships explicitly via a graph_payload instead of relying on LLM extraction. Use it for structured data you already maintain, like service dependency manifests, org charts, or infrastructure topology. HydraDB stores your declared edges alongside auto-extracted ones and uses both at query time.
How do I personalize AI agent responses for different users?
Store per-user context as memories in persona-specific collections. At query time, pass the user's collection alongside the shared knowledge collection with type="all". HydraDB merges shared knowledge and personal memories by relevancy score, so a backend engineer and an engineering manager get different emphasis from the same underlying facts.
Can I use HydraDB with any LLM?
Yes. HydraDB handles ingestion, indexing, and retrieval. The build_string helper formats the results (chunks, graph paths, and chunk relations) into a single prompt-ready string you can pass to any LLM via its chat completions API. The tutorial uses OpenAI as an example, but any model that accepts a context string works.
How long does it take to index documents in HydraDB?
Small sources typically finish in a few minutes, progressing through queued, processing, graph_creation, and completed statuses. Sources become searchable at graph_creation, but graph queries require completed status. Poll with client.context.status() before querying.
What types of documents can I ingest into HydraDB?
HydraDB accepts text-based documents (Markdown, plain text, structured data) as shared knowledge via type="knowledge". You can also directly connect internal tools (like Slack, Jira, or Notion) using App Sources to automatically ingest knowledge bases, tickets, and messages. For structured topologies like service manifests or dependency graphs, use graph_payload to declare entities and relationships explicitly. Per-user context goes in as type="memory" in persona-specific collections.
What is a company brain for AI agents?
A company brain is a connected knowledge system that lets an AI agent search internal documents, messages, tickets, service data, and user context. It returns relevant information and the relationships between sources.
How do you build a company brain for an AI agent?
Ingest company knowledge into a retrieval system, organize shared and user-specific data, add relationships between entities, and query the combined context. This tutorial uses HydraDB for ingestion, graph context, retrieval, and memory.
What is a Perplexity-style company brain?
It is an internal AI search experience that answers questions using company data instead of the public web. It retrieves evidence from multiple sources, connects related facts, and gives the AI agent structured context for its answer.
How can an AI agent search Slack, Jira, and internal documents?
Connect each source through document ingestion or App Sources. Store Slack messages, Jira tickets, meeting notes, architecture records, and other files in the same searchable knowledge collection.
Why is vector search not enough for a company brain?
Vector search finds text that is similar to a query, but it does not reliably show how the results connect. Graph context can link services, APIs, teams, incidents, decisions, and owners.
What is the difference between a company brain and standard RAG?
Standard RAG usually retrieves relevant text chunks. A company brain also adds relationships, shared knowledge, structured topology, and user-specific memory to help the AI agent produce a more complete answer.
How does graph context improve AI agent answers?
Graph context adds relationship evidence to retrieved text. It can show which service depends on an API, which team owns the service, which incident affected it, and which migration plan addresses the issue.
Can a company brain personalize answers for different employees?
Yes. Store user or role context in separate memory collections and query it with shared company knowledge. The same question can then emphasize implementation details for an engineer or timelines and ownership for a manager.
Top comments (0)