A Python API to serve data from the knowledge graph for the Sustainable Energy Academy.
Warning
The package is currently undergoing a major revamp. Some features may be missing or not working as intended. Feel free to open an issue.
Follow the steps below to run the API locally.
- Clone the repository and navigate to the project folder.
- Create and activate a virtual environment.
- Create and populate the
.envfile based on.env.example. - Run
make installto install project dependencies. - To launch the API, run
make run. The API will be running at http://127.0.0.1:8000.
git clone https://github.com/UNDP-Data/dsc-sea-ai-api
cd dsc-sea-ai-api
python -m venv .venv
source .venv/bin/activate
make install
make run
# INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)For Azure Blob auth you can use either:
STORAGE_SAS_URL(full SAS URL), orSTORAGE_ACCOUNT_NAMEwithSTORAGE_ACCOUNT_KEY/STORAGE_SAS_TOKEN.
For local-only development or assistant-kit testing, set LANCEDB_URI to a
filesystem directory. When this is set, the backend uses local LanceDB storage
and does not require Azure Blob credentials:
export LANCEDB_URI="/path/to/local-lancedb"If that local database contains assistant corpus tables but no nodes/edges
knowledge-graph tables, the API uses a small built-in fallback graph so /nodes,
/graph, and /graph/v2 remain testable offline.
Optional local tester env (separate tester app):
KG_TESTER_API_KEY(or fallbackAPI_KEY) for tester proxy authentication to backend.KG_TESTER_LOCAL_API_BASE_URLfor local backend target (defaulthttp://127.0.0.1:8000).KG_TESTER_REMOTE_API_BASE_URLto prefill remote backend target (defaulthttps://sea-ai-api.azurewebsites.net).
All protected endpoints require X-Api-Key.
-
GET /documents[?pattern=<text>&limit=<int>]- Searches canonical publication records in the
documentstable when available. - Response fields include legacy metadata plus additive fields such as:
document_idsourcepublisherdocument_typepublication_dateseries_nametopicsgeographies
- Searches canonical publication records in the
-
GET /sources- Lists source registry records from the
sourcestable when available.
- Lists source registry records from the
GET /graph?query=<text>[&hops=<int>]- Response:
nodes[]withname,description,neighbourhood,weight,colouredges[]withsubject,predicate,object,description,weight
GET /graph/v2?query=<text>- Response:
nodes[]withname,description,tier,weight,colourtieris one of:central,secondary,periphery
edges[]withsubject,predicate,object,description,weightlevelis not returned in the API payload.
POST /model[?graph_version=v1|v2]graph_versiondefault isv2.- Request body must include at least one message; empty lists return
400. - Response includes
X-Request-Idheader for request correlation. - The stream is NDJSON; graph and text generation run in parallel.
- A deterministic scope guard runs before generation. Off-topic, prompt-extraction, and clearly unsafe requests are blocked before they reach the model or retrieval stack.
- Chunk order is not fixed. The graph chunk usually arrives early, but clients must handle it arriving before, during, or after text deltas.
- To avoid stalled streams under slow storage/model conditions,
/modelapplies:MODEL_GRAPH_TIMEOUT_SECONDS(graph build timeout),MODEL_STREAM_IDLE_TIMEOUT_SECONDS(max idle gap between streamed chunks),MODEL_TOOLS_PREP_TIMEOUT_SECONDS(SQL/RAG tool preparation timeout),MODEL_STREAM_WATCHDOG_SECONDS(overall no-progress watchdog for stream completion),RETRIEVE_CHUNKS_TIMEOUT_SECONDS(RAG chunk retrieval timeout).
- This controls the schema of
graphreturned in streamed/modelchunks:v1: legacy graph schema (neighbourhoodon nodes,levelon edges)v2: staged graph schema (tieron nodes, no edgelevel)
- Publication retrieval is now document-centric when a
documentstable is present:- the backend ranks canonical document records first,
- then loads chunks only from shortlisted approved documents,
- and streams
documents[]references separately from answer text.
The retrieval corpus now supports a document-centric layout:
sources- canonical source registry with publisher / authority metadata
documents- canonical publication records with approval status, source linkage, document type, publication date, topical tags, geography tags, and quality signals
chunks- chunk-level text records linked to
document_id
- chunk-level text records linked to
The API remains backward-compatible with the previous chunk-only flow. If documents
or sources tables do not exist yet, retrieval falls back to the legacy chunk-first path.
Knowledge graph logic is versioned under:
src/kg/v1.pyfor V1 graph assemblysrc/kg/v2.pyfor V2 staged graph assemblysrc/kg/types.pyfor V2 response models
Compatibility adapters are retained:
src/entities_v2.pysrc/graph_v2.py
The KG tester is now a separate frontend app in frontend/. The backend API deployment
does not include the tester route.
- Start the API backend locally (
make run). - Start the tester frontend app (
make run-tester). - Open http://127.0.0.1:8010/kg-tester.
- Enter:
- target (
Local serverorRemote API), - graph version (
default,v1, orv2, sent asgraph_versionto/modelwhen selected), - a graph
query(for exampleclimate change mitigation), - remote API base URL (only when target is
Remote API).
- target (
- Submit to call
/modeland view:- streamed answer text (delta chunks),
- query ideas (clickable chips that trigger a new query),
- graph payload returned in the stream,
- an interactive D3 force graph,
- the raw JSON response payload.
Interaction shortcuts:
- Press
Enterin the form to run the current query. - Click a graph node to run a follow-up query (
tell me more about <node>). - Click an idea chip to run that suggestion immediately.
Notes:
- The tester proxy uses
KG_TESTER_API_KEY(orAPI_KEY) from environment and does not expose API keys in browser JavaScript. - The tester proxy forwards
X-Request-Id(or generates one) to simplify backend trace correlation. - The tester app calls backend APIs server-to-server, so browser CORS is not required for remote targets.
- No extra CORS proxy is needed when using the standalone tester app.
- The tester app is local-only by design (loopback clients only).
Example local startup:
# terminal 1
make run
# terminal 2
export KG_TESTER_API_KEY="$API_KEY"
make run-testermake run-tester binds to 127.0.0.1:8010 by default.
The SGP AI tester is a separate local-only frontend for the sgp_ai assistant.
It proxies to /assistants/sgp_ai/model and /assistants/sgp_ai/debug/retrieve
without exposing API keys in browser JavaScript.
Recommended startup with automatic port fallback:
cd "/Users/ben/Documents/UNDP/SEH/dsc-energy-ai-backend"
.venv/bin/python scripts/run_sgp_ai_local.py --reloadThe launcher starts the backend and tester UI, reuses an existing backend when
one is already serving the API, and moves the tester to the next free port when
8015 is occupied. Use the Tester UI: URL printed by the launcher.
Example local startup when port 8000 is already in use:
# terminal 1
.venv/bin/uvicorn main:app --host 127.0.0.1 --port 8016
# terminal 2
SGP_TESTER_API_BASE_URL=http://127.0.0.1:8016 make run-sgp-testerIf 8015 is already in use, set a different tester port manually:
SGP_TESTER_API_BASE_URL=http://127.0.0.1:8016 SGP_TESTER_PORT=8017 make run-sgp-testerOpen http://127.0.0.1:8015/sgp-ai-tester, or the alternate tester URL printed by the launcher/manual port you selected.
The tester checks both assistant profile installation and corpus readiness. If it
shows SGP AI installed; corpus missing, refresh storage credentials and import
the corpus:
export LANCEDB_URI="/Users/ben/Documents/UNDP/SGP/Innovation Library/.local_lancedb"
.venv/bin/python scripts/install_assistant_kit.py \
--kit "/Users/ben/Documents/UNDP/SGP/SGP AI/sgp_ai" \
--overwrite \
--import-corpus \
--include-chunksPre-commit validation:
python3 -m py_compile main.py src/security.py src/database.py src/genai.py src/entities.py src/kg/__init__.py src/kg/v1.py src/kg/v2.py frontend/kg_tester_app.py tests/test_model.py tests/test_genai.py
make test # requires dev dependencies installedTo evaluate V2 output quality over a representative query set:
python3 scripts/evaluate_graph_v2.py --api-key "$API_KEY"Optional flags:
--queries-file path/to/queries.txtfor custom query sets--output /tmp/graph_v2_eval.jsonto persist full metrics
Use the scripts below to upgrade an existing chunk-only corpus into the new document-centric layout and validate metadata completeness.
Deploy a dataframe artifact directly to LanceDB. This is the reusable script
form of the original main.ipynb table-deployment cells:
python3 scripts/deploy_lancedb_table.py \
--input data/chunks-vYY-MM-DD.parquet \
--table chunks \
--mode overwriteIf a prepared dataframe still needs embeddings, pass a text column and optional embedded artifact output:
python3 scripts/deploy_lancedb_table.py \
--input data/chunks-without-vectors.parquet \
--table chunks \
--embed-text-column content \
--embedded-output data/chunks-with-vectors.parquet \
--mode overwriteBy default the backend uses Azure-backed az://lancedb. If LANCEDB_URI is set,
the same command writes to that local or explicit LanceDB URI instead.
Assistant profiles can also define isolated storage, for example SGP uses
az://lancedb/sgp_ai while SEA keeps the default az://lancedb. A local
LANCEDB_URI override still wins for prototype runs.
Bootstrap canonical sources and documents tables from the current chunks table:
python3 scripts/bootstrap_corpus_tables.py --overwriteIf you also want to rewrite the chunks table so every chunk includes document_id
and provenance fields:
python3 scripts/bootstrap_corpus_tables.py --overwrite --rewrite-chunksNotes:
--overwritealone creates / refreshessourcesanddocuments, but leaves the existingchunksschema untouched.--rewrite-chunksis required if you want chunk provenance fields such asdocument_id,chunk_id, andchunk_indexto exist in thechunkstable.- Importing enriched manifest chunks also requires
--rewrite-chunksto have been run.
Validate the upgraded corpus:
python3 scripts/validate_corpus_tables.pyThe validator reports:
- table existence and counts,
- missing required document metadata,
- document status distribution,
- sample chunk provenance completeness.
Import additional curated documents from a local YAML manifest:
python3 scripts/import_corpus_manifest.py --manifest data/corpus/sample_manifest.yaml --include-chunksSupporting files:
data/corpus/sources.yaml: seed source registry for trusted and partner corporadata/corpus/sample_manifest.yaml: example manual import manifest
Use the retrieval debug endpoint to inspect the document-first ranking path for a single query:
curl -sS "http://127.0.0.1:8000/debug/retrieve?query=tell%20me%20more%20about%20feed%20in%20tariff&limit=12" \
-H "X-Api-Key: $API_KEY"The response includes:
- selected documents,
- selected chunks,
- retrieval profile,
- query variants,
- retrieval branches taken,
- final selected path (
document_first,chunk_lexical,chunk_vector, orfallback).
To run a repeatable multi-prompt diagnostic surface and save both retrieval-debug and
/model stream outputs:
python3 scripts/run_retrieval_surface.py --base-url http://127.0.0.1:8000 --output-prefix tmp/retrieval_surfaceThis writes:
tmp/retrieval_surface.json: machine-readable combined resultstmp/retrieval_surface.txt: human-readable report with retrieval debug and raw NDJSON
For stream-only debugging:
python3 scripts/debug_model_stream.py --base-url http://127.0.0.1:8000 --output tmp/model_stream_debug.txtThe project is hooked up to CI/CD via GitHub Actions.
- Workflow:
.github/workflows/azure-webapps-python.yml - Azure Web App name:
sea-ai-api - A push to
maintriggers deployment. - The
frontend/KG tester app is intentionally separate and is not required for API deployment.
All contributions must follow Conventional Commits.
The codebase is formatted with black and isort. Use the provided Makefile for these
routine operations.
- Clone or fork the repository
- Create a new branch (
git checkout -b feature-branch) - Make your changes
- Ensure your code is properly formatted (
make format) - Commit your changes (
git commit -m 'Add some feature') - Push to the branch (
git push origin feature-branch) - Open a pull request
This project is licensed under the BSD 3-Clause License. See the LICENSE file.