Automated knowledge-graph generator on Civo
This tutorial will show you how to build a lightweight, end-to-end pipeline with no heavy infrastructure, no specialized machine learning background, just a small FastAPI backend, a simple JavaScript frontend, and a GPU node doing all the AI lifting.
Written by
Software Engineer at GoCardless
Written by
Software Engineer at GoCardless
Discover how to turn unstructured text into a visual, searchable knowledge graph using GPU-powered Kubernetes. Before we dive into code, it’s important to understand why knowledge graphs matter and why companies are investing in building them.
Over 80% of most modern information is unstructured: emails, documents, reports, support tickets, research papers, logs, transcripts, you name it. Hidden inside that text are entities (people, places, companies, technologies) and the relationships between them. Knowledge graphs are the simplest way to reveal this structure. They turn messy text into a connected map that’s easy to search, analyze, and visualize.
This is exactly why large-scale tech platforms and data-driven organizations rely on knowledge graphs for search, recommendations, fraud detection, cybersecurity, and regulatory compliance. They make relationships computable. They turn ambiguity into clarity. They help people and systems answer questions faster.
The problem is: building them usually requires complex pipelines, heavyweight NLP frameworks, or expensive enterprise tools.
This tutorial will show you how to build a lightweight, end-to-end pipeline with no heavy infrastructure, no specialized machine learning background, just a small FastAPI backend, a simple JavaScript frontend, and a GPU node doing all the AI lifting.
By the end, you’ll have a practical, working example of how AI can transform raw text into structured insight, a foundational pattern for search engines, analytics tools, recommendation systems, research assistants, and more.
Prerequisites
- Civo account with GPU access
- Docker installed locally
- Python installed
- relaxAI API key from the relaxAI dashboard
- kubectl installed
Project overview
What this project builds
In this tutorial, you’ll create a simple system that turns text into a visual knowledge graph. It’s designed to be easy to understand, even if you’ve never worked with AI pipelines before.
Pipeline flow
Here’s how the process works from start to finish:
- The user enters text in the frontend (this could be a paragraph, a short article, or any piece of writing)
- The backend sends the text to relaxAI (the model analyzes the content and identifies important entities and how they relate)
- relaxAI returns a clean JSON structure describing the graph, for example:
{"nodes": [{ "id": "Alice", "type": "Person" },{ "id": "ACME Corp", "type": "Organization" }],"edges": [{ "source": "Alice", "target": "ACME Corp", "relation": "works_for" }]}
- The frontend visualizes this graph using a lightweight library (tools like D3.js or Cytoscape.js display the nodes and connections in an interactive layout)
- The user explores the graph visually (instead of reading dense text, they can quickly see relationships and key information at a glance)
Components you’ll deploy
To make this work, you’ll deploy a small, easy-to-understand setup on Civo:
- Backend API Pod: Processes text, calls relaxAI, and returns graph-ready JSON.
- Frontend Pod: Renders the interactive graph in the browser.
- Unified dockerization step: You’ll build and package both components into container images.
- Unified deployment step: Deploy both containers to your Civo Kubernetes cluster.
This keeps the whole project lightweight, approachable, and perfect for beginners exploring AI on Kubernetes.
Project structure
Civo-knowledge-graph-generator/├── backend/│ ├── main.py│ ├── requirements.txt│ └── Dockerfile├── frontend/│ ├── index.html│ └── Dockerfile└── kubernetes/├── backend-deployment.yaml└── frontend-deployment.yaml
Step 1: Create a GPU-enabled Kubernetes cluster
- Log in to the Civo Dashboard
- Create a new Kubernetes cluster
- Add a GPU node pool (one node is sufficient for this demo)
- Download your kubeconfig
- Connect to the cluster by running
set KUBECONFIG=path-to-kubeconfig-filein your terminal (useexportinstead ofseton Linux or macOS).
Verify connectivity:
kubectl get nodes

