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
9 changes: 4 additions & 5 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
# The root Dockerfile builds only server/, so exclude the whole repo and add that one
# subtree back. An allow-list keeps the context minimal without needing an update every
# time the repo grows a new top-level directory.
# The root Dockerfile builds only server/, so exclude everything and add that subtree back.
# An allow-list keeps the context minimal as the repo grows new top-level directories.
*
!server/

# Re-exclude what lives under server/ but never belongs in an image layer: a local
# virtualenv (larger than the image itself), local secrets, and build droppings.
# Re-exclude what lives under server/ but never belongs in a layer: a local virtualenv
# (larger than the image), local secrets, and build droppings.
server/.venv
server/.env
server/.env.*
Expand Down
24 changes: 14 additions & 10 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# Copy to .env for local development. On Render these are set by render.yaml, except
# OPENAI_API_KEY, which Render prompts for at deploy time.
#
# Optional settings are left commented out rather than assigned an empty value. An
# empty assignment is not the same as unset: it reaches the app as '' and is taken as
# a real setting. Uncomment a line to use it.
# Optional settings stay commented out rather than assigned an empty value: '' is not the
# same as unset, and arrives at the app as a real setting. Uncomment a line to use it.

# --- Required ---------------------------------------------------------------------
# Graphiti calls an LLM to extract entities and facts. https://platform.openai.com/api-keys
Expand All @@ -13,11 +12,10 @@ OPENAI_API_KEY=
# Bearer token required by every endpoint except /healthcheck. Mandatory, with no way to switch
# auth off; blank counts as missing.
#
# Render generates it when the service is first created, and `docker compose` defaults it to
# insecure-local-dev-key — so leave this unset unless you want your own value, which is then what
# the local stack and the curl examples expect. Must be 16+ printable-ASCII characters: the server
# budgets failed attempts, but that can't save a key like `dev`, and header values travel as
# latin-1 with clients encoding anything outside ASCII inconsistently.
# Render generates it on first deploy and `docker compose` defaults it to insecure-local-dev-key,
# so leave this unset unless you want your own value — which the local stack and the curl examples
# then expect. Must be 16+ printable-ASCII characters: the server budgets failed attempts, but
# that can't save a key like `dev`, and headers travel as latin-1.
# GRAPHITI_API_KEY=

# --- Graph backend ----------------------------------------------------------------
Expand All @@ -39,11 +37,17 @@ FALKORDB_DATABASE=default_db
# Bolt port; read by docker-compose.yml, which defaults it to 7687.
# NEO4J_PORT=

# --- Serving ----------------------------------------------------------------------
# The port uvicorn binds to, set for you in render.yaml. Setting it here changes nothing under
# docker compose: both API services pin it to 8000, which is what their healthchecks and
# published ports name. Edit docker-compose.yml's mapping to move where the stack is reachable.
# PORT=8000

# --- Model ------------------------------------------------------------------------
# Any OpenAI model id; leave unset to use graphiti-core's default.
MODEL_NAME=gpt-5.5
# Concurrent LLM calls during ingestion. graphiti-core defaults to 20; this is lower to
# stay under the rate limits on a fresh OpenAI account.
# Concurrent LLM calls during ingestion. Below graphiti-core's default of 20, to stay under
# the rate limits on a fresh OpenAI account.
SEMAPHORE_LIMIT=10
# Embedding model; leave unset for graphiti-core's default (text-embedding-3-small).
# EMBEDDING_MODEL_NAME=
Expand Down
37 changes: 15 additions & 22 deletions .github/workflows/server-tests.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
name: Server Tests

# Tests for the Graphiti graph_service (REST API), in two jobs, both on PRs and pushes to
# main that touch the server:
# Tests for graph_service (the REST API), on PRs and pushes to main that touch the server:
#
# - unit-tests: everything needing no secrets and no database, notably the auth guards in
# tests/test_auth.py. Runs on forks too, so a router included without its auth dependency
Expand All @@ -24,8 +23,8 @@ on:
permissions:
contents: read

