ChronoGraph
ChronoGraph is a temporal causal retrieval engine that answers "why" questions by combining vector retrieval, knowledge graphs, and time-aware graph traversal.
| Graph view | Edge explainability | Replay |
|---|---|---|
▶ Watch the narrated demo (56s)
It ingests incident timelines, builds a causal knowledge graph, and traces multi-hop cause-effect chains under temporal-consistency constraints — surfacing which upstream event actually caused this outcome, and why it's the cause and not a lexical coincidence.
Framed broadly: ChronoGraph is a reusable temporal causal retrieval layer. Root-cause analysis is its first application, but the same graph + retrieval + scoring stack applies to attack-chain analysis, fraud investigation, manufacturing fault diagnosis, and any "which upstream event caused this outcome?" domain.
Why this is interesting
Naive RAG retrieves the most textually similar chunks. That fails root-cause analysis, where the answer is governed by causality and time, not lexical similarity:
- Two incidents can mention the same service with different root causes on different dates. Checkout failed in January because of a bad deploy; it failed in March because of catalog-search degradation. "Checkout" alone cannot tell them apart.
- The correct root cause is the deepest temporally-consistent cause, not the most similar sentence.
- A causal answer is only trustworthy if every edge carries evidence, confidence, and a timestamp.
Why plain GraphRAG fails
Question: "Why was checkout failing in March?"
GraphRAG ChronoGraph
──────── ──────────
Find "checkout" Find "checkout"
↓ ↓
Neighbor expansion Filter by time (March)
↓ ↓
Deployment March incident
↓ ↓
Payment Catalog search degradation
↓ ↓
Cache Correct root cause
↓
Returns the JANUARY incident ✔ time disambiguates
✘ time is ignored
Text-graph expansion ignores temporality, so the most-connected neighbors (from an older, high-profile incident) dominate. ChronoGraph treats time as a first-class retrieval constraint: causal hops must obey cause-before-effect, and a query's explicit date actively down-weights paths rooted on other dates.
Who this helps
ChronoGraph is a building block, not a product — teams drop it into their stack to answer "why" questions that plain search/LLM pipelines get wrong:
- SRE / incident postmortems — "Why did checkout fail in March?" across deploy, config, and dependency logs; answer arrives as an evidence-traced causal chain, not a similarity-ranked blob.
- On-call triage — the same error message on different dates often has different root causes; time-aware ranking surfaces the current cause instead of the most-famous one.
- Release engineering — correlate deploy/config-change events to downstream service degradation and pin the responsible change.
- Support engineering — map user-reported symptoms to known incident patterns and route tickets by traced root cause.
- Data/ML platforms — replace brittle regex or lookup tables with a queryable temporal-causal layer over incident data.
- Evaluations / observability tooling —
chronograph benchmarkis a drop-in regression gate (asserts ≥90% root-cause accuracy and that the graph engine beats vector-only RAG).
Because everything is optional (heuristic LLM, in-memory graph/vector by default), it runs fully offline with zero API keys, which makes it usable in CI, air-gapped environments, and demos alike.
The architecture differs from typical GraphRAG:
Typical GraphRAG ChronoGraph
───────────── ─────────────
Question Question
↓ ↓
Vector Search Entity Linking
↓ ↓
Graph Expansion Temporal Filtering
↓ ↓
LLM Causal Traversal
↓
Evidence Ranking
↓
LLM Explanation
Evaluation
make benchmark (or python -m chronograph.evals.run_eval)
| Metric | TF-IDF RAG | ChronoGraph |
|---|---|---|
| Root-cause accuracy | 53% | 100% |
| Avg path length | — | 1.2 |
| Avg confidence | — | 0.70 |
| Avg latency | 0.9 ms | 5.0 ms |
| Evidence trace | ❌ | ✅ |
| Explainable edges | ❌ | ✅ |
- Corpus: 16 synthetic incidents (Jan–Jun 2024) with manually labeled root causes and 32 cross-incident why-questions, built around four deliberate distractor patterns:
- exact-date disambiguation — same service, different dates, different causes (checkout failing Jan 10 vs Feb 5 vs Apr 22);
- month-only dates — "in March" vs "in February";
- recency cues — "right now", "these days", "recently";
- unique-vocabulary — "after the v3.1.0 deploy", "coupon campaign", "queue".
- Labels are excluded from the retrievable index, so the baseline cannot read the answer — it must answer from incident text alone.
- Hit = the expected root-cause label appears in the traced root-cause candidate or answer.
- The report prints the questions where the engines differ:
* Why was checkout failing in March?
expected root cause : catalog search
baseline retrieved : INCIDENT: INC-2024-0101 TITLE: checkout errors spike after payment-service deploy
causal traced : event-ev-0308-1 --[CAUSED_BY]--> event-catalog-search-degradation-...
The baseline fails because "checkout" alone matches the January incident; only time-aware reasoning disambiguates.
Scope note: 100% is on this included demo corpus, not a production dataset. Extending the benchmark (more incidents, harder distractors, real-world logs) is the primary next step — see Roadmap.
Path scoring
ChronoGraph selects the answer path with an explicit weighted formula (weights in chronograph/config.py, tunable via .env):
Score(path) =
0.40 × SemanticRelevance
+ 0.25 × CausalConfidence
+ 0.15 × TemporalConsistency
+ 0.10 × Recency
− 0.10 × LengthPenalty
| Term | Weight | Meaning |
|---|---|---|
SemanticRelevance |
0.40 | Query-to-path token overlap via a shared stemmed tokenizer — weighted 0.2 × root-overlap + 0.5 × hop/evidence-overlap + 0.3 × anchor-overlap, so hop evidence snippets count more than the root label |
CausalConfidence |
0.25 | Mean hop confidence (relation weight × temporal order) |
TemporalConsistency |
0.15 | Mean cause-before-effect agreement; gated to 0.4 when the query's explicit date mismatches the root cause's date |
Recency |
0.10 | Prefers root causes closer to the present — applied only when the query carries a recency cue ("right now", "these days", "recently", …) |
LengthPenalty |
0.10 | Normalized path length, discouraging long speculative chains |
The temporal and recency terms are the crux: when a query names a date ("February 25"), any path whose root cause happened on another date is heavily down-weighted regardless of lexical similarity; when a query has no date but says "right now", recency breaks the tie between same-symptom incidents across dates.
Complexity
| Stage | Complexity |
|---|---|
| Entity linking (TF-IDF scoring) | O(n log n) |
| Temporal graph traversal (BFS, depth-bounded) | O(V + E) |
| Path ranking | O(P) |
| Memory | O(V + E) |
Architecture
Two retrieval systems run in parallel and converge on candidate entities before temporal traversal — that's what makes this hybrid, not a pipeline:
Query
│
┌──────────────┴──────────────┐
│ │
Vector Retrieval Entity Linking
(TF-IDF / Qdrant) (graph neighborhood)
│ │
└──────────────┬──────────────┘
│
Candidate Entities
│
Temporal Graph Traversal
(cause ← effect, look-back)
│
Path Scoring Engine
(α·sem · β·causal · γ·temp − δ·len)
│
Evidence Aggregator
│
LLM Explanation
Components
| Module | Responsibility |
|---|---|
graph/schema.py |
Entity types + relation taxonomy (SYMPTOM_OF, CAUSED_BY, TRIGGERED_BY, INTRODUCED_BY, AFFECTS, …) |
graph/builder.py |
Turns incident timelines into a causal graph; time-ordered cause linking, trigger detection |
graph/memory.py, graph/neo4j_store.py |
Swap-in graph backends behind one GraphStore interface |
retrieval/entity_linker.py |
Hybrid anchor selection (vector hits + incident-event entities from chunk hits + graph neighborhood) with weak-hit thresholding |
retrieval/causal_path.py |
Multi-hop causal traversal, prunes hops that violate cause-before-effect, recency look-back window |
reasoning/engine.py |
Query routing (causal vs simple), weighted path scoring (α/β/γ/δ/ε), recency-cue gating, answer synthesis |
llm/heuristic_client.py, llm/openai_client.py |
Deterministic offline ranking/explanation, or OpenAI-backed |
vector/memory.py, vector/qdrant_store.py |
TF-IDF in-memory or Qdrant (sentence-transformers) |
utils/text.py |
Shared tokenizer (stemming, digits, stopword variants) used by linking, ranking, and path relevance |
evals/ |
16-incident corpus + 32-question benchmark + NaiveRAG baseline |
Key design decisions
- Time-aware traversal. Every causal hop checks the effect precedes its cause; edges that point "backwards in time" are pruned. A look-back window (
time_window_hours=48) bounds how far back a cause can reach. - Date-anchored scoring. When a query names a date, paths rooted on other dates are down-weighted — the key disambiguator for same-service incidents.
- Recency-cue gating. Recency only enters the score when the query says "right now", "these days", "recently", etc. — a date-anchored query is never beaten by recency, and a recency query is never beaten by a date gate.
- Anchor-aware relevance. Paths are scored against the originating linked entity plus the path surface — stemming + stopword-filtered token overlap, with hop evidence snippets weighted highest.
- Auditable edges. Each hop carries relation type, confidence, timestamp, temporal consistency, evidence snippets, and a rationale.
- Honest evaluation. Ground-truth labels are excluded from the retrieval index; the baseline must answer from text alone. Results are reported per-question.
- Pluggable backends. Graph: Neo4j (docker) or in-memory. Vector: Qdrant (docker) or TF-IDF. LLM: OpenAI or deterministic heuristic — the demo runs fully offline with zero external keys.
Quickstart
Install as a library (no cloning)
# published to PyPI (once the first release is tagged):
pip install whygraph
# or, from the repo directly (always works):
pip install git+https://github.com/HASSANFARYAD/ChronoGraph.git
That gives you a chronograph command globally, plus an importable package:
# answer a 'why' question (auto-loads demo incidents with --demo)
chronograph ask "Why was checkout failing in March?" --demo
# ingest your own incidents from JSON, then ask questions against them
chronograph ingest incidents.json
chronograph ask "Why did payments slow down on February 25?"
# regression gate for CI / pre-commit (exits non-zero on regressions)
chronograph benchmark
# start the REST API + Swagger at http://localhost:8000/docs
chronograph api
Auto-discover incidents from logs (no AI)
Point it at a log file or directory and ChronoGraph parses timestamped lines (ISO, Python-logging, syslog), clusters them into incidents per service + time window, and builds the causal graph — so "find the incidents from these logs" needs zero configuration:
chronograph sniff ./logs --ask "Why was checkout slow?"
# Found 3 log file(s):
# ./logs/checkout.log (214 timestamped lines)
# Clustered 7 incident(s):
# INC-CHECKOUT-SERVICE-20240225-095800 ...
# Ingested into graph: {'nodes': 132, 'edges': 145}
# Answer: checkout slow because connection pool exhausted behind payment-service deploy
Also available as a REST endpoint for other services to call:
POST /logs/sniff with {"path": "/var/log/myapp", "window_seconds": 600}.
Or use it as a library in your own code:
from chronograph.factory import build_engine, ensure_demo_data
engine = build_engine() # picks engines from env; works fully offline by default
ensure_demo_data(engine) # optional demo data for an empty graph
result = engine.answer("Why was checkout failing in March?")
print(result.answer, result.confidence)
Optional engines are opt-in extras so the core stays light:
pip install "whygraph[all]" # everything below
pip install "whygraph[neo4j]" # Neo4j graph backend
pip install "whygraph[qdrant]" # Qdrant vector backend
pip install "whygraph[llm]" # OpenAI LLM client
One-command (Docker)
docker compose up --build
# backend → http://localhost:8000/docs
# frontend → http://localhost:5173
Local development
# backend
python -m venv .venv
.venv\Scripts\activate # Windows
pip install -r requirements.txt
# run the benchmark (no keys, no infra required)
make benchmark
# or: python -m chronograph.evals.run_eval
# start the API (auto-ingests demo incidents)
uvicorn chronograph.api.main:app --reload
# frontend (needs Node 18+)
cd frontend
npm install
npm run dev
Open http://localhost:5173, pick a sample question, then:
- watch the engine highlight the traced causal path in the graph view;
- hit Replay causal timeline to animate the chain hop-by-hop (symptom → cause → root cause);
- click any highlighted edge to inspect its evidence, confidence, timestamp, and rationale.
Running with Neo4j + Qdrant + OpenAI
docker compose up -d neo4j qdrant # just the stores
cp .env.example .env # set OPENAI_API_KEY, then:
LLM_PROVIDER=openai
GRAPH_ENGINE=neo4j
VECTOR_ENGINE=qdrant
pip install "whygraph[all]" # or the individual [neo4j]/[qdrant]/[llm] extras
API
| Endpoint | Description |
|---|---|
GET /health |
Liveness + active engines |
POST /ingest |
Ingest Incident[] payloads (or {"load_demo": true}) |
POST /logs/sniff |
{"path": "<log file or dir>", "window_seconds": 600} — auto-discover incidents from logs and ingest them |
GET /graph/stats |
Node/edge counts |
GET /graph?center=<entity_id>&depth=2 |
Export a subgraph for visualization |
POST /query |
{"question": "Why were orders timing out on January 15?"} → answer, best path (hops with confidence/time/temporal-consistency/evidence), candidate paths, confidence, latency |
GET /evals/run |
Re-run the evaluation on the live engine |
Publishing to PyPI
The CI workflow .github/workflows/publish.yml publishes on tag pushes (v*) using trusted publishing (no hard-coded token). The package is distributed on PyPI as whygraph (the chronograph name was taken by an unrelated project; the import package stays chronograph).
git tag v0.1.0
git push origin v0.1.0
First time only, connect the repo to PyPI:
- Create an account at https://pypi.org and add the
pypienvironment. - In PyPI → Account settings → Publishing → add a new trusted publisher:
- Project name:
whygraph - Owner:
HASSANFARYAD· Repo:ChronoGraph· Workflow:publish.yml
- Project name:
- The workflow's
environment: pypijob will authenticate automatically on the next tag push and create the project on first publish.
Local build + upload (alternative, needs a PyPI token):
pip install build twine
make build # or: python -m build
make release # or: twine upload dist/*
Roadmap
Near-term:
- Expand the benchmark further beyond the demo corpus (this release grew it from 6/10 to 16 incidents/32 questions; next: real or public log datasets).
- Publish against existing GraphRAG baselines (Microsoft GraphRAG-style, community detection, hybrid vector+KG) in addition to TF-IDF.
- Domain adapters — observability alerts, GitHub Issues, PagerDuty, Jira, Kubernetes, CI/CD, CloudTrail — as ingestion plugins.
Research directions:
- Temporal shortest-path algorithms over the causal graph
- Probabilistic edge weighting from historical incident frequency
- Bayesian confidence propagation across multi-hop chains
- Streaming graph updates + incremental graph indexing
- Online graph learning from postmortem corrections
- Ragas/DeepEval-style metrics (faithfulness, evidence grounding) + a CI eval job
References & prior art
Inspired by and builds on:
- Microsoft GraphRAG — combining knowledge graphs with LLM retrieval
- Retrieval-Augmented Generation (Lewis et al., 2020)
- Causal inference — the potential-outcomes / causal-graph framing (Pearl)
- Temporal knowledge graphs — time-stamped facts and TKGC methods
- Graph neural networks — representation learning over relational data
- Neo4j — graph database and Cypher traversal
- Qdrant — vector similarity search
Project layout
chronograph/
api/ FastAPI app (health, ingest, logs/sniff, query, graph, evals)
evals/ dataset + benchmark harness + NaiveRAG baseline
graph/ schema, graph builder, memory + Neo4j stores
llm/ OpenAI + heuristic (offline) clients
logs/ log discovery + parsing + incident clustering (sniff)
reasoning/ CausalRAGEngine, query routing, weighted path scoring
retrieval/ entity linking + time-aware causal path finding
utils/ shared text tokenizer (stemming/stopwords) + timestamp / temporal-ordering helpers
vector/ TF-IDF memory store + Qdrant store
frontend/ React + @xyflow/react demo (graph viz, replay, edge explainability)
tests/ pytest suite (log sniffer, tokenizer, entity linker, eval gate)
Metadata
Release files for whygraph 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| whygraph-0.1.0.tar.gz | 52.7 kB | With provenance | Details for whygraph-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| whygraph-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for whygraph-0.1.0-py3-none-any.whl |
Total release size: 105.5 kB
Release files / whygraph-0.1.0.tar.gz
| Download URL | whygraph-0.1.0.tar.gz |
|---|---|
| Size | 52.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
52e196a8c74220da5ba4b03a3cb3d88731cd8a611d7b755b1e51782f548f4688
|
|
BLAKE2b-256 checksum How to use checksums |
2e299933a8326da05859301b06a6b01d2e30a0ab17485d17c6c617c5fafc561f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 5, 2026.
Transparency logRelease files / whygraph-0.1.0-py3-none-any.whl
| Download URL | whygraph-0.1.0-py3-none-any.whl |
|---|---|
| Size | 52.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d2677a20511d970f9dffa01d62694a1c14369a58646883d9b5e247fdd2050907
|
|
BLAKE2b-256 checksum How to use checksums |
81a5235221b13e6f74e361129c3988938b86ca861bc7a452a8a2fa52a8284ebf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 5, 2026.
Transparency logSecurity for whygraph 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for whygraph-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 5, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.