Source: Image by author
Even though this tutorial uses small inputs, the cluster is GPU-enabled, so the same setup scales naturally to heavier inference workloads without architectural changes.
Step 2: Build the backend (entity/relationship extractor)
The backend is a small FastAPI service that runs on your Civo cluster and does one job: take raw text, send it to relaxAI, and return a clean JSON representation of the entities and relationships it finds. Instead of dealing with messy model output, your frontend gets a predictable structure with nodes and edges that is ready to render as a graph.
At a high level, the backend exposes a single /graphify endpoint. When the user submits text from the UI, this endpoint builds a carefully structured prompt and sends it to relaxAI’s chat completions API running on Civo. The model responds with a description of the entities (like people, organizations, locations) and how they connect. The backend always returns data in the same JSON structure described earlier (a list of nodes and a list of edges).
Here’s a simple visual representation of that schema:

Source: Image by author
Behind the scenes, the code adds a few important safety rails. The relaxAI prompt strongly enforces “JSON-only” output, so the model does not mix explanations with data. The backend then cleans up common formatting issues (like markdown code fences) and tries to parse the response as strict JSON.
If the model still returns something slightly off, the code applies a small “repair” step to handle minor issues such as missing brackets or trailing commas, then validates the result into Pydantic models for Node and Edge.
The service also includes:
- A
/healthendpoint so Kubernetes can run liveness/readiness probes - CORS configuration so the browser-based frontend can call the API
- Environment-variable configuration for your relaxAI API key and endpoint, so nothing sensitive is hardcoded
With those pieces in place, the full backend comes together as a single FastAPI application. It wires up the models, the relaxAI call, the JSON-repair logic, and the supporting endpoints into one self-contained file.
Below is the complete main.py implementation that you’ll deploy on your Civo cluster.
main.py
Import necessary libraries:
import osimport jsonimport httpxfrom fastapi import FastAPI, HTTPExceptionfrom fastapi.middleware.cors import CORSMiddlewarefrom pydantic import BaseModel
Environment variables:
RELAXAI_API_KEY = os.getenv("RELAXAI_API_KEY")RELAXAI_API_URL = os.getenv("RELAXAI_API_URL","https://api.relax.ai/v1/chat/completions")if not RELAXAI_API_KEY:raise RuntimeError("RELAXAI_API_KEY is missing.")
FastAPI setup:
app = FastAPI(title="Knowledge Graph Backend (JSON-Safe)")app.add_middleware(CORSMiddleware,allow_origins=["*"],allow_methods=["*"],allow_headers=["*"],)
Models setup:
class GraphifyRequest(BaseModel):text: strclass Node(BaseModel):id: strtype: strclass Edge(BaseModel):source: strtarget: strrelation: strclass GraphifyResponse(BaseModel):nodes: list[Node]edges: list[Edge]
Health check:
@app.get("/health")def health():return {"status": "ok"}
Hard JSON-only prompt builder:
def build_prompt(text: str) -> str:return f"""You are an information extraction engine.STRICT REQUIREMENTS:- OUTPUT ONLY JSON.- NO explanation.- NO natural language.- NO markdown.- NO commentary.- NO code fences.- If unsure, output an empty list for nodes/edges.Extract entities (as nodes) and relationships (as edges) from the text.VALID JSON FORMAT (follow EXACTLY):{{"nodes": [{{"id": "Alice", "type": "Person"}},{{"id": "CompanyX", "type": "Organization"}}],"edges": [{{"source": "Alice", "target": "CompanyX", "relation": "works_at"}}]}}Text:{text}"""
This helper function builds the exact prompt sent to relaxAI. It forces the model into a strict “JSON-only” mode by explicitly forbidding explanations, markdown, or natural language. The prompt also provides a concrete example of the expected structure so the model knows exactly how to format its response.
By injecting the user’s text at the bottom, the backend reliably guides relaxAI to return a clean set of nodes and edges every time.
JSON repair helpers:
def clean_model_output(s: str) -> str:"""Remove backticks, code fences, leading junk."""s = s.strip()if s.startswith("```json"):s = s[len("```json"):].strip()if s.startswith("```"):s = s[3:].strip()if s.endswith("```"):s = s[:-3].strip()return sdef try_parse_json_strict(s: str):"""Strict JSON parsing."""return json.loads(s)def try_parse_json_fallback(s: str):"""RelaxAI may add trailing commas or missing quotes.We attempt limited cleanup."""# Remove trailing commas before closing braces/bracketss = s.replace(",}", "}").replace(",]", "]")# Replace weird quote styles if any (very rare)s = s.replace("“", "\"").replace("”", "\"")return json.loads(s)
These helper functions make the backend far more resilient to imperfect model output. cleanmodeloutput strips away markdown fences or stray formatting that relaxAI might prepend or append. tryparsejsonstrict attempts a normal JSON parse first, while tryparsejsonfallback performs small, targeted repairs, like removing trailing commas or fixing curly quotes, before trying again. Together, they significantly boost the chances of turning the model’s response into valid, machine-readable JSON without breaking the safety or structure of the graph.
Main endpoint:
@app.post("/graphify", response_model=GraphifyResponse)async def graphify(payload: GraphifyRequest):if not payload.text.strip():raise HTTPException(status_code=400, detail="Text input cannot be empty.")prompt = build_prompt(payload.text)body = {"model": "Llama-4-Maverick-17B-128E","messages": [{"role": "system", "content": "Return ONLY JSON following the given structure."},{"role": "user", "content": prompt}],"temperature": 0,"max_tokens": 300}
Call relaxAI:
try:async with httpx.AsyncClient(timeout=40.0) as client:response = await client.post(RELAXAI_API_URL,json=body,headers={"Authorization": f"Bearer {RELAXAI_API_KEY}"})except httpx.RequestError as e:raise HTTPException(status_code=503, detail=f"relaxAI unreachable: {str(e)}")if response.status_code != 200:raise HTTPException(status_code=response.status_code, detail=response.text)# Extract model contenttry:content = response.json()["choices"][0]["message"]["content"]content = clean_model_output(content)except Exception as e:raise HTTPException(status_code=500, detail=f"Bad relaxAI response structure: {str(e)}")# JSON parsing attemptsdata = None# 1) TRY STRICT JSONtry:data = try_parse_json_strict(content)except Exception:pass# 2) TRY FALLBACK JSONif data is None:try:data = try_parse_json_fallback(content)except Exception:raise HTTPException(status_code=500,detail=f"Model returned invalid JSON even after cleanup: {content}")# Guaranteed safe outputnodes = [Node(**n) for n in data.get("nodes", []) if "id" in n and "type" in n]edges = [Edge(**e) for e in data.get("edges", []) if "source" in e and "target" in e]return GraphifyResponse(nodes=nodes, edges=edges)
Once this backend is in place and containerized, you have a reliable API that turns arbitrary text into a graph-ready JSON payload. In the next step, the frontend will use this endpoint to fetch nodes and edges from Civo and draw them as an interactive knowledge graph in the browser.
Step 3: Build the frontend (graph visualizer)
With the backend doing the heavy lifting, the frontend’s job is simple: take the model’s extracted entities and relationships and turn them into an interactive graph. This is where everything becomes visual. Instead of staring at raw JSON, users can actually see how concepts connect.
The frontend is kept intentionally lightweight. No frameworks, no bundlers, no build process, just a single index.html using Cytoscape.js, a tiny helper script, and a hard-coded API endpoint. That’s enough to render a smooth, responsive knowledge graph inside the browser.
Here’s the basic flow:
- The user types text into a textbox.
- The frontend sends that text to the backend’s
/graphifyendpoint. - The backend returns JSON containing
nodesandedges. - Cytoscape.js renders that into a full graph layout.
- Users can pan, zoom, hover, and explore the structure as a real network.
Even though the UI is minimal, Cytoscape gives the frontend a surprising amount of polish. You get physics-based layout, smooth animations, styled nodes, and edges that automatically reposition themselves as the graph grows. The result feels far bigger than the tiny amount of code powering it.
If you want to improve things later, custom node icons, color-coded relationships, graph filtering, dark mode, etc., all of that can be added on top of this foundation. But the core here is deliberately small for demonstration purposes.
Index.html
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Knowledge Graph Generator</title><!-- Cytoscape.js for graph visualization --><script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.28.1/cytoscape.min.js"></script><style>* { box-sizing: border-box; margin: 0; padding: 0; }body {font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;background: linear-gradient(135deg, #1a1a2e, #16213e);min-height: 100vh;color: #e0e0e0;}.container { max-width: 1400px; margin: 0 auto; padding: 20px; }header { text-align: center; padding: 30px 0; }h1 {font-size: 2.5rem;background: linear-gradient(90deg, #00d4ff, #7b2cbf);-webkit-background-clip: text;-webkit-text-fill-color: transparent;margin-bottom: 10px;}.subtitle { color: #888; font-size: 1.1rem; }.main-content {display: grid;grid-template-columns: 1fr 2fr;gap: 20px;margin-top: 20px;}.input-panel,.graph-panel {background: rgba(255,255,255,0.05);border: 1px solid rgba(255,255,255,0.1);border-radius: 16px;padding: 24px;}textarea {width: 100%;height: 300px;background: rgba(0,0,0,0.3);border: 1px solid rgba(255,255,255,0.2);border-radius: 8px;padding: 16px;color: #e0e0e0;font-size: 1rem;resize: vertical;}button {width: 100%;padding: 14px 28px;margin-top: 16px;background: linear-gradient(90deg,#00d4ff,#7b2cbf);border: none;border-radius: 8px;color: white;font-size: 1rem;font-weight: 600;cursor: pointer;}#graph-container {flex: 1;min-height: 500px;background: rgba(0,0,0,0.3);border: 1px solid rgba(255,255,255,0.1);border-radius: 8px;}.legend { display: flex; gap: 20px; margin-top: 16px; flex-wrap: wrap; }.legend-item { display: flex; align-items: center; gap: 8px; }.legend-dot { width: 12px; height: 12px; border-radius: 50%; }.person { background: #00d4ff; }.organization { background: #7b2cbf; }.location { background: #00c853; }.event { background: #ff9100; }.status-message { margin-top: 16px; padding: 12px; border-radius: 8px; font-size: 0.9rem; }.error { background: rgba(255,82,82,0.2); color: #ff8a80; border: 1px solid rgba(255,82,82,0.4); }.success { background: rgba(0,200,83,0.2); color: #69f0ae; border: 1px solid rgba(0,200,83,0.4); }.loading { background: rgba(0,212,255,0.2); color: #80d8ff; border: 1px solid rgba(0,212,255,0.4); }@media (max-width: 900px) {.main-content { grid-template-columns: 1fr; }#graph-container { min-height: 400px; }}</style></head><body><div class="container"><header><h1>Knowledge Graph Generator</h1><p class="subtitle">Transform text into interactive knowledge graphs using AI</p></header><div class="main-content"><div class="input-panel"><h2>Input Text</h2><textarea id="text-input" placeholder="Enter text to analyze..."></textarea><button id="generate-btn">Generate Knowledge Graph</button><div id="status-container"></div></div><div class="graph-panel"><h2>Knowledge Graph</h2><div id="graph-container"></div><div class="legend"><div class="legend-item"><div class="legend-dot person"></div>Person</div><div class="legend-item"><div class="legend-dot organization"></div>Organization</div><div class="legend-item"><div class="legend-dot location"></div>Location</div><div class="legend-item"><div class="legend-dot event"></div>Event</div></div><div id="stats" class="stats"></div></div></div></div><script>// ============================================================================// CONFIGURATION// ============================================================================const API_URL = window.API_URL || "http://<BACKEND-EXTERNAL-IP>:8000";let cy;// ============================================================================// GRAPH INITIALIZATION// ============================================================================function initGraph() {cy = cytoscape({container: document.getElementById("graph-container"),style: [{selector: "node",style: {"label": "data(id)","width": 50,"height": 50,"font-size": "12px","color": "#e0e0e0","border-width": 3,"border-color": "#fff","text-valign": "bottom","text-margin-y": 8}},{ selector: 'node[type="Person"]', style: { "background-color": "#00d4ff" } },{ selector: 'node[type="Organization"]', style: { "background-color": "#7b2cbf" } },{ selector: 'node[type="Location"]', style: { "background-color": "#00c853" } },{ selector: 'node[type="Event"]', style: { "background-color": "#ff9100" } },{selector: "edge",style: {"line-color": "#666","target-arrow-color": "#666","target-arrow-shape": "triangle","curve-style": "bezier","label": "data(relation)","font-size": "10px","color": "#999"}}],layout: { name: "grid" }});}// ============================================================================// STATUS & STATS HELPERS// ============================================================================function showStatus(msg, type) {document.getElementById("status-container").innerHTML =`<div class="status-message ${type}">${msg}</div>`;}function clearStatus() {document.getElementById("status-container").innerHTML = "";}function updateStats(nodes, edges) {document.getElementById("stats").innerHTML =`<div><strong>${nodes}</strong> entities</div><div><strong>${edges}</strong> relationships</div>`;}// ============================================================================// MAIN FUNCTION// ============================================================================async function generateGraph() {const text = document.getElementById("text-input").value.trim();const btn = document.getElementById("generate-btn");if (!text) {showStatus("Please enter some text first.", "error");return;}btn.disabled = true;btn.textContent = "Generating...";showStatus("Analyzing text...", "loading");try {const response = await fetch(`${API_URL}/graphify`, {method: "POST",headers: { "Content-Type": "application/json" },body: JSON.stringify({ text })});if (!response.ok) {const err = await response.json();throw new Error(err.detail || "Server error");}const data = await response.json();if (!data.nodes?.length) {showStatus("No entities found. Try a longer text sample.", "error");updateStats(0, 0);return;}cy.elements().remove();data.nodes.forEach(n =>cy.add({ group: "nodes", data: { id: n.id, type: n.type } }));data.edges.forEach(e =>cy.add({group: "edges",data: {source: e.source,target: e.target,relation: e.relation.replace(/_/g, " ")}}));cy.layout({name: "cose",animate: true,idealEdgeLength: 100,nodeRepulsion: 8000}).run();cy.fit(50);showStatus(`Extracted ${data.nodes.length} entities and ${data.edges.length} relationships.`, "success");updateStats(data.nodes.length, data.edges.length);} catch (err) {showStatus(`Error: ${err.message}`, "error");} finally {btn.disabled = false;btn.textContent = "Generate Knowledge Graph";}}// ============================================================================// INITIALIZATION// ============================================================================document.addEventListener("DOMContentLoaded", () => {initGraph();updateStats(0, 0);});document.getElementById("text-input").addEventListener("keydown", e => {if (e.ctrlKey && e.key === "Enter") generateGraph();});document.getElementById("generate-btn").onclick = generateGraph;</script></body></html>
Here’s how the frontend should look:

