Skip to main content

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
Graph view Edge inspection 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 benchmark is 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.


Distributed trace correlation

ChronoGraph treats distributed-trace context as first-class causal signal, so root-cause chains can traverse spans across both events and incidents:

  • Capture — chronograph sniff / chronograph watch extract trace_id/span_id from log lines (W3C traceparent headers, dd.* JSON fields, or plain text), and every TimelineEvent carries them through clustering and ingestion.
  • Persist — trace context is stored as properties on EVENT graph nodes, not a side registry, so chains survive restarts and work identically on the in-memory and Neo4j stores.
  • Within an incident — same-trace events are chained with TRIGGERED_BY in time order.
  • Across incidents — distributed traces whose spans land in different per-service incidents (e.g. api-gateway → checkout-service → payment-service) are stitched into one request chain by a graph-driven stitcher, and the resulting chain is emitted once (idempotent re-ingestion).
  • Incident-safe identity — EVENT graph nodes are namespaced by incident id, so identical event ids in different incidents never collide or overwrite each other's trace context.
Why did payment timeout after the gateway routed the request?
  event-ev-py (payment-service) --[TRIGGERED_BY]--> event-ev-gw (api-gateway)
                                                      ^ cross-incident trace stitch

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, and a cross-incident trace stitcher that chains distributed-trace spans across per-service incidents
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, incremental vector re-indexing on ingest
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), with batched upsert_many writes
logs/sniffer.py Log discovery, parsing, trace/span extraction, incident clustering
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

  1. 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.
  2. Date-anchored scoring. When a query names a date, paths rooted on other dates are down-weighted — the key disambiguator for same-service incidents.
  3. 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.
  4. 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.
  5. Auditable edges. Each hop carries relation type, confidence, timestamp, temporal consistency, evidence snippets, and a rationale.
  6. Honest evaluation. Ground-truth labels are excluded from the retrieval index; the baseline must answer from text alone. Results are reported per-question.
  7. 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 (v0.1.x+ live):
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

# automation: tail logs, auto-ingest incidents, auto-answer root cause,
# and POST each new incident + traced causal answer to a webhook
chronograph watch ./logs --webhook https://hooks.slack.com/services/...

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}.

Automate incident response (chronograph watch)

watch runs the whole detection → explanation → delivery loop unattended. It tails log files, clusters new timestamped lines into incidents, ingests them into the graph, auto-builds a date-anchored "why" question per incident, traces the root-cause path, and POSTs it to a webhook (Slack / Teams / generic) so the loop is fully automated — no manual steps:

# tail a log dir, poll every 2s, group events into 10-minute incidents,
# and push each new incident + causal answer to a Slack webhook
chronograph watch ./logs --window 600 --interval 2 --webhook https://hooks.slack.com/services/AAA/BBB/CCC

Webhook payload (JSON) per new incident:

{
  "event": "incident_detected",
  "incident": {"id": "INC-CHECKOUT-SERVICE-20240225-095815", "service": "checkout-service", "title": "...", "time": "..."},
  "question": "Why did checkout-service fail on February 25?",
  "answer": "Checkout service failed because connection pool exhaustion was caused by the payment-service deploy at 09:58:10Z.",
  "confidence": 0.71,
  "path": [{"source": "...", "rel_type": "CAUSED_BY", "target": "..."}],
  "latency_ms": 185
}

Options: --window (time window in seconds to cluster events into one incident, default 600), --interval (poll interval in seconds, default 2), --webhook (URL; defaults to WATCH_WEBHOOK_URL env var / .env), --ask (question template with {service} and {date} placeholders; default "Why did {service} fail{date}?"), --top-k (paths to trace, default 3).

The question's date anchor disambiguates recurring incidents — a "why" query that names a date is hard-filtered to causal paths rooted on that date before ranking (see Path scoring).

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:

  1. watch the engine highlight the traced causal path in the graph view;
  2. hit Replay causal timeline to animate the chain hop-by-hop (symptom → cause → root cause);
  3. 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.2.0          # tag the version you just bumped in pyproject.toml
git push origin v0.2.0

First time only, connect the repo to PyPI:

  1. Create an account at https://pypi.org and add the pypi environment.
  2. In PyPI → Account settings → Publishing → add a new trusted publisher:
    • Project name: whygraph
    • Owner: HASSANFARYAD · Repo: ChronoGraph · Workflow: publish.yml
  3. The workflow's environment: pypi job 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
  • Online graph learning from postmortem corrections
  • Ragas/DeepEval-style metrics (faithfulness, evidence grounding) + a CI eval job
  • Distributed-trace span-gap inference (reconstructing missing spans from timing + parent ids)

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) + watch daemon + webhook notifier
  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.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for whygraph 0.3.0
File Size Uploaded Provenance Actions
whygraph-0.3.0.tar.gz 68.3 kB With provenance Details for whygraph-0.3.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for whygraph 0.3.0
File Interpreter ABI Platform Actions
whygraph-0.3.0-py3-none-any.whl Python 3 none any Details for whygraph-0.3.0-py3-none-any.whl

Total release size: 130.5 kB

Release files / whygraph-0.3.0.tar.gz

Download URL whygraph-0.3.0.tar.gz
Size 68.3 kB
Tags Source
SHA-256 checksum
How to use checksums
b194e054bacc3b36395cf1f78928c42ffc94eeea3ff11886976c2d71e3aae5c7
BLAKE2b-256 checksum
How to use checksums
ac15e1b59ef2e1532fe5af8247e41f0c12cbbd91469b8f28cb79fd79e89a9c40
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 6, 2026.

Transparency log

Release files / whygraph-0.3.0-py3-none-any.whl

Download URL whygraph-0.3.0-py3-none-any.whl
Size 62.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
373b29060ed3e7bb6336b8d38c1c3864eff5c362ff817a800ae829b25dfe9ca9
BLAKE2b-256 checksum
How to use checksums
59a212340e5c0990d5890f8f84659f42b61db9f0c6a474ef01632025f3565c88
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 6, 2026.

Transparency log

Security for whygraph 0.3.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.

Select from 2 files

Attestation for whygraph-0.3.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 6, 2026.

Transparency log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page