Metadata-Version: 2.4
Name: whygraph
Version: 0.2.0
Summary: Temporal causal retrieval engine: answers 'why' questions with time-aware knowledge-graph traversal
Author: Hassan Faryad
License-Expression: MIT
Project-URL: Homepage, https://github.com/HASSANFARYAD/ChronoGraph
Project-URL: Repository, https://github.com/HASSANFARYAD/ChronoGraph
Project-URL: Changelog, https://github.com/HASSANFARYAD/ChronoGraph/releases
Keywords: graphrag,causal-inference,knowledge-graph,retrieval-augmented-generation,root-cause-analysis,rag,neo4j,qdrant
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Software Development :: Libraries
Classifier: Intended Audience :: Developers
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: fastapi>=0.115
Requires-Dist: uvicorn[standard]>=0.30
Requires-Dist: pydantic>=2.7
Requires-Dist: pydantic-settings>=2.3
Requires-Dist: python-dotenv>=1.0
Requires-Dist: networkx>=3.3
Requires-Dist: numpy>=1.26
Requires-Dist: scikit-learn>=1.4
Requires-Dist: python-multipart
Provides-Extra: llm
Requires-Dist: openai>=1.40; extra == "llm"
Provides-Extra: neo4j
Requires-Dist: neo4j>=5.23; extra == "neo4j"
Provides-Extra: qdrant
Requires-Dist: qdrant-client>=1.9; extra == "qdrant"
Requires-Dist: sentence-transformers>=2.5; extra == "qdrant"
Provides-Extra: all
Requires-Dist: openai>=1.40; extra == "all"
Requires-Dist: neo4j>=5.23; extra == "all"
Requires-Dist: qdrant-client>=1.9; extra == "all"
Requires-Dist: sentence-transformers>=2.5; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Requires-Dist: mypy>=1.10; extra == "dev"

# 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](docs/screenshots/graph-view.png) | ![Edge inspection](docs/screenshots/edge-inspection.png) | ![Replay](docs/screenshots/replay.png) |

[▶ Watch the narrated demo (56s)](docs/screenshots/chronograph-demo.mp4)

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

---

## 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

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)

```bash
# 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:

```bash
# 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:

```bash
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:

```bash
# 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:

```json
{
  "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](#path-scoring)).

Or use it as a library in your own code:

```python
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:

```bash
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)

```bash
docker compose up --build
# backend  → http://localhost:8000/docs
# frontend → http://localhost:5173
```

### Local development

```bash
# 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

```bash
docker compose up -d neo4j qdrant   # just the stores
cp .env.example .env                # set OPENAI_API_KEY, then:
```

```ini
LLM_PROVIDER=openai
GRAPH_ENGINE=neo4j
VECTOR_ENGINE=qdrant
```

```bash
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`).

```bash
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):

```bash
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
- Incremental vector re-indexing as `chronograph watch` streams new incidents
- 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) + 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)
```