# Cancel superseded runs on the same ref so repeated pushes don't keep paying for
# real OpenAI calls.
# Cancel superseded runs on the same ref, so repeated pushes don't keep paying for real
# OpenAI calls.
concurrency:
group: server-tests-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
Expand All @@ -50,8 +49,8 @@ jobs:
run: uv sync --extra dev
- name: Run unit tests
working-directory: server
# By marker, not filename, so a new module is picked up by default and only opts
# out by marking itself integration. --strict-markers makes a typo an error.
# By marker, not filename, so a new module is picked up by default and opts out only
# by marking itself integration. --strict-markers makes a typo an error.
run: uv run pytest -m "not integration" --strict-markers -p no:cacheprovider

live-server-tests:
Expand All @@ -70,9 +69,8 @@ jobs:
with:
version: "latest"
- name: Start FalkorDB
# Run the database on the host network rather than a `services:` block: on
# the depot-ubuntu runner pool bridge-network NAT silently drops packets to
# published service-container ports. Readiness is checked via `docker exec`.
# Host network rather than a `services:` block: on the depot-ubuntu runner pool,
# bridge-network NAT silently drops packets to published service-container ports.
run: docker run -d --name falkordb --network host falkordb/falkordb:latest
- name: Install dependencies
working-directory: server
Expand All @@ -93,21 +91,16 @@ jobs:
DB_BACKEND: falkordb
FALKORDB_HOST: localhost
FALKORDB_PORT: "6379"
# gpt-5.5 is the graphiti-core default, but it requires a key/account
# with fast gpt-5.5 access; CI pins a lighter, broadly-available model so
# the live test stays fast and reliable. It exercises the same pipeline.
# graphiti-core defaults to gpt-5.5, which needs an account with fast access to it.
# A lighter, broadly-available model exercises the same pipeline.
MODEL_NAME: gpt-4.1-mini
# Raise episode/LLM-call concurrency so the live episode processes quickly.
# Raised so the live episode processes quickly.
SEMAPHORE_LIMIT: "20"
# pytest exits 5 when no tests are collected. The legitimate case is a fork
# PR with no OPENAI_API_KEY: the suite self-skips at module level. When the
# key IS present (same-repo run) exit 5 is unexpected — the live suite should
# have run — so fail loudly rather than report a green build that tested
# nothing. Likely causes: FalkorDB became unreachable (the suite also skips at
# module level when it can't connect, even though the prior step gated it), or
# the `integration` marker was renamed/removed so the selection matches
# nothing. Real failures (exit 1), import errors (exit 2), and usage errors
# such as a renamed test-file path (exit 4) always fail the job too.
# pytest exits 5 when nothing is collected. The legitimate case is a fork PR with no
# OPENAI_API_KEY, where the suite self-skips at module level. With the key present, exit
# 5 means the live suite should have run and didn't — FalkorDB unreachable, or the
# `integration` marker renamed — so fail rather than report a green build that tested
# nothing. Real failures (1), import errors (2), and usage errors (4) fail the job too.
run: |
set +e
uv run pytest tests/test_live_falkordb_int.py -m integration -p no:cacheprovider
Expand Down
32 changes: 12 additions & 20 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
# syntax=docker/dockerfile:1.9
FROM python:3.12-slim

# The graphiti-core release installed from PyPI below, and the value of the version
# labels. This default is the single source of truth for the pin: render.yaml,
# docker-compose.yml, and local builds all inherit it, so a bump happens here and
# nowhere else. It is a PyPI release number, not this repo's own version.
# The graphiti-core PyPI release installed below, and the version labels. Single source of
# truth for the pin: render.yaml, docker-compose.yml, and local builds all inherit it.
#
# 0.29.2 in particular does not work on FalkorDB: it writes episodes fine but reads them
# back empty, so /search and /episodes return nothing. Verify a write-then-search round
# trip before bumping. Release CI overrides this with the version it is publishing.
# 0.29.2 in particular is broken on FalkorDB — it writes episodes but reads them back empty,
# so /search returns nothing. Verify a write-then-search round trip before bumping.
ARG GRAPHITI_VERSION=0.29.3

