Add Knowledge Explorer deployment templates

This commit is contained in:
Zohaib Hassnain
2026-06-23 13:37:25 +05:00
parent a450f9eddc
commit 21ddee94f7
39 changed files with 1150 additions and 31 deletions
+103
View File
@@ -0,0 +1,103 @@
# Start with a tiny Docker context and opt in only files used by Dockerfile.
*
!Dockerfile
!.dockerignore
!pyproject.toml
!README.md
!LICENSE
!MANIFEST.in
!semantica/
!semantica/**
!integrations/
!integrations/**
!explorer/
!explorer/**
# VCS, local config, and secrets.
.git
.git/**
.github
.github/**
.claude
.claude/**
.codex
.codex/**
.agents
.agents/**
.env
.env.*
*.env
# Python build/test/cache artifacts.
__pycache__
**/__pycache__
*.py[cod]
.pytest_cache
.pytest_cache/**
.mypy_cache
.mypy_cache/**
.ruff_cache
.ruff_cache/**
.tox
.tox/**
.venv
.venv/**
venv
venv/**
coverage
coverage/**
htmlcov
htmlcov/**
*.egg-info
*.egg-info/**
build
build/**
dist
dist/**
# Frontend dependency/build artifacts.
node_modules
node_modules/**
explorer/node_modules
explorer/node_modules/**
explorer/dist
explorer/dist/**
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Local outputs and large generated samples.
logs
logs/**
*.log
*.tmp
*.bak
*.backup
tests
tests/**
explorer/tests
explorer/tests/**
docs
docs/**
site
site/**
.mkdocs_cache
.mkdocs_cache/**
cookbook
cookbook/**
examples
examples/**
demo_assets
demo_assets/**
demo_out
demo_out/**
demo_out_*
demo_out_*/**
outputs
outputs/**
pytest-cache-files-*
pytest-cache-files-*/**
test_data
test_data/**
sample_data
sample_data/**
+28 -16
View File
@@ -1,29 +1,41 @@
FROM node:26-alpine AS frontend-builder # syntax=docker/dockerfile:1
FROM node:22-alpine AS frontend-builder
WORKDIR /app/semantica-explorer WORKDIR /app
COPY explorer/package*.json ./explorer/
WORKDIR /app/explorer
RUN npm ci
COPY explorer/ ./
RUN mkdir -p /app/semantica && npm run build
COPY semantica-explorer/package.json semantica-explorer/package-lock.json* ./ FROM python:3.12-slim AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
RUN npm install PYTHONUNBUFFERED=1 \
FALKORDB_HOST=falkordb \
FALKORDB_PORT=6379 \
COPY semantica-explorer/ ./ ALLOWED_ORIGINS=http://localhost:8000,http://127.0.0.1:8000
RUN npm run build
FROM python:3.14-slim AS runtime
WORKDIR /app WORKDIR /app
COPY pyproject.toml ./ RUN groupadd --system semantica \
COPY semantica/ ./semantica/ && useradd --system --gid semantica --home-dir /app --shell /usr/sbin/nologin semantica
COPY pyproject.toml README.md LICENSE MANIFEST.in ./
COPY semantica/ ./semantica/
COPY integrations/ ./integrations/
COPY --from=frontend-builder /app/semantica/static ./semantica/static COPY --from=frontend-builder /app/semantica/static ./semantica/static
RUN pip install --no-cache-dir ".[explorer]" RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir ".[explorer]" \
&& chown -R semantica:semantica /app
USER semantica
EXPOSE 8000 EXPOSE 8000
CMD ["python", "-m", "uvicorn", "semantica.explorer.app:app", "--host", "0.0.0.0", "--port", "8000"] HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD python -c "import json, urllib.request; data=json.load(urllib.request.urlopen('http://127.0.0.1:8000/api/health', timeout=3)); raise SystemExit(0 if data.get('status') == 'ok' else 1)"
CMD ["python", "-m", "uvicorn", "semantica.explorer.app:app", "--host", "0.0.0.0", "--port", "8000"]
+13
View File
@@ -0,0 +1,13 @@
# Azure Container Apps
Deploy with Azure Developer CLI from this template directory:
```bash
cd deploy/azure
azd auth login
azd init --environment semantica-ke
azd env set AZURE_LOCATION eastus
azd up
```
The Bicep template provisions a Container Apps managed environment, HTTP ingress, scale-to-zero, max 10 replicas, and a `/api/health` liveness probe.
+14
View File
@@ -0,0 +1,14 @@
name: semantica-knowledge-explorer
metadata:
template: semantica-knowledge-explorer@0.1.0
services:
explorer:
project: ../..
host: containerapp
language: docker
docker:
path: ./Dockerfile
context: .
infra:
provider: bicep
path: .
+113
View File
@@ -0,0 +1,113 @@
targetScope = 'resourceGroup'
param environmentName string = 'semantica-ke'
param location string = resourceGroup().location
param imageName string
param containerPort int = 8000
param allowedOrigins string = '*'
param falkordbHost string = 'falkordb'
param falkordbPort string = '6379'
var appName = '${environmentName}-explorer'
var logAnalyticsName = '${environmentName}-logs'
var managedEnvironmentName = '${environmentName}-env'
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
name: logAnalyticsName
location: location
properties: {
sku: {
name: 'PerGB2018'
}
retentionInDays: 30
}
}
resource managedEnvironment 'Microsoft.App/managedEnvironments@2024-03-01' = {
name: managedEnvironmentName
location: location
properties: {
appLogsConfiguration: {
destination: 'log-analytics'
logAnalyticsConfiguration: {
customerId: logAnalytics.properties.customerId
sharedKey: logAnalytics.listKeys().primarySharedKey
}
}
}
}
resource containerApp 'Microsoft.App/containerApps@2024-03-01' = {
name: appName
location: location
tags: {
'azd-service-name': 'explorer'
}
properties: {
managedEnvironmentId: managedEnvironment.id
configuration: {
activeRevisionsMode: 'Single'
ingress: {
external: true
targetPort: containerPort
transport: 'auto'
allowInsecure: false
}
}
template: {
containers: [
{
name: 'explorer'
image: imageName
env: [
{
name: 'ALLOWED_ORIGINS'
value: allowedOrigins
}
{
name: 'FALKORDB_HOST'
value: falkordbHost
}
{
name: 'FALKORDB_PORT'
value: falkordbPort
}
]
probes: [
{
type: 'Liveness'
httpGet: {
path: '/api/health'
port: containerPort
}
initialDelaySeconds: 20
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
}
]
resources: {
cpu: json('0.5')
memory: '1Gi'
}
}
]
scale: {
minReplicas: 0
maxReplicas: 10
rules: [
{
name: 'http-concurrency'
http: {
metadata: {
concurrentRequests: '100'
}
}
}
]
}
}
}
}
output endpoint string = 'https://${containerApp.properties.configuration.ingress.fqdn}'
+24
View File
@@ -0,0 +1,24 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"environmentName": {
"value": "${AZURE_ENV_NAME}"
},
"location": {
"value": "${AZURE_LOCATION}"
},
"imageName": {
"value": "${SERVICE_EXPLORER_IMAGE_NAME}"
},
"allowedOrigins": {
"value": "*"
},
"falkordbHost": {
"value": "falkordb"
},
"falkordbPort": {
"value": "6379"
}
}
}
+12
View File
@@ -0,0 +1,12 @@
# Fly.io
Deploy from a clean checkout using the root Dockerfile:
```bash
flyctl auth login
flyctl launch --copy-config --config deploy/fly/fly.toml --no-deploy
flyctl secrets set FALKORDB_HOST=localhost FALKORDB_PORT=6379
flyctl deploy --config deploy/fly/fly.toml
```
Change `app` in `fly.toml` before launch if the default app name is already taken.
+30
View File
@@ -0,0 +1,30 @@
app = "semantica-knowledge-explorer"
primary_region = "iad"
kill_signal = "SIGTERM"
kill_timeout = "30s"
[build]
dockerfile = "Dockerfile"
[env]
ALLOWED_ORIGINS = "https://semantica-knowledge-explorer.fly.dev"
FALKORDB_HOST = "localhost"
FALKORDB_PORT = "6379"
[http_service]
internal_port = 8000
force_https = true
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 0
[[http_service.checks]]
grace_period = "20s"
interval = "30s"
method = "GET"
timeout = "5s"
path = "/api/health"
[[vm]]
size = "shared-cpu-1x"
memory = "512mb"
+16
View File
@@ -0,0 +1,16 @@
# GCP Cloud Run
Create the Secret Manager entries, then submit the Cloud Build pipeline:
```bash
gcloud services enable cloudbuild.googleapis.com run.googleapis.com secretmanager.googleapis.com
printf "falkordb-host.example.internal" | gcloud secrets create falkordb-host --data-file=-
printf "6379" | gcloud secrets create falkordb-port --data-file=-
gcloud builds submit --config deploy/gcp/cloudbuild.yaml --substitutions _REGION=us-central1,_SERVICE_NAME=knowledge-explorer
```
For declarative deploys, replace `PROJECT_ID` in `cloudrun-service.yaml`, then run:
```bash
gcloud run services replace deploy/gcp/cloudrun-service.yaml --region us-central1
```
+52
View File
@@ -0,0 +1,52 @@
substitutions:
_REGION: us-central1
_SERVICE_NAME: knowledge-explorer
_IMAGE: gcr.io/$PROJECT_ID/knowledge-explorer
steps:
- name: gcr.io/cloud-builders/docker
args:
- build
- -t
- ${_IMAGE}:$SHORT_SHA
- -t
- ${_IMAGE}:latest
- .
- name: gcr.io/cloud-builders/docker
args:
- push
- ${_IMAGE}:$SHORT_SHA
- name: gcr.io/cloud-builders/docker
args:
- push
- ${_IMAGE}:latest
- name: gcr.io/google.com/cloudsdktool/cloud-sdk
entrypoint: gcloud
args:
- run
- deploy
- ${_SERVICE_NAME}
- --image
- ${_IMAGE}:$SHORT_SHA
- --region
- ${_REGION}
- --platform
- managed
- --allow-unauthenticated
- --port
- "8000"
- --min-instances
- "0"
- --max-instances
- "10"
- --set-env-vars
- ALLOWED_ORIGINS=*
- --set-secrets
- FALKORDB_HOST=falkordb-host:latest,FALKORDB_PORT=falkordb-port:latest
images:
- ${_IMAGE}:$SHORT_SHA
- ${_IMAGE}:latest
+45
View File
@@ -0,0 +1,45 @@
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: knowledge-explorer
annotations:
run.googleapis.com/ingress: all
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/minScale: "0"
autoscaling.knative.dev/maxScale: "10"
spec:
containerConcurrency: 80
timeoutSeconds: 300
containers:
- image: gcr.io/PROJECT_ID/knowledge-explorer:latest
ports:
- name: http1
containerPort: 8000
env:
- name: ALLOWED_ORIGINS
value: "*"
- name: FALKORDB_HOST
valueFrom:
secretKeyRef:
name: falkordb-host
key: latest
- name: FALKORDB_PORT
valueFrom:
secretKeyRef:
name: falkordb-port
key: latest
resources:
limits:
cpu: "1"
memory: 512Mi
livenessProbe:
httpGet:
path: /api/health
port: 8000
initialDelaySeconds: 20
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
+9
View File
@@ -0,0 +1,9 @@
# Helm
Deploy the Knowledge Explorer chart:
```bash
helm lint deploy/helm/knowledge-explorer
helm upgrade --install knowledge-explorer deploy/helm/knowledge-explorer --namespace semantica --create-namespace
helm upgrade --install knowledge-explorer deploy/helm/knowledge-explorer --namespace semantica --create-namespace -f deploy/helm/knowledge-explorer/values.prod.yaml
```
@@ -0,0 +1,6 @@
apiVersion: v2
name: knowledge-explorer
description: Semantica Knowledge Explorer deployment chart
type: application
version: 0.1.0
appVersion: "0.5.0"
+9
View File
@@ -0,0 +1,9 @@
# Knowledge Explorer Helm Chart
```bash
helm lint deploy/helm/knowledge-explorer
helm upgrade --install knowledge-explorer deploy/helm/knowledge-explorer --namespace semantica --create-namespace
helm upgrade --install knowledge-explorer deploy/helm/knowledge-explorer --namespace semantica --create-namespace -f deploy/helm/knowledge-explorer/values.prod.yaml
```
Set `autoscaling.enabled=true` to render the HPA. Put sensitive values in Kubernetes Secrets and reference them outside this chart, or pass non-secret env values through `env`.
@@ -0,0 +1,29 @@
{{- define "knowledge-explorer.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "knowledge-explorer.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- define "knowledge-explorer.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
app.kubernetes.io/name: {{ include "knowledge-explorer.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
{{- define "knowledge-explorer.selectorLabels" -}}
app.kubernetes.io/name: {{ include "knowledge-explorer.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
@@ -0,0 +1,10 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "knowledge-explorer.fullname" . }}
labels:
{{- include "knowledge-explorer.labels" . | nindent 4 }}
data:
{{- range $key, $value := .Values.env }}
{{ $key }}: {{ $value | quote }}
{{- end }}
@@ -0,0 +1,89 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "knowledge-explorer.fullname" . }}
labels:
{{- include "knowledge-explorer.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
{{- include "knowledge-explorer.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "knowledge-explorer.selectorLabels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: explorer
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.targetPort }}
protocol: TCP
envFrom:
- configMapRef:
name: {{ include "knowledge-explorer.fullname" . }}
{{- if .Values.secretEnv }}
env:
{{- range $key, $value := .Values.secretEnv }}
- name: {{ $key }}
valueFrom:
secretKeyRef:
name: {{ $value.secretName }}
key: {{ $value.secretKey }}
{{- end }}
{{- end }}
livenessProbe:
httpGet:
path: {{ .Values.livenessProbe.path }}
port: http
initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.livenessProbe.periodSeconds }}
timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }}
failureThreshold: {{ .Values.livenessProbe.failureThreshold }}
readinessProbe:
httpGet:
path: {{ .Values.readinessProbe.path }}
port: http
initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.readinessProbe.periodSeconds }}
timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }}
failureThreshold: {{ .Values.readinessProbe.failureThreshold }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
@@ -0,0 +1,22 @@
{{- if .Values.autoscaling.enabled -}}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "knowledge-explorer.fullname" . }}
labels:
{{- include "knowledge-explorer.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "knowledge-explorer.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
@@ -0,0 +1,41 @@
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "knowledge-explorer.fullname" . }}
labels:
{{- include "knowledge-explorer.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- with .Values.ingress.className }}
ingressClassName: {{ . }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "knowledge-explorer.fullname" $ }}
port:
name: http
{{- end }}
{{- end }}
{{- end }}
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "knowledge-explorer.fullname" . }}
labels:
{{- include "knowledge-explorer.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "knowledge-explorer.selectorLabels" . | nindent 4 }}
@@ -0,0 +1,27 @@
image:
repository: ghcr.io/semantica-agi/semantica-knowledge-explorer
tag: latest
pullPolicy: IfNotPresent
ingress:
enabled: true
hosts:
- host: knowledge-explorer.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: knowledge-explorer-tls
hosts:
- knowledge-explorer.example.com
env:
ALLOWED_ORIGINS: https://knowledge-explorer.example.com
FALKORDB_HOST: falkordb.semantic-data.svc.cluster.local
FALKORDB_PORT: "6379"
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 75
@@ -0,0 +1,83 @@
replicaCount: 2
image:
repository: semantica-knowledge-explorer
pullPolicy: IfNotPresent
tag: latest
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
podAnnotations: {}
podLabels: {}
podSecurityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
service:
type: ClusterIP
port: 80
targetPort: 8000
ingress:
enabled: false
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: knowledge-explorer.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: knowledge-explorer-tls
hosts:
- knowledge-explorer.example.com
env:
ALLOWED_ORIGINS: https://knowledge-explorer.example.com
FALKORDB_HOST: falkordb
FALKORDB_PORT: "6379"
secretEnv: {}
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
livenessProbe:
path: /api/health
initialDelaySeconds: 20
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
path: /api/health
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}
+12
View File
@@ -0,0 +1,12 @@
# Kubernetes
Apply the raw manifests with Kustomize:
```bash
cp deploy/kubernetes/secret.yaml.example deploy/kubernetes/secret.yaml
kubectl apply -f deploy/kubernetes/secret.yaml
kubectl apply -k deploy/kubernetes
kubectl -n semantica rollout status deployment/knowledge-explorer
```
Update the image name and ingress host before deploying to production. `secret.yaml` is intentionally ignored from the kustomization; keep only `secret.yaml.example` in git.
+9
View File
@@ -0,0 +1,9 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: knowledge-explorer-config
namespace: semantica
data:
ALLOWED_ORIGINS: "https://knowledge-explorer.example.com"
FALKORDB_HOST: "falkordb"
FALKORDB_PORT: "6379"
+69
View File
@@ -0,0 +1,69 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: knowledge-explorer
namespace: semantica
labels:
app.kubernetes.io/name: knowledge-explorer
app.kubernetes.io/part-of: semantica
spec:
replicas: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app.kubernetes.io/name: knowledge-explorer
template:
metadata:
labels:
app.kubernetes.io/name: knowledge-explorer
app.kubernetes.io/part-of: semantica
spec:
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: explorer
image: semantica-knowledge-explorer:latest
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8000
envFrom:
- configMapRef:
name: knowledge-explorer-config
- secretRef:
name: knowledge-explorer-secrets
optional: true
livenessProbe:
httpGet:
path: /api/health
port: http
initialDelaySeconds: 20
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /api/health
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
+24
View File
@@ -0,0 +1,24 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: knowledge-explorer
namespace: semantica
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts:
- knowledge-explorer.example.com
secretName: knowledge-explorer-tls
rules:
- host: knowledge-explorer.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: knowledge-explorer
port:
name: http
+8
View File
@@ -0,0 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- configmap.yaml
- deployment.yaml
- service.yaml
- ingress.yaml
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: semantica
+8
View File
@@ -0,0 +1,8 @@
apiVersion: v1
kind: Secret
metadata:
name: knowledge-explorer-secrets
namespace: semantica
type: Opaque
stringData:
FALKORDB_PASSWORD: "replace-me-if-your-falkordb-requires-auth"
+15
View File
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: knowledge-explorer
namespace: semantica
labels:
app.kubernetes.io/name: knowledge-explorer
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: knowledge-explorer
ports:
- name: http
port: 80
targetPort: http
+15
View File
@@ -0,0 +1,15 @@
# Railway
Deploys the Knowledge Explorer from the root `Dockerfile` and checks `/api/health`.
```bash
railway login
railway init
railway add --database redis
railway variable --set "FALKORDB_HOST=${{Redis.REDISHOST}}"
railway variable --set "FALKORDB_PORT=${{Redis.REDISPORT}}"
railway variable --set "ALLOWED_ORIGINS=https://${{RAILWAY_PUBLIC_DOMAIN}}"
railway up
```
The Redis plugin variables are wired to the requested FalkorDB env names for deployment compatibility. The Explorer currently reads these settings but does not persist graph state to FalkorDB.
+9
View File
@@ -0,0 +1,9 @@
[build]
builder = "DOCKERFILE"
dockerfilePath = "Dockerfile"
[deploy]
healthcheckPath = "/api/health"
healthcheckTimeout = 300
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 10
+11
View File
@@ -0,0 +1,11 @@
# Render
This Blueprint provisions a Docker web service plus a Render Key Value instance and wires the datastore host/port into the Explorer env vars.
```bash
render login
render blueprints validate deploy/render/render.yaml
render blueprint apply deploy/render/render.yaml
```
After creation, update `ALLOWED_ORIGINS` in the Render dashboard if you attach a custom domain.
+29
View File
@@ -0,0 +1,29 @@
services:
- type: web
name: semantica-knowledge-explorer
runtime: docker
plan: starter
region: oregon
dockerfilePath: ./Dockerfile
dockerContext: .
healthCheckPath: /api/health
envVars:
- key: ALLOWED_ORIGINS
value: https://semantica-knowledge-explorer.onrender.com
- key: FALKORDB_HOST
fromService:
type: keyvalue
name: semantica-explorer-redis
property: host
- key: FALKORDB_PORT
fromService:
type: keyvalue
name: semantica-explorer-redis
property: port
- type: keyvalue
name: semantica-explorer-redis
plan: starter
ipAllowList: []
maxmemoryPolicy: noeviction
persistenceMode: snapshot
+42
View File
@@ -0,0 +1,42 @@
services:
explorer:
command:
- python
- -m
- uvicorn
- semantica.explorer.app:app
- --host
- 0.0.0.0
- --port
- "8000"
- --reload
- --reload-dir
- /app/semantica
environment:
ALLOWED_ORIGINS: http://localhost:5173,http://127.0.0.1:5173,http://localhost:8000,http://127.0.0.1:8000
FALKORDB_HOST: falkordb
FALKORDB_PORT: "6379"
volumes:
- ./semantica:/app/semantica
- ./pyproject.toml:/app/pyproject.toml:ro
frontend:
image: node:22-alpine
working_dir: /app/explorer
command: sh -c "npm ci && npm run dev -- --host 0.0.0.0"
environment:
VITE_EXPLORER_API_TARGET: http://explorer:8000
VITE_EXPLORER_WS_TARGET: ws://explorer:8000
ports:
- "5173:5173"
volumes:
- ./explorer:/app/explorer
- explorer_node_modules:/app/explorer/node_modules
depends_on:
explorer:
condition: service_started
networks:
- semantica
volumes:
explorer_node_modules:
+25 -1
View File
@@ -1,11 +1,35 @@
services: services:
explorer:
build:
context: .
dockerfile: Dockerfile
image: semantica-knowledge-explorer:latest
environment:
FALKORDB_HOST: falkordb
FALKORDB_PORT: "6379"
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-http://localhost:8000,http://127.0.0.1:8000}
depends_on:
falkordb:
condition: service_started
ports:
- "8000:8000"
networks:
- semantica
restart: unless-stopped
falkordb: falkordb:
image: falkordb/falkordb:latest image: falkordb/falkordb:latest
ports: ports:
- "6379:6379" - "6379:6379"
volumes: volumes:
- falkordb_data:/data - falkordb_data:/data
restart: always networks:
- semantica
restart: unless-stopped
networks:
semantica:
driver: bridge
volumes: volumes:
falkordb_data: falkordb_data:
+5 -2
View File
@@ -2,6 +2,9 @@ import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import path from 'path' import path from 'path'
const apiTarget = process.env.VITE_EXPLORER_API_TARGET ?? 'http://127.0.0.1:8000'
const wsTarget = process.env.VITE_EXPLORER_WS_TARGET ?? apiTarget.replace(/^http/, 'ws')
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
@@ -57,11 +60,11 @@ export default defineConfig({
server: { server: {
proxy: { proxy: {
'/api': { '/api': {
target: 'http://127.0.0.1:8000', target: apiTarget,
changeOrigin: true, changeOrigin: true,
}, },
'/ws': { '/ws': {
target: 'ws://127.0.0.1:8000', target: wsTarget,
ws: true, ws: true,
}, },
}, },
+39 -10
View File
@@ -1,4 +1,4 @@
""" """
Semantica Explorer FastAPI application factory. Semantica Explorer FastAPI application factory.
""" """
@@ -14,10 +14,36 @@ from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from .. import __version__ from .. import __version__
from ..context.context_graph import ContextGraph
from .session import GraphSession from .session import GraphSession
from .ws import ConnectionManager from .ws import ConnectionManager
def _read_int_env(name: str, default: int) -> int:
raw_value = os.environ.get(name)
if raw_value is None or raw_value.strip() == "":
return default
try:
return int(raw_value)
except ValueError:
return default
def _read_explorer_settings() -> dict:
raw_origins = (
os.environ.get("ALLOWED_ORIGINS")
or os.environ.get("EXPLORER_CORS_ORIGINS")
or "http://localhost:5173,http://127.0.0.1:5173"
)
return {
"allowed_origins": [
origin.strip() for origin in raw_origins.split(",") if origin.strip()
],
"falkordb_host": os.environ.get("FALKORDB_HOST", "localhost"),
"falkordb_port": _read_int_env("FALKORDB_PORT", 6379),
}
def _install_mutation_bridge(app: FastAPI, session: GraphSession) -> None: def _install_mutation_bridge(app: FastAPI, session: GraphSession) -> None:
previous_callback = getattr(session.graph, "mutation_callback", None) previous_callback = getattr(session.graph, "mutation_callback", None)
@@ -43,13 +69,15 @@ def _install_mutation_bridge(app: FastAPI, session: GraphSession) -> None:
def create_app(session: Optional[GraphSession] = None) -> FastAPI: def create_app(session: Optional[GraphSession] = None) -> FastAPI:
active_session = session or GraphSession(ContextGraph(advanced_analytics=False))
settings = _read_explorer_settings()
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
app.state.event_loop = asyncio.get_running_loop() app.state.event_loop = asyncio.get_running_loop()
app.state.ws_manager = ConnectionManager() app.state.ws_manager = ConnectionManager()
if session is not None: app.state.session = active_session
app.state.session = session _install_mutation_bridge(app, active_session)
_install_mutation_bridge(app, session)
yield yield
app = FastAPI( app = FastAPI(
@@ -59,10 +87,11 @@ def create_app(session: Optional[GraphSession] = None) -> FastAPI:
lifespan=lifespan, lifespan=lifespan,
) )
_raw_origins = os.environ.get( app.state.explorer_settings = settings
"EXPLORER_CORS_ORIGINS", "http://localhost:5173,http://127.0.0.1:5173" app.state.falkordb_host = settings["falkordb_host"]
) app.state.falkordb_port = settings["falkordb_port"]
_cors_origins = [o.strip() for o in _raw_origins.split(",") if o.strip()] app.state.allowed_origins = settings["allowed_origins"]
# allow_credentials lets browsers send cookies/auth headers cross-origin. # allow_credentials lets browsers send cookies/auth headers cross-origin.
# The Explorer has no authentication, so credentials serve no purpose and # The Explorer has no authentication, so credentials serve no purpose and
# enabling them when origins are broadened creates cross-site request risk. # enabling them when origins are broadened creates cross-site request risk.
@@ -71,7 +100,7 @@ def create_app(session: Optional[GraphSession] = None) -> FastAPI:
_allow_credentials = os.environ.get("EXPLORER_CORS_CREDENTIALS", "false").lower() == "true" _allow_credentials = os.environ.get("EXPLORER_CORS_CREDENTIALS", "false").lower() == "true"
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=_cors_origins, allow_origins=settings["allowed_origins"],
allow_credentials=_allow_credentials, allow_credentials=_allow_credentials,
allow_methods=["GET", "POST", "DELETE", "OPTIONS"], allow_methods=["GET", "POST", "DELETE", "OPTIONS"],
allow_headers=["Content-Type", "Authorization"], allow_headers=["Content-Type", "Authorization"],
@@ -169,7 +198,7 @@ def create_app(session: Optional[GraphSession] = None) -> FastAPI:
@app.get("/api/health") @app.get("/api/health")
async def health(): async def health():
return {"status": "healthy"} return {"status": "ok"}
@app.get("/api/info") @app.get("/api/info")
async def info(): async def info():
+36 -2
View File
@@ -1,4 +1,4 @@
"""Integration tests for the explorer API.""" """Integration tests for the explorer API."""
import json import json
from pathlib import Path from pathlib import Path
@@ -151,7 +151,7 @@ class TestHealthInfo:
def test_health(self, client): def test_health(self, client):
response = client.get("/api/health") response = client.get("/api/health")
assert response.status_code == 200 assert response.status_code == 200
assert response.json()["status"] == "healthy" assert response.json() == {"status": "ok"}
def test_info(self, client): def test_info(self, client):
response = client.get("/api/info") response = client.get("/api/info")
@@ -161,6 +161,40 @@ class TestHealthInfo:
assert payload["status"] == "active" assert payload["status"] == "active"
assert payload["version"] assert payload["version"]
def test_env_settings_are_read_from_supported_names(self, monkeypatch):
monkeypatch.setenv(
"ALLOWED_ORIGINS",
"https://app.example.com, https://team.example.com",
)
monkeypatch.setenv("FALKORDB_HOST", "falkordb.internal")
monkeypatch.setenv("FALKORDB_PORT", "6380")
app = create_app()
assert app.state.allowed_origins == [
"https://app.example.com",
"https://team.example.com",
]
assert app.state.falkordb_host == "falkordb.internal"
assert app.state.falkordb_port == 6380
def test_env_settings_fall_back_to_legacy_cors_name(self, monkeypatch):
monkeypatch.delenv("ALLOWED_ORIGINS", raising=False)
monkeypatch.setenv("EXPLORER_CORS_ORIGINS", "https://legacy.example.com")
app = create_app()
assert app.state.allowed_origins == ["https://legacy.example.com"]
def test_default_app_initializes_empty_graph_session(self):
with TestClient(create_app()) as test_client:
response = test_client.get("/api/graph/nodes")
assert response.status_code == 200
payload = response.json()
assert payload["nodes"] == []
assert payload["total"] == 0
class TestGraphNodes: class TestGraphNodes:
def test_list_nodes(self, client): def test_list_nodes(self, client):