Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ All three development phases are finished. The system is fully operational end-t
| Agent Mode | Complete | `src/pharmagraphrag/agent/` (LangGraph ReAct + multi-agent supervisor) |
| Observability | Complete | `src/pharmagraphrag/observability.py` (Langfuse tracing) |
| Docker Compose | Complete | `docker-compose.yml` + `docker/` |
| CI/CD | Complete | `.github/workflows/ci.yml` + `deploy.yml` |
| Kubernetes / Helm | Complete | `k8s/` (raw manifests) + `helm/pharmagraphrag/` (chart) |
| CI/CD | Complete | `.github/workflows/ci.yml` + `deploy.yml` + `deploy-gke.yml` |
| Evaluation | Complete | `src/pharmagraphrag/evaluation/` (RAGAS metrics, agent eval, curated testset) |
| Tests | 263 passing | `tests/` |
| Cloud Deployment | Live | Streamlit Cloud + Cloud Run + Neo4j Aura |
Expand Down Expand Up @@ -102,6 +103,7 @@ FDA FAERS (CSV) + DailyMed (API)
- **API**: FastAPI >= 0.115 with Pydantic v2
- **UI**: Streamlit 1.54+ with streamlit-agraph, pyvis, plotly
- **Containers**: Docker Compose (Neo4j + API + UI + optional Ollama)
- **Kubernetes**: Helm 3 chart (`helm/pharmagraphrag/`) + raw manifests (`k8s/`). HPA on CPU/memory, startup probes tuned for ~50s embedding-model cold start, LoadBalancer for UI, ClusterIP for API, optional GKE Ingress + managed cert. Designed for on-demand GKE Autopilot (destroy cluster after demos).
- **CI/CD**: GitHub Actions (ci.yml: lint+test on push; deploy.yml: CD on v* tags via Cloud Build)
- **Evaluation**: RAGAS 0.4.3 (Faithfulness, Relevancy, Precision, Recall, Correctness) + custom agent tool accuracy
- **Testing**: pytest (261 tests passing)
Expand Down
43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ jobs:
runs-on: ubuntu-latest
outputs:
docker: ${{ steps.filter.outputs.docker }}
helm: ${{ steps.filter.outputs.helm }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
Expand All @@ -75,6 +76,48 @@ jobs:
- 'uv.lock'
- 'requirements.txt'
- '.github/workflows/ci.yml'
helm:
- 'helm/**'
- 'k8s/**'
- '.github/workflows/ci.yml'

# ---------------------------------------------------------------
# Job 3 — Validate Helm chart and K8s manifests
# ---------------------------------------------------------------
helm-validate:
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.helm == 'true'
steps:
- uses: actions/checkout@v4

- name: Set up Helm
uses: azure/setup-helm@v4
with:
version: v3.16.2

- name: Helm lint
run: helm lint helm/pharmagraphrag

- name: Helm template (default values)
run: helm template demo helm/pharmagraphrag --namespace pharmagraphrag > /tmp/rendered.yaml

- name: Install kubeconform
run: |
curl -L -o kubeconform.tar.gz https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz
tar xf kubeconform.tar.gz
sudo mv kubeconform /usr/local/bin/

- name: Validate rendered manifests against K8s schema
run: kubeconform -strict -ignore-missing-schemas -schema-location default /tmp/rendered.yaml

- name: Validate raw k8s/ manifests
run: |
# Skip the example secret file
for f in k8s/*.yaml; do
[[ "$f" == *secret.example.yaml ]] && continue
kubeconform -strict -ignore-missing-schemas -schema-location default "$f"
done

# ---------------------------------------------------------------
# Job 2 — Build Docker images (only when Docker-relevant files change)
Expand Down
146 changes: 146 additions & 0 deletions .github/workflows/deploy-gke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# ============================================================
# GitHub Actions: Deploy PharmaGraphRAG to GKE
# ============================================================
# Triggered by tags matching v*-k8s (e.g. v1.5.0-k8s).
# Reuses CI workflow first, then builds API+UI images and
# deploys via Helm to a GKE Autopilot cluster.
#
# Required GitHub secrets:
# - GCP_SA_KEY: Service account JSON key (roles: container.developer,
# artifactregistry.writer, storage.admin)
# - GKE_CLUSTER_NAME: e.g. pharmagraphrag-autopilot
# - GKE_CLUSTER_LOCATION: e.g. us-central1
# - GKE_PROJECT_ID: e.g. pharmagraphrag
# - PGRAG_NEO4J_URI
# - PGRAG_NEO4J_PASSWORD
# - PGRAG_GEMINI_API_KEY
# ============================================================
name: Deploy to GKE

on:
push:
tags:
- "v*-k8s"
workflow_dispatch:
inputs:
tag:
description: "Image tag to deploy (e.g. v1.5.0-k8s)"
required: true
default: "latest-k8s"

env:
REGISTRY: gcr.io
PROJECT_ID: ${{ secrets.GKE_PROJECT_ID }}
API_IMAGE: pharmagraphrag-api
UI_IMAGE: pharmagraphrag-ui
CLUSTER_NAME: ${{ secrets.GKE_CLUSTER_NAME }}
CLUSTER_LOCATION: ${{ secrets.GKE_CLUSTER_LOCATION }}
NAMESPACE: pharmagraphrag

jobs:
ci:
uses: ./.github/workflows/ci.yml

build-and-deploy:
needs: ci
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Resolve image tag
id: tag
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "value=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT"
else
echo "value=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
fi

- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}

- name: Set up gcloud CLI
uses: google-github-actions/setup-gcloud@v2
with:
project_id: ${{ env.PROJECT_ID }}

- name: Configure Docker for GCR
run: gcloud auth configure-docker gcr.io --quiet

- name: Download ChromaDB snapshot from GCS
run: |
mkdir -p data/chroma
gcloud storage cp -r gs://pharmagraphrag-data/chroma/chroma/* data/chroma/

- name: Build API image
run: |
cp docker/Dockerfile.cloudrun.dockerignore .dockerignore
docker build -f docker/Dockerfile.cloudrun \
-t ${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.API_IMAGE }}:${{ steps.tag.outputs.value }} \
-t ${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.API_IMAGE }}:latest-k8s \
.

- name: Build UI image
run: |
docker build -f docker/Dockerfile.ui \
-t ${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.UI_IMAGE }}:${{ steps.tag.outputs.value }} \
-t ${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.UI_IMAGE }}:latest-k8s \
.

- name: Push images
run: |
docker push ${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.API_IMAGE }}:${{ steps.tag.outputs.value }}
docker push ${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.API_IMAGE }}:latest-k8s
docker push ${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.UI_IMAGE }}:${{ steps.tag.outputs.value }}
docker push ${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.UI_IMAGE }}:latest-k8s

- name: Install gke-gcloud-auth-plugin
run: gcloud components install gke-gcloud-auth-plugin --quiet

- name: Get GKE credentials
run: |
gcloud container clusters get-credentials ${{ env.CLUSTER_NAME }} \
--region ${{ env.CLUSTER_LOCATION }} \
--project ${{ env.PROJECT_ID }}

- name: Create namespace if missing
run: |
kubectl get namespace ${{ env.NAMESPACE }} 2>/dev/null \
|| kubectl create namespace ${{ env.NAMESPACE }}

- name: Apply secrets
run: |
kubectl -n ${{ env.NAMESPACE }} create secret generic pharmagraphrag-secrets \
--from-literal=GEMINI_API_KEY='${{ secrets.PGRAG_GEMINI_API_KEY }}' \
--from-literal=NEO4J_URI='${{ secrets.PGRAG_NEO4J_URI }}' \
--from-literal=NEO4J_PASSWORD='${{ secrets.PGRAG_NEO4J_PASSWORD }}' \
--dry-run=client -o yaml | kubectl apply -f -

- name: Set up Helm
uses: azure/setup-helm@v4
with:
version: v3.14.0

- name: Deploy via Helm
run: |
helm upgrade --install pharmagraphrag helm/pharmagraphrag \
--namespace ${{ env.NAMESPACE }} \
--set image.registry=${{ env.REGISTRY }}/${{ env.PROJECT_ID }} \
--set image.tag=${{ steps.tag.outputs.value }} \
--set secrets.create=false \
--set secrets.existingSecret=pharmagraphrag-secrets \
--wait --timeout 10m

- name: Show deployment status
run: |
kubectl -n ${{ env.NAMESPACE }} get pods,svc,hpa
echo "---"
echo "UI external IP (may take 1-2 min):"
kubectl -n ${{ env.NAMESPACE }} get svc pharmagraphrag-ui -o wide
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,17 @@ The project is **deployed and live** on a distributed cloud architecture:
| API + Vector Store | [Google Cloud Run](https://cloud.google.com/run) | [pharmagraphrag-api-...run.app](https://pharmagraphrag-api-893694384146.us-central1.run.app/health) |
| Knowledge Graph | [Neo4j Aura](https://neo4j.com/cloud/aura-free/) | Managed instance (11.9K nodes, 381K rels) |

### Deployment Options

PharmaGraphRAG ships with two cloud deployment paths that you can pick from:

| Path | Best for | Cost (idle) | Files |
| --- | --- | --- | --- |
| **Cloud Run + Streamlit Cloud** (default) | Low-traffic demos, scale-to-zero | $0 | [`cloudbuild.yaml`](cloudbuild.yaml), [`docker/Dockerfile.cloudrun`](docker/Dockerfile.cloudrun) |
| **GKE Autopilot + Helm** | Production-grade orchestration, HPA, rolling updates | ~$1-2/h while running | [`k8s/`](k8s/), [`helm/pharmagraphrag/`](helm/pharmagraphrag/), [`.github/workflows/deploy-gke.yml`](.github/workflows/deploy-gke.yml) |

The Kubernetes path was added to the portfolio to demonstrate production patterns: parameterized Helm chart, HorizontalPodAutoscalers, ConfigMap/Secret separation, probes tuned for the embedding-model cold start, and an automated GitHub Actions workflow that builds images, pushes to GCR and rolls out via `helm upgrade`. The cluster is **provisioned on-demand**: manifests live permanently in the repo, and the cluster is destroyed after each demo to avoid GKE costs. See [`k8s/README.md`](k8s/README.md) for the deploy/destroy commands.

<details>
<summary>📋 Reproducing the deployment</summary>

Expand Down
17 changes: 17 additions & 0 deletions helm/pharmagraphrag/.helmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Patterns to ignore when building Helm packages.
.DS_Store
.git/
.gitignore
.bzr/
.hg/
.svn/
*.swp
*.tmp
*.bak
*.orig
*~
.project
.idea/
*.tmproj
.vscode/
README.md
20 changes: 20 additions & 0 deletions helm/pharmagraphrag/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
apiVersion: v2
name: pharmagraphrag
description: GraphRAG system for drug interactions & adverse events (FDA data + Neo4j + ChromaDB + Gemini/Ollama)
type: application
version: 0.1.0
appVersion: "0.1.0"
keywords:
- graphrag
- rag
- neo4j
- chromadb
- llm
- pharmacovigilance
home: https://github.com/jmponcebe/PharmaGraphRAG
sources:
- https://github.com/jmponcebe/PharmaGraphRAG
maintainers:
- name: Jose María Ponce Bernabé
email: jmponcebe@gmail.com
icon: https://raw.githubusercontent.com/jmponcebe/PharmaGraphRAG/main/assets/screenshots/social-preview.png
63 changes: 63 additions & 0 deletions helm/pharmagraphrag/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# PharmaGraphRAG Helm chart

A Helm 3 chart that packages PharmaGraphRAG (API + UI) for Kubernetes.

## TL;DR

```bash
# Install / upgrade (uses an existing Secret by default)
helm upgrade --install pharmagraphrag ./helm/pharmagraphrag \
--namespace pharmagraphrag --create-namespace \
--set secrets.create=true \
--set secrets.values.GEMINI_API_KEY="$GEMINI_API_KEY" \
--set secrets.values.NEO4J_URI="$NEO4J_URI" \
--set secrets.values.NEO4J_PASSWORD="$NEO4J_PASSWORD"

# Uninstall (frees up GKE costs)
helm uninstall pharmagraphrag -n pharmagraphrag
```

## What it deploys

- `Deployment` + `Service` + `HPA` for the FastAPI **API**.
- `Deployment` + `Service` (LoadBalancer by default) + `HPA` for the Streamlit **UI**.
- `ConfigMap` with non-secret config (LLM model, Neo4j user, Chroma path, inter-service URL).
- `Secret` with credentials (created by the chart in dev, or referenced from an existing one in prod).
- Optional `Ingress` + GKE `ManagedCertificate` for custom-domain HTTPS.
- Production-grade probes: startup probe sized for ~50s embedding-model cold start, plus liveness + readiness.

## Key values

| Key | Default | Notes |
|---|---|---|
| `image.registry` | `gcr.io/pharmagraphrag` | Change to your registry |
| `image.tag` | `latest` | Falls back to `Chart.appVersion` if empty |
| `api.replicaCount` / `ui.replicaCount` | `1` | Ignored when `autoscaling.enabled` |
| `api.autoscaling.{min,max}Replicas` | `1` / `3` | CPU 70%, memory 80% targets |
| `ui.autoscaling.{min,max}Replicas` | `1` / `2` | CPU 75% target |
| `ui.service.type` | `LoadBalancer` | Use `ClusterIP` if exposing via Ingress |
| `secrets.create` | `false` | `true` to let the chart create the Secret (dev only) |
| `secrets.existingSecret` | `pharmagraphrag-secrets` | Name when `create=false` |
| `ingress.enabled` | `false` | Set `true` + provide `ingress.host` to use GKE Ingress |

## Render without installing

```bash
helm template demo ./helm/pharmagraphrag --namespace pharmagraphrag
```

## Lint

```bash
helm lint ./helm/pharmagraphrag
```

## Why a chart and not just raw manifests?

Both are checked in:

- [`k8s/`](../../k8s) — raw manifests, useful for understanding what gets created.
- [`helm/pharmagraphrag/`](.) — Helm chart, recommended for real deploys: parameterization,
upgrade/rollback, environment-specific values files, NOTES output, secret indirection.

CI/CD ([`deploy-gke.yml`](../../.github/workflows/deploy-gke.yml)) uses the Helm chart.
24 changes: 24 additions & 0 deletions helm/pharmagraphrag/templates/NOTES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
PharmaGraphRAG release "{{ .Release.Name }}" deployed to namespace "{{ .Release.Namespace }}".

To check status:
kubectl -n {{ .Release.Namespace }} get pods,svc,hpa,ingress

{{- if eq .Values.ui.service.type "LoadBalancer" }}

The UI is exposed as a LoadBalancer Service. Wait ~1-2 min for the external IP to be assigned, then:
kubectl -n {{ .Release.Namespace }} get svc {{ .Release.Name }}-ui

Once the EXTERNAL-IP shows a value (not <pending>), open: http://<EXTERNAL-IP>
{{- end }}

{{- if .Values.ingress.enabled }}

Ingress is enabled at host: {{ .Values.ingress.host }}
Point a DNS A record to the ingress IP, then access: https://{{ .Values.ingress.host }}
{{- end }}

API is reachable inside the cluster at:
http://{{ .Release.Name }}-api.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.api.service.port }}

To uninstall (and free up GKE costs):
helm uninstall {{ .Release.Name }} -n {{ .Release.Namespace }}
Loading
Loading