Source: Image by author
Important note: This value const API_URL = window.API_URL || "http://<BACKEND-EXTERNAL-IP>:8000"; is baked into the Docker image at build time. That means you should deploy the backend first, wait for its LoadBalancer IP to become available, and then build the frontend image using that IP.
At this point, the frontend and backend are fully connected. You type text, the backend extracts meaning, and the browser builds a real-time knowledge graph. Next, we’ll package everything into Docker and deploy it on Civo so your graph visualizer runs as a fully hosted application.
Step 4: Dockerization
Create backend/Dockerfile:
FROM python:3.11-slimWORKDIR /app# Install dependenciesCOPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txt# Copy codeCOPY main.py .# Run as non-rootRUN useradd -u 1001 appuserUSER 1001EXPOSE 8000CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Create backend/requirements.txt:
fastapi==0.109.0uvicorn[standard]==0.27.0httpx==0.26.0pydantic==2.5.3python-dotenv==1.0.1
Create frontend/Dockerfile:
FROM nginx:alpine# Copy static frontend to nginx public directoryCOPY index.html /usr/share/nginx/html/# Expose port 80 for Civo LoadBalancerEXPOSE 80# Simple healthcheckHEALTHCHECK --interval=30s --timeout=5s --retries=3 \CMD wget -qO- http://localhost:80/ || exit 1CMD ["nginx", "-g", "daemon off;"]
Build and push images:
# Build backend imagedocker build -t YOUR_REGISTRY/knowledge-graph-backend:latest .# Push backend imagedocker push YOUR_REGISTRY/knowledge-graph-backend:latest# Build frontend imagedocker build -t YOUR_REGISTRY/knowledge-graph-frontend:latest .# Push frontend imagedocker push YOUR_REGISTRY/knowledge-graph-frontend:latest
Step 5: Deploy on Civo Kubernetes
Create kubernetes/backend-deployment.yaml:
# ---------------------------------------------------------# Namespace# ---------------------------------------------------------apiVersion: v1kind: Namespacemetadata:name: knowledge-graph---# ---------------------------------------------------------# RelaxAI Secret (user replaces the placeholder)# ---------------------------------------------------------apiVersion: v1kind: Secretmetadata:name: relaxai-secretnamespace: knowledge-graphtype: OpaquestringData:api-key: "YOUR_API_KEY_HERE"---# ---------------------------------------------------------# Backend Deployment# ---------------------------------------------------------apiVersion: apps/v1kind: Deploymentmetadata:name: knowledge-backendnamespace: knowledge-graphspec:replicas: 2selector:matchLabels:app: knowledge-backendtemplate:metadata:labels:app: knowledge-backendspec:containers:- name: backendimage: alimohamed782/knowledge-graph-backend:latestimagePullPolicy: Alwaysports:- containerPort: 8000env:- name: RELAXAI_API_KEYvalueFrom:secretKeyRef:name: relaxai-secretkey: api-key- name: RELAXAI_API_URLvalue: "https://api.relax.ai/v1/chat/completions"# Keep resource limits modest for Civo clustersresources:requests:cpu: "100m"memory: "256Mi"limits:cpu: "500m"memory: "512Mi"# Health checks → very important for FastAPIlivenessProbe:httpGet:path: /healthport: 8000initialDelaySeconds: 5periodSeconds: 15readinessProbe:httpGet:path: /healthport: 8000initialDelaySeconds: 3periodSeconds: 10# Reasonable security context for a simple API podsecurityContext:runAsNonRoot: truerunAsUser: 1001allowPrivilegeEscalation: falsesecurityContext:fsGroup: 1000---# ---------------------------------------------------------# Backend Service (LoadBalancer - public access)# ---------------------------------------------------------apiVersion: v1kind: Servicemetadata:name: knowledge-backendnamespace: knowledge-graphspec:type: LoadBalancerselector:app: knowledge-backendports:- port: 8000targetPort: 8000protocol: TCPname: http
Apply it:
kubectl apply -f kubernetes/backend-deployment.yaml
Create kubernetes/frontend-deployment.yaml:
# ---------------------------------------------------------# Frontend Deployment (nginx serving static files)# ---------------------------------------------------------apiVersion: apps/v1kind: Deploymentmetadata:name: knowledge-frontendnamespace: knowledge-graphspec:replicas: 2selector:matchLabels:app: knowledge-frontendtemplate:metadata:labels:app: knowledge-frontendspec:containers:- name: frontendimage: alimohamed782/knowledge-graph-frontend:latestimagePullPolicy: Alwaysports:- containerPort: 80# Basic resource limits (static HTML + JS requires almost nothing)resources:requests:cpu: "20m"memory: "32Mi"limits:cpu: "100m"memory: "64Mi"# Health checks for nginxlivenessProbe:httpGet:path: /port: 80initialDelaySeconds: 5periodSeconds: 20readinessProbe:httpGet:path: /port: 80initialDelaySeconds: 3periodSeconds: 10securityContext:allowPrivilegeEscalation: false# Pod-level security contextsecurityContext:fsGroup: 101---# ---------------------------------------------------------# Frontend Service (public LoadBalancer)# ---------------------------------------------------------apiVersion: v1kind: Servicemetadata:name: knowledge-frontendnamespace: knowledge-graphspec:type: LoadBalancerselector:app: knowledge-frontendports:- name: httpport: 80targetPort: 80protocol: TCP
Apply it:
kubectl apply -f kubernetes/frontend-deployment.yaml
Verify deployments:
kubectl get pods -n knowledge-graphkubectl get svc -n knowledge-graph
Get external IPs:
# Backend LoadBalancer IPkubectl get svc knowledge-backend -n knowledge-graph# Frontend LoadBalancer IPkubectl get svc knowledge-frontend -n knowledge-graph
Step 6: Test and visualize
Now that both the backend and frontend are deployed, you can verify everything is working by sending real text through the system and checking whether the generated knowledge graph appears in the browser. The snippet below is long enough to produce an interesting graph, with multiple entities and cross-organization relationships, making it ideal for a first test.
Use this text in the frontend input box:
In 2024, OpenAI partnered with Microsoft to deploy an advanced AI safety system across Azure data centers. Sam Altman met with leaders at the European Commission in Brussels to discuss regulatory guidelines for frontier models. Meanwhile, Google DeepMind researchers collaborated with the University of Oxford to publish a study on reinforcement learning methods for autonomous robotics. The report highlighted that NVIDIA GPUs remained essential for large-scale training workloads. Later that year, Anthropic announced a joint initiative with AWS to improve secure model deployment across enterprise environments.
If everything is configured correctly, your backend should extract entities, infer relationships, normalize them into the required JSON format, and the frontend should render them into an interactive graph as shown below:

Source: Image by author
The graph above is the final output of the full pipeline. Each circle represents an entity the model detected in the text (people, organizations, locations, or products), and each arrow represents a relationship inferred between them.
The system read a multi-sentence paragraph and automatically mapped out partnerships, meetings, collaborations, and technology dependencies. Instead of a wall of text, you now have a structured visual summary: OpenAI’s partnership with Microsoft and Azure, Sam Altman’s meeting with the European Commission in Brussels, Google DeepMind’s collaboration with the University of Oxford, NVIDIA’s role in large-scale training, and Anthropic’s joint initiative with AWS.
This is exactly the type of transformation the project is designed to deliver, turning unstructured language into a clear, navigable knowledge map.
This tutorial is intentionally small and focused, but it is designed as a foundation rather than a toy example. While the inputs used here are modest, the same pipeline can quickly grow in complexity as you process longer documents, batch multiple texts, or integrate the system into real applications. At that point, GPU acceleration becomes essential for keeping inference fast and responsive. Because the cluster is already GPU-enabled, no architectural changes are required to scale from simple experiments to more demanding, production-style workloads.
Key takeaways
Building a knowledge-graph extractor doesn’t require a massive data stack. A lightweight FastAPI service plus a small JS frontend is enough to turn unstructured text into a visual, explorable graph.
Civo’s GPU infrastructure makes this especially smooth. Provisioning is fast, costs stay predictable, and you avoid the usual cloud clutter that slows small AI projects down.
Once you have this pipeline running end-to-end, you’re not just drawing pretty graphs. You’ve built the core of a data-centric AI system: a repeatable way to turn messy language into structured relationships that other tools, agents, or downstream workflows can build on.
Use this as a foundation and expand as needed, more models, better extraction logic, bigger datasets. The architecture scales with you.
Additional resources

Software Engineer at GoCardless
Mostafa Ibrahim is a software engineer and technical writer specializing in developer-focused content for SaaS and AI platforms. He currently works as a Software Engineer at GoCardless, contributing to production systems and scalable payment infrastructure.
Alongside his engineering work, Mostafa has written more than 200 technical articles reaching over 500,000 readers. His content covers topics including Kubernetes deployments, AI infrastructure, authentication systems, and retrieval-augmented generation (RAG) architectures.
Share this article