{"id":50816094,"url":"https://github.com/umerjavaidkh/agentic_graph_rag","last_synced_at":"2026-06-13T09:33:49.042Z","repository":{"id":361977387,"uuid":"1254614132","full_name":"umerjavaidkh/agentic_graph_rag","owner":"umerjavaidkh","description":"Agentic GraphRAG Engine built with Neo4j, LangGraph, and FastAPI for hybrid structured and unstructured retrieval, Text-to-Cypher analytics, and graph-native RBAC.","archived":false,"fork":false,"pushed_at":"2026-06-02T04:55:16.000Z","size":5929,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-02T05:04:00.045Z","etag":null,"topics":["charts","followup-intent","graph-rag","neo4j","ontology","reasoning","role-based-access-control","structured-data","unstructured-data","visual-question-answering"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/umerjavaidkh.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-30T19:43:47.000Z","updated_at":"2026-06-02T04:55:20.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/umerjavaidkh/agentic_graph_rag","commit_stats":null,"previous_names":["umerjavaidkh/agentic_graph_rag"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/umerjavaidkh/agentic_graph_rag","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/umerjavaidkh%2Fagentic_graph_rag","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/umerjavaidkh%2Fagentic_graph_rag/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/umerjavaidkh%2Fagentic_graph_rag/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/umerjavaidkh%2Fagentic_graph_rag/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/umerjavaidkh","download_url":"https://codeload.github.com/umerjavaidkh/agentic_graph_rag/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/umerjavaidkh%2Fagentic_graph_rag/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34279898,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-13T02:00:06.617Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["charts","followup-intent","graph-rag","neo4j","ontology","reasoning","role-based-access-control","structured-data","unstructured-data","visual-question-answering"],"created_at":"2026-06-13T09:33:48.942Z","updated_at":"2026-06-13T09:33:49.021Z","avatar_url":"https://github.com/umerjavaidkh.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Agentic GraphRAG\n\n**One Neo4j graph. Two knowledge modes. Answers that flat RAG cannot reliably give.**\n\n## Demos\n\n**1. Ingestion pipeline** — drop PDFs in the bulk-upload UI and watch them become a versioned Neo4j knowledge graph (lightweight parser, parallel NER + LLM extraction, batched writes, live job status).\n\n[![Agentic GraphRAG ingestion pipeline](https://img.youtube.com/vi/K4XIat6xpEw/maxresdefault.jpg)](https://youtu.be/K4XIat6xpEw)\n\n\u003e ▶️ Watch: https://youtu.be/K4XIat6xpEw\n\n**2. Retrieval + eval run** — the **30-case eval suite** answered live in the chat UI: 20 unstructured document questions + 10 structured analytics questions (bar/line/doughnut charts), each validated with an on-screen PASS/FAIL banner. Final score: **30/30**.\n\n[![Agentic GraphRAG demo — 30/30 eval pass](https://img.youtube.com/vi/7011-xkI1RI/maxresdefault.jpg)](https://youtu.be/7011-xkI1RI)\n\n\u003e ▶️ Watch: https://youtu.be/7011-xkI1RI\n\n---\n\nAgentic GraphRAG is a production-oriented **Graph RAG** stack that keeps **structured business data** and **unstructured documents** in the same graph database, then routes each question to the right retrieval strategy—or combines both. You get SQL-grade analytics on Northwind-style entities *and* multi-hop reasoning over WHO reports, policies, and manuals—without maintaining separate vector DBs, ETL pipelines, and ad-hoc orchestration glue.\n\nBuilt with **Neo4j · FastAPI · LangGraph · OpenAI**.\n\n---\n\n## Current status\n\nThe core platform is **feature-complete** for a production-oriented v1:\n\n| Area | Status |\n|------|--------|\n| Dual-graph RAG (structured + documents + hybrid) | ✅ |\n| Scalable ingestion (Redis + RQ workers, versioning) | ✅ |\n| Google OIDC auth, RBAC, per-user thread isolation | ✅ |\n| Streaming answers (`/query/stream`) with charts | ✅ |\n| Retrieval feedback loop + ops dashboard (`/feedback`) | ✅ |\n| Regression eval suites | ✅ |\n\n### Roadmap (future)\n\n| Planned | Description |\n|---------|-------------|\n| **Short memory (per user)** | Recent turns and session context across a thread — beyond today's single last-turn snapshot |\n| **Long memory (per user)** | Durable facts and preferences retrieved at query time (graph-backed or dedicated store) |\n| **Multi-language** | Query and answer in multiple languages (detection, prompts, and UI i18n) |\n\n---\n\n## Why this is different\n\n| Typical flat RAG | Agentic GraphRAG |\n|------------------|------------------|\n| One chunk index for everything | **Dedicated graphs** for tables vs. documents |\n| Similarity search only | **Cypher** for metrics + **hybrid retrieval** for PDFs |\n| Weak on counts, joins, time series | **Aggregations, rankings, charts** from live Neo4j |\n| Loses document structure | **Hierarchy**: Document → Chapter → Section → Page → Region |\n| Single-hop Q\u0026A | **Multi-hop** paths across entities, networks, and field stories |\n| Guesses when context is missing | **Eval suite** includes anti-hallucination and empty-result cases |\n\n**Unique in practice:** the same user session can ask *“Top 5 products by revenue in 1997”* (structured) and *“Which field epidemiology network deployed fellows to Greece and Kosovo?”* (unstructured, 2–3 hop)—with an **LLM MCP router** choosing `query_data` vs. `search_documents`, RBAC enforcing who sees what, and the chat UI rendering **tables, bar/line/doughnut charts**, or narrative answers as appropriate.\n\n---\n\n## What it does\n\n```mermaid\nflowchart TB\n  Q[User question] --\u003e R[MCP router]\n  R --\u003e|metrics / SQL-like| S[Structured agent]\n  R --\u003e|policies / PDFs| U[Unstructured agent]\n  R --\u003e|both| H[Hybrid answer]\n  S --\u003e C[Text-to-Cypher → Neo4j]\n  U --\u003e V[Vector + full-text + graph expand]\n  U --\u003e T[TOC / page / fact lookup]\n  C --\u003e N[(Neo4j)]\n  V --\u003e N\n  T --\u003e N\n  S --\u003e UI[Charts + tables + narrative]\n  U --\u003e UI\n  H --\u003e UI\n```\n\n| Mode | Best for | Examples |\n|------|----------|----------|\n| **Structured** | Counts, filters, rankings, time series, BI | *“Monthly order count in 1997”* · *“Revenue share by category”* |\n| **Unstructured** | Facts, relationships, timelines, synthesis over PDFs | *“ISBN of the annual report”* · *“Network that deployed fellows to Malta and Moldova”* |\n| **Hybrid** | Incidents + policy context in one answer | *“Show compliance incidents and summarize related policy guidance”* |\n\n**Unstructured retrieval is not “vector-only.”** It layers semantic search, lexical match, graph expansion from extracted entities, structural TOC/page fetch, and phrase-based fact lookup (URLs, licenses, abbreviations)—so complex questions anchor on the right section before synthesis.\n\n**Structured retrieval is not “text-to-SQL on CSV.”** It uses your **Neo4j schema** (Northwind demo or your own Cypher ingest), schema-aware repair, multistep plans for hard questions, and automatic chart selection (bar, horizontal bar, line, pie/doughnut).\n\n---\n\n## Complex questions this stack is built for\n\nTry these in chat, or run the bundled eval suites (`python3 scripts/run_rag_eval.py --help`):\n\n| Complexity | Structured (Northwind · `regular_001` / `regular_office`) | Unstructured (Go.Data · `public_001`) |\n|------------|-----------------------------------------------------------|----------------------------------------|\n| **Lookup** | Which supplier provides Chai? | What is the electronic version ISBN of the Go.Data annual report 2021? |\n| **Aggregation** | How many products exist in each category? | How many countries and territories were supported during 2020–2021? |\n| **Multi-hop** | Which customers purchased products in the Seafood category? | Which network deployed alumni to Greece, Malta, Moldova, and Kosovo? |\n| **Temporal** | Show monthly order count in 1997. | Which deployment came first: Cox's Bazar or Kasese, Uganda? |\n| **Compare / synthesize** | Revenue share by category (doughnut). | Contrast proximity tracing tools vs. Go.Data as categorized by WHO. |\n| **Anti-hallucination** | Which categories have never appeared in an order? | Which Silicon Valley firm wrote the Go.Data iOS app? *(should deny—not invent)* |\n\n---\n\n## Tech stack\n\n| Layer | Technology |\n|-------|------------|\n| Graph database | Neo4j 5.x |\n| AI orchestration | LangGraph |\n| API | FastAPI + Uvicorn |\n| LLM / Embeddings | OpenAI (gpt-4o-mini, text-embedding-3-small) |\n| PDF parsing | PyMuPDF + pdfplumber |\n| Job queue | Redis + RQ *(optional — in-process fallback when unset)* |\n| Containers | Docker / Docker Compose |\n\n---\n\n## Quick start\n\n```bash\ngit clone https://github.com/umerjavaidkh/agentic_graph_rag.git\ncd agentic_graph_rag\ncp .env.example .env          # add OPENAI_API_KEY\ndocker compose up --build\n```\n\nOpen:\n\n| Page | URL |\n|------|-----|\n| **Chat** | http://localhost:8000/chat |\n| **Upload** | http://localhost:8000/upload |\n| **Feedback monitor** | http://localhost:8000/feedback |\n| **API docs** | http://localhost:8000/docs |\n| **Health** | http://localhost:8000/health |\n\n\u003e Do **not** set `NEO4J_URI` in `.env` when using the bundled Docker Neo4j — it is wired automatically.\n\n### Enable Redis workers (parallel ingestion)\n\nAdd to `.env`:\n\n```env\nREDIS_URL=redis://redis:6379/0\n```\n\nThen:\n\n```bash\ndocker compose up --build              # starts Neo4j + Redis + API + 1 worker\ndocker compose up --scale worker=3    # scale to 3 parallel workers\n```\n\nWithout `REDIS_URL`, jobs run inside FastAPI via `BackgroundTasks` — fine for local dev.\n\n---\n\n## Uploading documents\n\nGo to **http://localhost:8000/upload**:\n\n- **Drop multiple PDFs** onto the drop zone — they are submitted concurrently.\n- Each file appears as a live job card with status, dispatch mode (`WORKER` / `IN-PROCESS`), version, and expandable logs.\n- The **queue status bar** shows Redis connectivity, queue depth, and failed job count.\n\nOr via `curl`:\n\n```bash\n# Single PDF\ncurl -X POST http://localhost:8000/ingest/unstructured \\\n  -F \"file=@sample_data_to_test/unstructured/rag_document.pdf\" \\\n  -F \"doc_key=rag-document\"\n\n# Cypher data (requires ALLOW_CYPHER_INGEST=true + admin Google sign-in)\ncurl -X POST http://localhost:8000/ingest/cypher \\\n  -H \"Authorization: Bearer $GOOGLE_ID_TOKEN\" \\\n  -F \"file=@sample_data_to_test/structured/northwind-data.cypher\"\n```\n\n`doc_key` controls versioning: same key + same file → skipped; same key + changed file → new revision.\n\n**Ingestion requires Google sign-in as an admin** — see [Authentication \u0026 roles](#authentication--roles). Use `/upload` in the browser (Sign in with Google) or pass the ID token from GIS as `Authorization: Bearer \u003ctoken\u003e`.\n\n---\n\n## Try it in chat\n\n### With Google sign-in (`AUTH_ENABLED=true`)\n\n1. Set `GOOGLE_CLIENT_ID` in `.env` and register `http://localhost:8000` as an **Authorized JavaScript origin** in Google Cloud Console.\n2. Open `/chat` → **Sign in with Google**.\n3. Default signed-in users get **`compliance_officer`** (documents + structured). Emails listed in `AUTH_EMAIL_ROLE_MAP` with `=admin` can use `/upload` for ingestion.\n\n| Track | Prerequisite | Signed-in role |\n|-------|--------------|----------------|\n| **Structured** | Northwind loaded via `/upload` (admin) | `compliance_officer` or `admin` |\n| **Unstructured** | PDF ingested via `/upload` (admin) | `compliance_officer` or `admin` |\n| **Hybrid** | Both loaded | `compliance_officer` or `admin` |\n| **Ingestion** | Admin email in `AUTH_EMAIL_ROLE_MAP` | `admin` only |\n\n### Dev / eval without Google (`AUTH_ENABLED=false`, or `AUTH_ALLOW_BODY_FALLBACK=true` while unsigned in)\n\n| Track | Prerequisite | User ID | Role |\n|-------|--------------|---------|------|\n| **Structured** | Northwind loaded | `regular_001` | `regular_office` |\n| **Unstructured** | PDF ingested | `public_001` | `public` |\n| **Hybrid** | Both loaded | `compliance_001` | `compliance_officer` |\n\n**Structured quick checks:**\n```\nWhich customers ordered the most?\nTop 5 products by sales revenue in 1997?\n```\n\n**Unstructured quick checks:**\n```\nList all the table of contents from the Go.Data document.\nWhat is the URL for the Go.Data Community of Practice portal?\n```\n\n**Hybrid:**\n```\nShow compliance incidents and summarize the related policy guidance.\n```\n\n### Regression eval (optional)\n\nSmoke suites under `eval/` exercise routing, answers, charts, and anti-hallucination cases against `/query`. Corpus-specific expectations live in JSON only — not in retriever code.\n\n```bash\npython3 scripts/run_rag_eval.py --suite all          # document + structured + advanced\npython3 scripts/run_rag_eval.py --attach-feedback    # label pass/fail for feedback loop\npytest tests/test_rag_eval_validators.py -q\n```\n\nSet `EVAL_BASE_URL` (default `http://localhost:8000`) and `EVAL_TIMEOUT` (default `180`) for slow LLM calls.\n\n---\n\n## Feedback monitor\n\nA dedicated **ops window** at **http://localhost:8000/feedback** shows how the feedback loop is performing — separate from chat, read-only, auto-refreshes every 30 seconds.\n\nRequires `RETRIEVAL_FEEDBACK_ENABLED=true` in `.env` (and `docker compose up -d --build app` after changes).\n\n### What you see\n\n| Panel | Purpose |\n|-------|---------|\n| **KPI cards** | Recording on/off, routing apply on/off, store type (Redis or JSONL), event and label counts |\n| **Label coverage chart** | Pass / fail / unlabeled breakdown |\n| **Pass rate by mode** | Which retrieval modes (`graph_rag_lexical`, `text2cypher`, etc.) perform best |\n| **Patterns table** | Learned question buckets, pass rates, retrieval + route hints, whether routing **would apply** |\n| **Recent events** | Last queries: `request_id`, mode, route, outcome, whether `feedback.routing` ran |\n| **Question probe** | Type any question to inspect pattern stats via `/feedback/stats` |\n\n### Chat thumbs (👍 / 👎)\n\nWhen `RETRIEVAL_FEEDBACK_ENABLED=true`, each assistant reply in `/chat` shows **Helpful?** buttons. A click posts to `POST /feedback/outcome` with that message’s `request_id` (same path as eval labeling). Votes appear on the dashboard after refresh.\n\n### Typical workflow\n\n1. Enable feedback in `.env`:\n   ```env\n   RETRIEVAL_FEEDBACK_ENABLED=true\n   RETRIEVAL_FEEDBACK_ROUTING=false   # observe first; set true when labels are ready\n   RETRIEVAL_FEEDBACK_STORE_QUESTION=false\n   REDIS_URL=redis://redis:6379/0\n   ```\n2. Use **chat** or run eval with `--attach-feedback` to label pass/fail.\n3. Open **/feedback** in a second browser tab while testing.\n4. When patterns have enough labeled samples (`RETRIEVAL_FEEDBACK_MIN_SAMPLES`), enable `RETRIEVAL_FEEDBACK_ROUTING=true` and watch **Routing applied** in recent events.\n\n### API\n\n| Endpoint | Description |\n|----------|-------------|\n| `GET /feedback` | Dashboard UI |\n| `GET /feedback/dashboard` | JSON aggregate (powers the UI) |\n| `GET /feedback/stats?question=…` | Stats + hint for one question pattern |\n| `POST /feedback/outcome` | Attach pass/fail to a prior `request_id` |\n\nFeedback shares the same Redis instance as ingestion (different key namespaces). Fine for current scale; see architecture notes if you outgrow a single Redis.\n\n---\n\n## Architecture\n\n**Query path**\n\n```\nUser Query → MCP Router (routing.py + feedback_loop resolver)\n                 ├─ Structured Agent → Text-to-Cypher / multistep → Neo4j → charts/tables\n                 ├─ Unstructured Agent → hybrid retrieval → Neo4j → narrative + sources\n                 └─ Hybrid (compliance role) → both paths → merged answer\n                      │\n                      ▼\n              feedback_loop (observe → label → learn → optional apply)\n```\n\n**Feedback loop** (`src/feedback_loop/`) — observe pipeline telemetry, label outcomes, optionally improve routing:\n\n| Stage | What happens |\n|-------|----------------|\n| **Observe** | After `/query` and `/query/stream`, persist compact telemetry (Redis stream or JSONL) |\n| **Label** | `POST /feedback/outcome` or eval `--attach-feedback` marks pass/fail per `request_id` |\n| **Learn** | Aggregate pass rates by question **pattern** (intent flags) + retrieval mode or route tool |\n| **Apply** | When `RETRIEVAL_FEEDBACK_ROUTING=true`, `FeedbackRoutingService` adjusts multistep vs text2cypher, document hybrid mode, or MCP route — only after enough labeled samples |\n\nSee **[Feedback monitor](#feedback-monitor)** below for the ops UI. Application code imports from `src/feedback_loop`; `src/telemetry/feedback/` is a deprecated shim.\n\n**Unstructured retrieval modes** (selected per question): vector similarity · full-text · graph expand from NER · TOC structural fetch · page-by-number · phrase/fact lookup (URLs, licenses).\n\n**Ingestion write path:**\n\n```\nPDF → LightPdfParser\n        │\n        ├── Axis 1: Document → Chapter → Section → Page → Region\n        ├── Page vision (optional, ENABLE_PAGE_VISION=true)\n        └── Axis 2: Embeddings · NER · Clustering · LLM relationship pass\n                      (parallel thread pools)\n              │\n              └── Neo4jExporter (UNWIND batched writes) → Neo4j\n```\n\n**With Redis workers:**\n\n```\nPOST /ingest  →  API  →  Redis queue\n                              │\n                    ┌─────────┴─────────┐\n                 Worker 1           Worker N\n                    │                   │\n             per-doc Redis lock (same doc serialised, different docs parallel)\n             parse → Axis 2 → batched Neo4j writes → update job in Redis\n                              │\nGET /ingest/jobs/{id}  →  reads from Redis  →  200\n```\n\n---\n\n## Configuration\n\nCopy `.env.example` → `.env`. Key variables:\n\n### Core\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `OPENAI_API_KEY` | **required** | LLM, embeddings, routing |\n| `NEO4J_USER` | `neo4j` | Neo4j username |\n| `NEO4J_PASSWORD` | `password123` | Neo4j password |\n| `CHAT_MODEL` | `gpt-4o-mini` | Document RAG synthesis; default for other stages |\n| `STRUCTURED_MODEL` | *(CHAT_MODEL)* | Text-to-Cypher + structured answers |\n| `ROUTING_MODEL` | *(CHAT_MODEL)* | MCP routing (`search_documents` vs `query_data`) |\n| `AXIS2_MODEL` | *(CHAT_MODEL)* | Ingestion NER + relationship LLM pass |\n| `EMBEDDING_MODEL` | `text-embedding-3-small` | Retrieval + ingest embeddings |\n| `VISION_MODEL` | `gpt-4o-mini` | Page vision (`ENABLE_PAGE_VISION=true`) |\n\n| `APP_PORT` | `8000` | API port |\n\nActive model resolution: `GET /config/models`\n\n### Authentication \u0026 roles\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `AUTH_ENABLED` | `false` | `true` → chat/upload require Google (or OIDC) for production paths |\n| `GOOGLE_CLIENT_ID` | *(empty)* | OAuth 2.0 Web client ID (GIS button in `/chat` and `/upload`) |\n| `AUTH_DEFAULT_ROLE` | `compliance_officer` | Role assigned to new Google users (chat: documents + structured) |\n| `AUTH_EMAIL_ROLE_MAP` | *(empty)* | Comma-separated `email=role` overrides, e.g. `you@corp.com=admin` |\n| `AUTH_JIT_PROVISION` | `true` | On each login, sync User + `HAS_ROLE` in Neo4j from config/maps |\n| `AUTH_ALLOW_BODY_FALLBACK` | `true` when auth off | `true` → unsigned `/query` may send `user_id`/`role` in JSON (eval/dev). Set **`false` in production**. |\n| `AUTH_PROVIDER` | `google` | `google` or `oidc` (corporate IdP via `OIDC_ISSUER`, `OIDC_AUDIENCE`) |\n| `AUTH_CLAIM_ROLE_MAP` | *(empty)* | Optional IdP group → role map (JSON or `Group=role` pairs) |\n\n**Admin configuration (ingestion):** map operator Google emails to `admin` in `.env`:\n\n```env\nAUTH_ENABLED=true\nGOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com\nAUTH_DEFAULT_ROLE=compliance_officer\nAUTH_EMAIL_ROLE_MAP=you@company.com=admin\nAUTH_ALLOW_BODY_FALLBACK=false   # production: Google only for /query\n```\n\n- **`admin`** — ingestion (`/upload`, `/ingest/*`), `/admin/reset-neo4j`, full RBAC.\n- **`compliance_officer`** — chat only (`/query`, `/query/stream`): documents + structured data.\n- **`regular_office`** — structured data only (demo user `regular_001`).\n- **`public`** — document graph only (demo user `public_001`).\n\nWhen a Bearer JWT is present, the server **ignores** sidebar/body `user_id` and `role` — identity comes from Google claims + maps above.\n\nPublic config for the UI: `GET /auth/config` · current principal: `GET /auth/me` (with `Authorization: Bearer …`).\n\n### Ingestion \u0026 scalability\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `REDIS_URL` | *(unset = in-process)* | Set to `redis://redis:6379/0` for workers |\n| `INGEST_QUEUE_NAME` | `ingest` | RQ queue name |\n| `AXIS2_NER_CONCURRENCY` | `8` | Parallel NER calls per doc |\n| `AXIS2_LLM_PAIR_CONCURRENCY` | `6` | Parallel LLM relationship calls per doc |\n| `AXIS2_MAX_LLM_PAIRS` | `300` | Cap on candidate pairs sent to LLM |\n| `NEO4J_WRITE_BATCH` | `2000` | UNWIND chunk size for bulk writes |\n| `DOC_SKIP_DUPLICATE_HASH` | `true` | Skip ingest when same file already active |\n| `DOC_VERSION_RETAIN_METADATA` | `true` | Keep expired `DocRevision` nodes for audit |\n| `ENABLE_PAGE_VISION` | `false` | Vision model descriptions for PDF pages |\n| `ALLOW_CYPHER_INGEST` | `false` | Enable `.cypher` file upload endpoint |\n| `ALLOW_DB_RESET` | `false` | Enable `/admin/reset-neo4j` |\n\n### Retrieval feedback loop\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `RETRIEVAL_FEEDBACK_ENABLED` | `false` | Record pipeline telemetry after queries (no behavior change) |\n| `RETRIEVAL_FEEDBACK_ROUTING` | `false` | Apply labeled hints to routing/retrieval (`true` = self-improvement) |\n| `RETRIEVAL_FEEDBACK_STORE_QUESTION` | `false` | Store first 120 chars of question in feedback events (privacy: keep `false` in prod) |\n| `RETRIEVAL_FEEDBACK_MIN_SAMPLES` | `30` | Minimum labeled outcomes before a hint can apply |\n| `RETRIEVAL_FEEDBACK_MIN_MARGIN` | `0.15` | Required pass-rate gap between best and second-best mode |\n| `REDIS_URL` | *(unset)* | Recommended for production feedback aggregates (same Redis as ingestion) |\n\n```bash\n# Label a prior query (e.g. from eval or thumbs)\ncurl -X POST http://localhost:8000/feedback/outcome \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"request_id\": \"abc123\", \"passed\": true}'\n\n# Inspect pattern stats + suggested hint (read-only)\ncurl \"http://localhost:8000/feedback/stats?question=Top%205%20products%20by%20revenue\u0026agent=structured\"\n\n# Full ops dashboard JSON (powers /feedback UI)\ncurl http://localhost:8000/feedback/dashboard\n```\n\n### NEO4J_URI — when to set it\n\n| Setup | Value |\n|-------|-------|\n| Docker + bundled Neo4j | **Leave unset** |\n| Docker + Neo4j on your Mac | `bolt://host.docker.internal:7687` |\n| API on Mac + Neo4j in Docker | `bolt://localhost:17687` |\n| API on Mac + local Neo4j | `bolt://localhost:7687` |\n\n---\n\n## API reference\n\n```bash\n# Query (dev — body user_id/role when AUTH_ALLOW_BODY_FALLBACK=true)\ncurl -X POST http://localhost:8000/query \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"question\": \"Which customers ordered the most?\", \"user_id\": \"regular_001\", \"role\": \"regular_office\"}'\n\n# Query (production — Google ID token)\ncurl -X POST http://localhost:8000/query \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Authorization: Bearer $GOOGLE_ID_TOKEN\" \\\n  -d '{\"question\": \"List all table of contents for the Go.Data document.\"}'\n\n# Upload PDF (admin JWT required)\ncurl -X POST http://localhost:8000/ingest/unstructured \\\n  -H \"Authorization: Bearer $GOOGLE_ID_TOKEN\" \\\n  -F \"file=@doc.pdf\" -F \"doc_key=my-doc\"\n\n# Job status (admin JWT required)\ncurl -H \"Authorization: Bearer $GOOGLE_ID_TOKEN\" \\\n  http://localhost:8000/ingest/jobs/{job_id}\ncurl -H \"Authorization: Bearer $GOOGLE_ID_TOKEN\" \\\n  \"http://localhost:8000/ingest/jobs?limit=20\"\ncurl -H \"Authorization: Bearer $GOOGLE_ID_TOKEN\" \\\n  http://localhost:8000/ingest/queue/status\n\n# Auth helpers\ncurl http://localhost:8000/auth/config\ncurl -H \"Authorization: Bearer $GOOGLE_ID_TOKEN\" http://localhost:8000/auth/me\n\n# Health / active models\ncurl http://localhost:8000/health\ncurl http://localhost:8000/config/models\n\n# Feedback loop (requires RETRIEVAL_FEEDBACK_ENABLED=true)\ncurl -X POST http://localhost:8000/feedback/outcome \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"request_id\": \"YOUR_REQUEST_ID\", \"passed\": true}'\ncurl \"http://localhost:8000/feedback/stats?question=monthly%20order%20count%20in%201997\"\n```\n\nResponse fields from `/ingest/jobs/{id}`: `status`, `dispatch` (`worker` / `background_task`), `logical_doc_id`, `revision_id`, `version_number`, `skipped_duplicate`, `logs[]`, `error`.\n\n---\n\n## Authentication \u0026 roles\n\n### How identity is resolved\n\n```mermaid\nflowchart LR\n  subgraph chat [Chat /query]\n    G[Google JWT] --\u003e C[claims + AUTH_EMAIL_ROLE_MAP]\n    C --\u003e JIT[Neo4j User HAS_ROLE]\n    JIT --\u003e RBAC[can_query_knowledge_area]\n    B[Body user_id/role] -.-\u003e|only if no JWT and AUTH_ALLOW_BODY_FALLBACK| RBAC\n  end\n  subgraph ingest [Ingest /upload]\n    G2[Google JWT] --\u003e A{role = admin?}\n    A --\u003e|yes| UP[/ingest/* /admin/*]\n    A --\u003e|no| DENY[403]\n  end\n```\n\n| Surface | Auth | Role required |\n|---------|------|----------------|\n| `/chat`, `POST /query`, `POST /query/stream` | JWT recommended; body fallback optional | Any role with data access (`compliance_officer`+ for both graphs) |\n| `/upload`, `POST /ingest/*`, `GET /ingest/*` | **JWT required** (no body fallback) | **`admin`** only |\n| `POST /admin/reset-neo4j` | **JWT required** | **`admin`** (+ `ALLOW_DB_RESET=true`) |\n\n### RBAC by role (Neo4j knowledge areas)\n\n| Role | Documents (`esg`) | Structured (`structured`) | Ingestion |\n|------|-------------------|---------------------------|-----------|\n| `public` | ✅ | ❌ | ❌ |\n| `regular_office` | ❌ | ✅ | ❌ |\n| `compliance_officer` | ✅ | ✅ | ❌ |\n| `admin` | ✅ | ✅ | ✅ |\n\n### Seeded demo users (eval / `AUTH_ALLOW_BODY_FALLBACK`)\n\n| User ID | Role | Documents | Structured |\n|---------|------|-----------|------------|\n| `public_001` | `public` | ✅ | ❌ |\n| `regular_001` | `regular_office` | ❌ | ✅ |\n| `compliance_001` | `compliance_officer` | ✅ | ✅ |\n| `admin_001` | `admin` | ✅ | ✅ |\n\nGoogle sign-in creates JIT users in Neo4j (`AUTH_JIT_PROVISION=true`). Role is taken from `AUTH_EMAIL_ROLE_MAP` first, else `AUTH_DEFAULT_ROLE`, and re-synced on each login.\n\n**Follow-up memory (`thread_id`):** scoped per user as `{user_id}:{session_uuid}` on the server. Two different Google users never share follow-up context; **New chat** only clears the current user's thread. Today this is a **single last-turn** snapshot (`conversation/thread_memory.py`). Per-user **short** and **long** memory are on the [roadmap](#roadmap-future).\n\n**Google OAuth `origin_mismatch`:** the browser origin must exactly match an **Authorized JavaScript origin** (e.g. `http://localhost:8000`, not `127.0.0.1` unless both are registered).\n\n---\n\n## Neo4j\n\n| Purpose | Value |\n|---------|-------|\n| Browser | http://localhost:17474 |\n| Bolt URL (in Browser login) | `neo4j://localhost:17687` |\n| Username / Password | `neo4j` / `password123` |\n\nPorts 17474 / 17687 avoid clashing with a local Neo4j on 7474 / 7687.\n\n```bash\n# Shell access\ndocker exec -it graphrag-neo4j cypher-shell -u neo4j -p password123\n```\n\n---\n\n## Project structure\n\n```\nagentic_graph_rag/\n├── sample_data_to_test/\n│   ├── unstructured/          # rag_document.pdf, rag_document_2.pdf\n│   └── structured/            # northwind-data.cypher\n├── src/\n│   ├── api.py                 # FastAPI routes, dispatch, job list, queue status\n│   ├── config/settings.py     # All env-var settings\n│   ├── ingestion/\n│   │   ├── service.py         # IngestionManager (store-backed, per-doc lock)\n│   │   ├── job_store.py       # RedisJobStore / InMemoryJobStore\n│   │   ├── queue.py           # RQ queue helpers\n│   │   ├── tasks.py           # run_ingest_job() — RQ worker callable\n│   │   └── models.py          # IngestionStatus enum\n│   ├── document/\n│   │   ├── parser.py          # LightPdfParser (PyMuPDF + pdfplumber)\n│   │   ├── page_vision.py     # Optional vision enrichment\n│   │   └── versioning.py      # Logical doc ID, revision plans, hashing\n│   ├── exporter/exporter.py   # Neo4jExporter — UNWIND batched writes\n│   ├── semantic/axis2.py      # Axis 2 (parallel NER + LLM relationship pass)\n│   ├── retrieval/\n│   │   ├── unstructured/        # DocumentRAGRetriever (facade + mixins)\n│   │   │   ├── retriever.py     # Public API + backward-compat exports\n│   │   │   ├── mixins/          # hybrid, graph_seeds, ranking, lexical, TOC/page/box strategies\n│   │   │   ├── query_intent.py  # Question-shape routing (TOC, page, synthesis, …)\n│   │   │   ├── toc_retrieval.py, visual_retrieval.py, executor.py\n│   │   └── structured/          # StructuredRetriever (facade)\n│   │       ├── retriever.py\n│   │       ├── cypher/          # generate, validate, repair, pipeline\n│   │       ├── multistep/       # planner, executor, context\n│   │       ├── schema/ · policies/ · formatting/\n│   ├── graph/                 # Neo4j constants, lifecycle helpers\n│   ├── presentation/          # UI blocks (markdown, tables, charts)\n│   ├── conversation/          # Thread memory + clarification\n│   ├── feedback_loop/         # Observe → label → learn → optional routing apply\n│   │   ├── pattern.py · profile.py · store.py · record.py · hints.py · dashboard.py\n│   │   ├── resolver.py        # Shared MCP tool resolution (router + stream)\n│   │   └── routing/           # Policy-based FeedbackRoutingService\n│   ├── static/\n│   │   ├── chat.html · upload.html · feedback.html   # Feedback ops monitor\n│   ├── auth/                  # RBAC + OIDC (Google JWT, JIT provision, deps)\n│   ├── streaming/             # NDJSON /query/stream orchestrator\n│   └── prompts/               # LLM prompts\n├── eval/                      # JSON smoke suites + validators\n├── scripts/run_rag_eval.py    # Regression eval against /query\n├── tests/\n│   ├── test_retrieval_feedback_unit.py\n│   ├── test_scalable_pipeline_unit.py\n│   ├── test_document_versioning_unit.py\n│   ├── test_toc_retrieval_unit.py\n│   └── test_rag_eval_validators.py\n├── docker-compose.yml         # Neo4j + Redis + API + worker\n├── Dockerfile\n└── .env.example\n```\n\n---\n\n## Troubleshooting\n\n**App not loading (port 8000 refused)**\n```bash\ndocker ps --filter name=graphrag\ndocker logs graphrag-app --tail 50\n# Common cause: missing OPENAI_API_KEY, or NEO4J_URI set to localhost in .env\ndocker compose up -d app\n```\n\n**Neo4j Browser: \"Connection failed\"** — use `neo4j://localhost:17687`, not `localhost:7687`.\n\n**Worker not picking up jobs**\n```bash\ndocker ps --filter name=worker\ndocker logs $(docker ps --filter name=worker -q | head -1) --tail 30\ndocker exec graphrag-redis redis-cli ping        # should return PONG\ncurl http://localhost:8000/ingest/queue/status   # check failed_jobs[]\n```\n\n**Job status lost after restart** — set `REDIS_URL` for durable storage; without it state lives only in the API process.\n\n**Access denied on structured queries** — with Google sign-in, use `compliance_officer` or `admin` (default: `AUTH_DEFAULT_ROLE=compliance_officer`). In dev sidebar mode: structured needs `regular_001` / `regular_office`; documents need `public_001` / `public`; both graphs need `compliance_001` or `admin_001`.\n\n**Ingestion returns 401/403** — sign in on `/upload` with an email listed in `AUTH_EMAIL_ROLE_MAP=…=admin`. Body `role`/`user_id` fields are **not** accepted on ingest routes.\n\n**Google sign-in: Error 400 `origin_mismatch`** — add `http://localhost:8000` to Authorized JavaScript origins in Google Cloud Console (match the exact URL you open in the browser).\n\n**Rebuild slow** — only rebuild the changed service:\n```bash\ndocker compose up -d --build app\n```\n\n---\n\n## Local development (without Docker)\n\n```bash\npython -m venv venv \u0026\u0026 source venv/bin/activate\npip install -r requirements.txt\n# .env: set OPENAI_API_KEY, NEO4J_URI=bolt://localhost:7687, NEO4J_PASSWORD\nuvicorn src.api:app --reload --host 0.0.0.0 --port 8000\n```\n\n---\n\n## Further reading\n\n**Medium article:** [Agentic Graph RAG — architecture and walkthrough](https://medium.com/p/0ee1f6baae26)\n\n---\n\n## Security\n\n- Never commit `.env` — it is gitignored\n- **Production checklist:**\n  - `AUTH_ENABLED=true`, `AUTH_ALLOW_BODY_FALLBACK=false` (no impersonation via JSON body)\n  - `AUTH_EMAIL_ROLE_MAP=your-admin@company.com=admin` — only listed emails can ingest\n  - `AUTH_DEFAULT_ROLE=compliance_officer` (or tighter) for everyone else\n  - `ALLOW_CYPHER_INGEST=false`, `ALLOW_DB_RESET=false`\n  - Do not publish Neo4j (17474/17687) or Redis (6379) to the public internet\n- Ingest and admin routes require a verified Google JWT + `admin` role — never trust form `role`/`user_id`\n- Rotate your OpenAI key if it was ever exposed\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fumerjavaidkh%2Fagentic_graph_rag","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fumerjavaidkh%2Fagentic_graph_rag","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fumerjavaidkh%2Fagentic_graph_rag/lists"}