# Inherit build arguments for labels
Expand Down Expand Up @@ -49,9 +46,8 @@ WORKDIR /app
COPY ./server/pyproject.toml ./server/README.md ./server/uv.lock ./
COPY ./server/graph_service ./graph_service

# Install server dependencies (without graphiti-core from lockfile)
# Then install graphiti-core from PyPI at the desired version
# This prevents the stale lockfile from pinning an old graphiti-core version
# Server deps from the lockfile, then graphiti-core from PyPI, so the stale lockfile
# can't pin an old graphiti-core.
ARG INSTALL_FALKORDB=false
RUN --mount=type=cache,target=/root/.cache/uv \
: "${GRAPHITI_VERSION:?must be a graphiti-core release from PyPI}" && \
Expand All @@ -76,15 +72,11 @@ USER app
ENV PORT=8000
EXPOSE $PORT

# Shell form, so $PORT actually reaches uvicorn: the exec form does no variable expansion,
# which is why this used to hardcode 8000 and ignore the PORT it declares above. `:-8000`
# covers PORT set but empty, which a dashboard or a .env makes easy and which is not the
# same as unset. `exec` replaces the shell, so uvicorn is PID 1 and gets SIGTERM directly
# on deploy or shutdown, rather than the shell absorbing it until the runtime kills us.
# Shell form, so $PORT reaches uvicorn at all — the exec form does no expansion. `:-8000`
# covers PORT set but empty, which a dashboard or a .env makes easy. `exec` keeps uvicorn as
# PID 1, so it gets SIGTERM directly instead of the shell absorbing it.
#
# uvicorn straight from the venv, not `uv run --no-sync uvicorn`: uv is installed under
# /root/.local/bin, and /root is 0700, so the app user cannot resolve it through PATH. The
# exec form got away with it because the runtime resolves the binary as root before
# dropping privileges; a shell doing its own PATH lookup gets EACCES. PATH already puts
# /app/.venv/bin first, so this is the same interpreter uv run would have selected.
# uvicorn from the venv, not `uv run`: uv lives under /root/.local/bin and /root is 0700, so
# the app user's PATH lookup gets EACCES. Same interpreter either way — PATH puts
# /app/.venv/bin first.
CMD ["sh", "-c", "exec uvicorn graph_service.main:app --host 0.0.0.0 --port ${PORT:-8000}"]
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ from `graphiti-api` over Render's private network.
| ----------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MODEL_NAME` | `gpt-5.5` | Any OpenAI model id. |
| `SEMAPHORE_LIMIT` | `10` | Concurrent LLM calls during ingestion. Deliberately below graphiti-core's default of 20, so a burst of episodes doesn't trip the rate limits on a fresh OpenAI key. Raise it once you know your account's limits. |
| `PORT` | `8000` | The port uvicorn binds to and the one Render routes to. Declared up front so the first deploy doesn't restart to rewire the network after detecting the port itself. Safe to change. |

The rest is wiring you shouldn't need to touch. `DB_BACKEND` selects the FalkorDB code
path; `FALKORDB_HOST` is filled in from the private service's hostname, with
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.test.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
services:
graph:
image: graphiti-service:${GITHUB_SHA}
# Localhost-bound, as in docker-compose.yml: the key below is committed to this repo.
# Localhost-bound, as in docker-compose.yml: the key below is committed.
ports:
- "127.0.0.1:8000:8000"
healthcheck:
Expand Down
42 changes: 20 additions & 22 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ services:
profiles: [""]
build:
context: .
# Localhost-bound, not the bare "8000:8000": the key below is committed to this repo, so
# publishing on every interface would hand the graph to anyone on a shared network. Drop the
# prefix to reach this from another machine, and set your own key in the same edit.
# Localhost-bound, not the bare "8000:8000": the key below is committed, so publishing on
# every interface would hand the graph to anyone on the network. Drop the prefix to reach
# this from another machine, and set your own key in the same edit.
ports:
- "127.0.0.1:8000:8000"
healthcheck:
Expand All @@ -22,16 +22,15 @@ services:
depends_on:
neo4j:
condition: service_healthy
# Everything else the app reads comes straight from .env, so the settings documented
# in .env.example reach the container instead of being silently dropped. Listing them
# here one by one would mean a blank value for each one the user left out, and a blank
# is not the same as unset: SEMAPHORE_LIMIT is read as int(os.getenv(...)), so '' is a
# crash on import. Absent from .env means absent from the environment.
# Everything else comes straight from .env, so the settings documented in .env.example
# reach the container. Listing them individually would pass a blank for each one left
# out, and blank is not unset: SEMAPHORE_LIMIT is read as int(os.getenv(...)), so '' is a
# crash on import.
env_file:
- path: .env
required: false
# Compose-specific wiring, which has to win over whatever .env says: these hostnames
# only make sense on the compose network. environment takes precedence over env_file.
# Compose-specific wiring, which has to win over .env these hostnames only exist on the
# compose network. environment takes precedence over env_file.
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
# Mandatory, so the stack needs one to boot. Named insecure- because it is committed and
Expand All @@ -41,10 +40,9 @@ services:
- NEO4J_USER=${NEO4J_USER:-neo4j}
- NEO4J_PASSWORD=${NEO4J_PASSWORD:-password}
- DB_BACKEND=neo4j
# Now that the Dockerfile's CMD honours PORT, pin it: the mapping above and the
# healthcheck below both name 8000 literally, so a PORT inherited from .env would
# move the server out from under them. Change the left-hand side of the mapping to
# publish somewhere else.
# Pinned because the CMD honours PORT: the mapping and healthcheck both name 8000, so a
# PORT inherited from .env would move the server out from under them. To publish
# elsewhere, change the left-hand side of the mapping.
- PORT=8000
neo4j:
image: neo4j:5.26.2
Expand All @@ -59,7 +57,7 @@ services:
timeout: 10s
retries: 10
start_period: 3s
# Localhost-bound, as above, with more at stake: NEO4J_PASSWORD defaults to `password` and
# Localhost-bound, as above, with more at stake: the password defaults to `password` and
# Bolt is unmediated write access.
ports:
- "127.0.0.1:7474:7474" # HTTP
Expand All @@ -73,16 +71,16 @@ services:
# Kept in step with render.yaml so local dev matches what Render runs.
image: falkordb/falkordb:v4.20.1
profiles: ["falkordb"]
# Localhost-bound, as above. FalkorDB has no password at all; on Render it is a pserv
# reachable only from graphiti-api, and this is the closest local equivalent.
# Localhost-bound, as above, and FalkorDB has no password at all. On Render it's a pserv
# reachable only from graphiti-api; this is the closest local equivalent.
ports:
- "127.0.0.1:6379:6379"
volumes:
- falkordb_data:/var/lib/falkordb/data
environment:
# REDIS_ARGS goes to redis-server; FALKORDB_ARGS goes to the module. The flags that
# used to sit in FALKORDB_ARGS were redis-server flags in the module's slot, and
# both were already the defaults. Append-only matches what render.yaml runs.
# REDIS_ARGS goes to redis-server, FALKORDB_ARGS to the module. The flags that used to
# sit in FALKORDB_ARGS were redis-server flags in the module's slot, and both were
# already the defaults. Append-only matches render.yaml.
- REDIS_ARGS=--appendonly yes
healthcheck:
test: ["CMD", "redis-cli", "-p", "6379", "ping"]
Expand Down Expand Up @@ -119,8 +117,8 @@ services:
- FALKORDB_PORT=6379
- FALKORDB_DATABASE=default_db
- DB_BACKEND=falkordb
# Pinned for the same reason as the graph service above. Published on 8001 to leave
# 8000 free for the neo4j profile; the container still listens on 8000.
# Pinned as on the graph service above. Published on 8001 to leave 8000 free for the
# neo4j profile; the container still listens on 8000.
- PORT=8000

volumes:
Expand Down
Loading
Loading