A technical deep dive for developers building multi-source AI context systems.
TL;DR
Meeting Lie Detector is a Python application on HydraDB that detects when workplace tools disagree about decisions—especially who owns what after a meeting. It ingests Linear, Slack, Notion, and real documents (PDF/Markdown), then answers multi-hop questions using staged retrieval (preferring HydraDB fast mode) while measuring latency, call count, and accuracy.
If you are building agents that “read the company brain,” this is a blueprint for cross-source truth, not single-index RAG.
The problem (beyond “RAG”)
Vector search finds similar text. It does not natively answer:
On the Phoenix work tracked as Linear PHX-42 around Tuesday, who did the PDF say owned auth, who claimed it in Slack within two hours, and does Notion still show the old owner?
That question needs:
- Temporal ordering (before / during / after the meeting)
-
Entity identity (
Alice Chen=@alice=alice-chen) - Knowledge updates (Notion page v1 vs v2)
- Multi-hop stitching (Linear → PDF → Slack → Notion)
- Actor-based filtering (what Bob said vs what the template still defaults to)
Teams usually paper over this with more Notion fields. Agents need a context substrate that can retrieve across connectors with metadata and graph structure. That’s HydraDB’s job; Meeting Lie Detector is the productized workflow on top.
Product shape
| Layer | Role |
|---|---|
| Connectors / files | Slack, Notion, Linear, documents |
| HydraDB | Isolated database, ingest, hybrid query, graph, connector sync |
| Multi-hop planner | Multiple targeted queries, not one mega-prompt |
| Synthesizer | Deterministic extractive answers (swap for LLM later) |
| Eval harness | Expected vs actual, latency, cost estimates |
Architecture
Why dual ingest?
-
Showcase path — committed sample data under
data/so anyone can reproduce the ownership-lie story without OAuth. - Native path — HydraDB Connectors API so real workspaces can sync live channels, pages, and calendars.
Both land in the same HydraDB database and are queryable with query_apps=True and metadata filters.
Data model (the sample world)
People intentionally collide as aliases:
- Alice Chen /
alice.chen@company.com/@alice/alice-chen/ “Alice C.” - Bob Martinez /
@bob/bob-m - Carol Nguyen /
@carol - David Kim /
@dkim
Projects: Phoenix, Orion, Mobile Redesign.
The critical timeline:
2026-06-18 Decision doc: Alice sole auth owner
2026-07-29 10:00 PT Project Sync / Linear PHX-42 (Alice assignee)
2026-07-29 11:05 PT Official PDF still lists Alice
2026-07-29 11:42 PT Slack: Bob claims eng ownership
2026-07-29 12:30 PT Post-meeting summary: Bob eng owner
2026-07-31 16:20 PT Notion Action Items updated → Bob
(template page never updated — still defaults Alice)
That is not a toy “hello world” index. It is a deliberate contradiction graph.
Ingestion: documents + app knowledge
Creating the database
from hydra_db import HydraDB
import time, os
client = HydraDB(token=os.environ["HYDRA_DB_API_KEY"])
database = "meeting_lie_detector"
client.databases.create(
database=database,
database_metadata_schema=[
{"name": "source", "data_type": "VARCHAR", "enable_match": True, "max_length": 64},
{"name": "type", "data_type": "VARCHAR", "enable_match": True, "max_length": 64},
{"name": "project", "data_type": "VARCHAR", "enable_match": True, "max_length": 64},
{"name": "date", "data_type": "VARCHAR", "enable_match": True, "max_length": 32},
{"name": "channel", "data_type": "VARCHAR", "enable_match": True, "max_length": 64},
],
)
while not client.databases.status(database=database).data.infra.ready_for_ingestion:
time.sleep(5)
Metadata fields like source and project power fast filters—critical for “Slack only, phoenix only” hops without paying for thinking mode.
App knowledge (connector-shaped records)
import json
app_item = {
"id": "slack_thread_after_sync_20260729",
"title": "Slack #phoenix-eng: Bob claims auth rewrite ownership",
"type": "slack",
"provider": "slack",
"kind": "message",
"timestamp": "2026-07-29T11:42:18-07:00",
"url": "https://company.slack.com/archives/C0PHXENG01/p...",
"content": {"text": open("data/slack/thread-after-sync-ownership-2026-07-29.txt").read()},
"tenant_metadata": {
"source": "slack",
"type": "thread",
"project": "phoenix",
"date": "2026-07-29",
"channel": "phoenix-eng",
},
"additional_metadata": {
"actors": ["Bob Martinez", "Alice Chen", "Carol Nguyen", "David Kim"],
"connector_label": "slack",
},
"relations": {
"ids": ["cal_evt_phoenix_sync_20260729", "doc_meeting_notes_20260729"],
"properties": {"reason": "cross_source_link"},
},
}
client.context.ingest(
type="knowledge",
database=database,
upsert=True,
app_knowledge=json.dumps([app_item]),
)
Real document upload (PDF)
with open("data/documents/meeting-notes-2026-07-29.pdf", "rb") as f:
client.context.ingest(
type="knowledge",
database=database,
upsert=True,
documents=[("meeting-notes-2026-07-29.pdf", f, "application/pdf")],
document_metadata=json.dumps([{"id": "doc_meeting_notes_20260729"}]),
metadata={"source": "document", "type": "meeting_notes", "project": "phoenix", "date": "2026-07-29"},
additional_metadata={"actors": ["Alice Chen", "Bob Martinez", "Carol Nguyen"]},
)
Always poll client.context.status until indexing_status is graph_creation or completed before querying.
Native connectors (live Slack / Notion / Linear)
HydraDB exposes a first-class Connectors API. The app wraps it in mld/native_connectors.py and connectors_setup.py:
list providers → create(provider, credentials, database)
→ discover resources → configure(resources)
→ sync → poll status
# High-level (see mld/native_connectors.py)
from mld.native_connectors import setup_provider
from mld.client import get_client
client = get_client()
setup_provider(client, "slack", database="meeting_lie_detector", lookback_days=90)
Credentials stay in environment variables (SLACK_BOT_TOKEN, NOTION_API_KEY, LINEAR_API_KEY)—never in the repo.
Multi-hop retrieval: the real product logic
Hackathon-style demos often do one query() and hope. This app uses explicit hop plans.
# mld/retrieval.py (simplified)
@dataclass
class QueryHop:
name: str
query: str
mode: str = "fast"
metadata_filters: dict | None = None
max_results: int = 6
graph_context: bool = False
# Flagship plan hops:
# 1. calendar + filters source=linear, project=phoenix
# 2. documents (official notes)
# 3. slack ownership claim
# 4. notion action items (recency_bias)
# 5. synthesis (fast or thinking)
Evaluation runs those hops in order:
# evaluate.py — core loop (conceptual)
plan = plan_for(question.id, prefer_mode=mode)
all_chunks, sources = [], []
for hop in plan.hops:
hop_mode = "fast" if mode == "fast" else (
"thinking" if hop.name == "synthesis" else "fast"
)
result = query_knowledge(
client,
hop.query,
mode=hop_mode,
metadata_filters=hop.metadata_filters,
graph_context=hop.graph_context or hop_mode == "thinking",
query_apps=True,
)
all_chunks += extract_chunk_texts(result)
sources += extract_source_titles(result)
answer = synthesize_answer(question.question, all_chunks, mode)
Fast vs thinking
| Mode | When used | Tradeoff |
|---|---|---|
| fast | Almost every hop; filter-heavy questions | Low latency, lower graph expansion |
| thinking | Optional final synthesis | Higher quality / relations, higher latency & cost |
Estimated cost constants live in mld/config.py for competition-style reporting (illustrative, not billing).
Answer synthesis
The default synthesizer is extractive and deterministic—no external LLM—so offline and online scoring is reproducible:
- Split retrieved chunks into sentences
- Rank by ownership / actor / question keyword hits
- Emit top sentences as the answer
You can replace this with HydraDB’s build_string(result) + any chat model without changing ingest.
from hydra_db.helpers import build_string
# context = build_string(result)
# → feed to your LLM with “answer only from context”
Optional heuristic lie signals live in mld/detect.py (e.g. Alice and Bob both appear as owners → ownership contradiction).
Evaluation harness
Twelve questions cover:
- Multi-hop calendar/doc/slack/notion
- Temporal knowledge updates
- Entity aliases
- Thread / actor queries
- Metadata-filter fast path
- Multilingual (Spanish question over English corpus)
- Third-party attribution
python evaluate.py --mode both
python evaluate_offline.py # no API key; validates corpus
python demo.py # flagship Q1 live
Artifacts under results/:
- Per-question hop logs (modes, latency, sources)
- Accuracy / partial scores
- Aggregate fast-mode success rate
Project layout for contributors
meeting-lie-detector/
├── data/ # Sample multi-source world
├── mld/
│ ├── catalog.py # Source registry + relations
│ ├── client.py # HydraDB helpers
│ ├── native_connectors.py
│ ├── retrieval.py # Multi-hop plans
│ ├── questions.py # Eval set
│ └── detect.py # Contradiction heuristics
├── ingest.py
├── connectors_setup.py
├── evaluate.py
├── evaluate_offline.py
├── demo.py
├── requirements.txt
└── .env.example
Operational tips
- Plan metadata schema before first ingest — filter fields are declared at database create.
- Stable IDs + upsert — re-ingest safely after editing sample files.
- Wait for indexing — querying too early looks like “HydraDB failed.”
-
Prefer filters over thinking —
project=phoenix+source=slackis free precision. - Forceful relations — link calendar ↔ thread ↔ PDF so thinking mode can expand.
-
Keep secrets out of git —
.envis ignored; use.env.exampleonly.
What you can build next
- A UI timeline of “decision → claim → doc lag → wiki fix”
- Continuous monitoring agent that alerts when Slack owner ≠ Notion owner
- Same multi-hop pattern for incidents (PagerDuty + Slack + Jira + runbooks)
- Per-user
collectionmemories for “how this manager prefers ownership reported”
HydraDB gives you the context substrate; Meeting Lie Detector shows a full vertical: ingest → multi-hop query → measure.
Most AI workplace tools answer from one pane of glass. Real organizations decide in meetings and diverge across tools. Meeting Lie Detector encodes that failure mode as data and retrieval—and uses HydraDB so the graph, metadata, and hybrid search do the heavy lifting.
Code & more: https://www.dailybuild.xyz/project/215-meeting-lie-detector

Top comments (0)