Build an auto-summarizing folder agent on Civo
Learn how to build an automated AI agent that monitors cloud storage, summarizes files with relaxAI, and outputs structured JSON metadata using Civo’s GPU-powered Kubernetes.
Written by
Software Engineer @ GoCardless
Written by
Software Engineer @ GoCardless
Picture this: you upload a PDF to a folder, and within seconds, a structured summary appears next to it. No manual triggers. No complicated workflows. Just effortless document processing that actually works.
That’s exactly what we’re building today: an automated summarization agent running on Civo Kubernetes with GPU support. It watches a Civo Object Store bucket, detects new files, sends them to a relaxAI-powered summarizer, and stores the results as JSON sidecars. The whole thing runs 24/7, with minimal infrastructure cost, and proves that AI pipelines don't need to be enterprise nightmares.
Prerequisites
As well as having basic Kubernetes knowledge, you will need to ensure that the following is in place before we dive in:
- A Civo account (this will require access to GPU-enabled nodes in your chosen Civo region)
- Docker installed locally
- Python 3.9+ installed
- relaxAI API access
- kubectl CLI configured and ready
Project folder structure
First, let's set up our project structure. Create this locally:
civo-summarizer-agent/├── summarizer/│ ├── app.py│ ├── requirements.txt│ └── Dockerfile├── watcher/│ ├── watcher.py│ ├── requirements.txt│ └── Dockerfile├── k8s/│ ├── namespace.yaml│ ├── summarizer-deployment.yaml│ ├── watcher-deployment.yaml│ └── secret.yaml
Step 1: Create a GPU Kubernetes cluster on Civo
First, provision a GPU-enabled 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 nodeskubectl get pods -A

Create a namespace for our project:
Create k8s/namespace.yaml:
apiVersion: v1kind: Namespacemetadata:name: summarizer-agent
Apply it:
kubectl apply -f k8s/namespace.yamlkubectl config set-context --current --namespace=summarizer-agent
Step 2: Set up Civo Object Store
Civo's Object Store is S3-compatible and simple to set up:
Create Object Store credentials:
- In the Civo dashboard, go to Object Stores → Credentials
- Click Create a new Credential
- Note down your Access Key and Secret Key (you'll need these)
Create an Object Store instance:
- Go to Object Stores → Create an Object Store
- Name it
summarizer-store - Select the same region as your cluster (e.g.,
lon1) - Click Create store
Create a bucket:
- Click on your newly created Object Store instance
- Click Create a new folder
- Name it
incoming-docs - Click Create folder
Your bucket URL will be like this: https://objectstore.lon1.civo.com/incoming-docs
Note: Replace lon1 with your actual region throughout this tutorial.
Step 3: Build the summarizer service (relaxAI)
This is our GPU-powered summarization engine, utilizing the relaxAI SDK.
Create summarizer/app.py:
# ============================================================# 1. SETUP & CONFIGURATION# Initializes FastAPI, loads API key, sets up relaxAI client,# and defines request/response data models.# ============================================================from fastapi import FastAPI, HTTPExceptionfrom pydantic import BaseModelfrom relaxAI import relaxAIimport os, jsonfrom typing import Listapp = FastAPI()RELAXAI_API_KEY = os.getenv("RELAXAI_API_KEY")client = relaxAI(api_key=RELAXAI_API_KEY) if RELAXAI_API_KEY else Noneclass SummarizeRequest(BaseModel):content: strfilename: strclass SummarizeResponse(BaseModel):title: strsummary: strkeywords: List[str]word_count: intfilename: str# ============================================================# 2. API ENDPOINTS# Provides a health check endpoint and the main summarization route.# ============================================================@app.get("/health")async def health():return {"status": "healthy", "relaxAI_configured": client is not None}@app.post("/summarize", response_model=SummarizeResponse)async def summarize(request: SummarizeRequest):if not client:raise HTTPException(status_code=500, detail="relaxAI API key not configured")# Limit content size to avoid token overflowcontent_preview = request.content[:8000]# Build summarization prompt for the modelprompt = f"""Analyze the following document and provide a structured summary.Document: {request.filename}Content:{content_preview}Provide your response in this exact JSON format:{{"title": "Generated title for the document","summary": "A concise 2-3 sentence summary","keywords": ["keyword1", "keyword2", "keyword3"],"word_count": <approximate word count>}}Return ONLY the JSON."""# ============================================================# 3. SUMMARIZATION LOGIC & ERROR HANDLING# Sends prompt to relaxAI, cleans the response, parses JSON,# and safely returns a validated structured summary.# ============================================================try:chat_completion_response = client.chat.create_completion(messages=[{"role": "system","content": "You are a document analysis expert. Always respond with valid JSON only."},{"role": "user","content": prompt}],model="Llama-4-Maverick-17B-128E",temperature=0.3,max_tokens=500)# Extract raw AI responseresponse_content = chat_completion_response.choices[0].message.content.strip()# Remove markdown code fences if presentif response_content.startswith("```json"):response_content = response_content[7:]if response_content.startswith("```"):response_content = response_content[3:]if response_content.endswith("```"):response_content = response_content[:-3]response_content = response_content.strip()# Parse model JSON outputsummary_data = json.loads(response_content)summary_data["filename"] = request.filenamereturn SummarizeResponse(**summary_data)except json.JSONDecodeError as e:raise HTTPException(500, f"Failed to parse AI response as JSON: {e}")except Exception as e:raise HTTPException(500, f"Summarization failed: {e}")# Run locally if executed directlyif __name__ == "__main__":import uvicornuvicorn.run(app, host="0.0.0.0", port=8000)
Create summarizer/requirements.txt:
fastapi==0.104.1uvicorn==0.24.0relaxAIpydantic==2.5.0
Create summarizer/Dockerfile:
FROM python:3.11-slimWORKDIR /appCOPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txtCOPY app.py .EXPOSE 8000CMD ["python", "app.py"]
Step 4: Build the watcher agent
This pod polls Civo Object Store and orchestrates the summarization workflow.
Create watcher/watcher.py:
# ============================================================# 1. SETUP & CONFIGURATION# Loads env variables, configures S3 client, and defines helpers.# ============================================================import boto3import httpximport jsonimport timeimport osfrom botocore.client import ConfigS3_ENDPOINT = os.getenv("S3_ENDPOINT")S3_ACCESS_KEY = os.getenv("S3_ACCESS_KEY")S3_SECRET_KEY = os.getenv("S3_SECRET_KEY")S3_BUCKET = os.getenv("S3_BUCKET", "incoming-docs")SUMMARIZER_URL = os.getenv("SUMMARIZER_URL", "http://summarizer-service:8000")POLL_INTERVAL = int(os.getenv("POLL_INTERVAL", "10"))# Civo Object Store clients3 = boto3.client('s3',endpoint_url=S3_ENDPOINT,aws_access_key_id=S3_ACCESS_KEY,aws_secret_access_key=S3_SECRET_KEY,config=Config(signature_version='s3v4'))def get_file_content(key: str) -> str:"""Downloads a file and returns decoded text."""try:response = s3.get_object(Bucket=S3_BUCKET, Key=key)content = response['Body'].read()try:return content.decode('utf-8')except UnicodeDecodeError:return content.decode('latin-1')except Exception as e:print(f"Error downloading {key}: {e}")return Nonedef upload_json(key: str, data: dict):"""Uploads JSON summary back to Object Store."""try:s3.put_object(Bucket=S3_BUCKET,Key=key,Body=json.dumps(data, indent=2),ContentType='application/json')print(f"✓ Uploaded {key}")except Exception as e:print(f"Error uploading {key}: {e}")def file_needs_processing(filename: str, existing_files: set) -> bool:"""Checks if a file doesn’t already have a .json summary."""if filename.endswith('.json'):return Falsereturn f"{filename}.json" not in existing_files# ============================================================# 2. FILE PROCESSING LOGIC# Downloads a file, sends to summarizer API, and uploads results.# ============================================================async def process_file(filename: str):"""Handles full summarize → upload workflow for a single file."""print(f"Processing {filename}...")content = get_file_content(filename)if not content:print(f"✗ Could not read {filename}")returntry:async with httpx.AsyncClient(timeout=120.0) as client:response = await client.post(f"{SUMMARIZER_URL}/summarize",json={"content": content, "filename": filename})response.raise_for_status()summary = response.json()json_key = f"{filename}.json"upload_json(json_key, summary)print(f"✓ Successfully processed {filename}")except httpx.TimeoutException:print(f"✗ Timeout while processing {filename}")except httpx.HTTPStatusError as e:print(f"✗ HTTP error: {e.response.status_code}")except Exception as e:print(f"✗ Failed to process {filename}: {e}")# ============================================================# 3. WATCH LOOP# Polls the bucket, finds new files, and processes them continuously.# ============================================================async def watch_bucket():print("="*60)print("CIVO SUMMARIZER WATCHER STARTED")print("="*60)print(f"Bucket: {S3_BUCKET}")print(f"Endpoint: {S3_ENDPOINT}")print(f"Polling interval: {POLL_INTERVAL}s")print(f"Summarizer URL: {SUMMARIZER_URL}")print("="*60)while True:try:resp = s3.list_objects_v2(Bucket=S3_BUCKET)if 'Contents' not in resp:print("Bucket is empty, waiting...")time.sleep(POLL_INTERVAL)continueexisting_files = {obj['Key'] for obj in resp['Contents']}files_to_process = [f for f in existing_files if file_needs_processing(f, existing_files)]if files_to_process:print(f"\nFound {len(files_to_process)} file(s) to process")for f in files_to_process:await process_file(f)else:print(".", end="", flush=True)except Exception as e:print(f"\n✗ Error in watch loop: {e}")time.sleep(POLL_INTERVAL)if __name__ == "__main__":import asyncioasyncio.run(watch_bucket())
Create watcher/requirements.txt:
boto3==1.29.7httpx==0.25.1
Create watcher/Dockerfile:
FROM python:3.11-slimWORKDIR /appCOPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txtCOPY watcher.py .CMD ["python", "watcher.py"]
Step 5: Dockerization
Build and push both images to a container registry. We'll use Docker Hub for simplicity.
# Login to Docker Hubdocker login# Build and push summarizercd summarizerdocker build -t yourusername/civo-summarizer:latest .docker push yourusername/civo-summarizer:latest# Build and push watchercd ../watcherdocker build -t yourusername/civo-watcher:latest .docker push yourusername/civo-watcher:latestcd ..
Step 6: Deployment
Now, we will deploy everything to Kubernetes with all necessary environment variables. Create k8s/secret.yaml:
apiVersion: v1kind: Secretmetadata:name: summarizer-secretsnamespace: summarizer-agenttype: OpaquestringData:S3_ENDPOINT: "https://objectstore.lon1.civo.com" # Change region as neededS3_ACCESS_KEY: "your-civo-access-key-here"S3_SECRET_KEY: "your-civo-secret-key-here"S3_BUCKET: "incoming-docs"RELAXAI_API_KEY: "your-relaxAI-api-key-here"
CRITICAL: Replace all placeholder values with your actual credentials from step 2!
Create k8s/summarizer-deployment.yaml:
apiVersion: apps/v1kind: Deploymentmetadata:name: summarizernamespace: summarizer-agentspec:replicas: 1selector:matchLabels:app: summarizertemplate:metadata:labels:app: summarizerspec:containers:- name: summarizerimage: yourusername/civo-summarizer:latestports:- containerPort: 8000env:- name: RELAXAI_API_KEYvalueFrom:secretKeyRef:name: summarizer-secretskey: RELAXAI_API_KEYresources:requests:memory: "2Gi"cpu: "1000m"nvidia.com/gpu: "1"limits:memory: "4Gi"cpu: "2000m"nvidia.com/gpu: "1"livenessProbe:httpGet:path: /healthport: 8000initialDelaySeconds: 30periodSeconds: 10readinessProbe:httpGet:path: /healthport: 8000initialDelaySeconds: 10periodSeconds: 5nodeSelector:node.kubernetes.io/instance-type: g4-g.4xlarge.kube.cluster # Adjust based on your GPU node type---apiVersion: v1kind: Servicemetadata:name: summarizer-servicenamespace: summarizer-agentspec:selector:app: summarizerports:- protocol: TCPport: 8000targetPort: 8000type: ClusterIP
Create k8s/watcher-deployment.yaml:
apiVersion: apps/v1kind: Deploymentmetadata:name: watchernamespace: summarizer-agentspec:replicas: 1selector:matchLabels:app: watchertemplate:metadata:labels:app: watcherspec:containers:- name: watcherimage: yourusername/civo-watcher:latestenv:- name: S3_ENDPOINTvalueFrom:secretKeyRef:name: summarizer-secretskey: S3_ENDPOINT- name: S3_ACCESS_KEYvalueFrom:secretKeyRef:name: summarizer-secretskey: S3_ACCESS_KEY- name: S3_SECRET_KEYvalueFrom:secretKeyRef:name: summarizer-secretskey: S3_SECRET_KEY- name: S3_BUCKETvalueFrom:secretKeyRef:name: summarizer-secretskey: S3_BUCKET- name: SUMMARIZER_URLvalue: "http://summarizer-service:8000"- name: POLL_INTERVALvalue: "10"resources:requests:memory: "256Mi"cpu: "100m"limits:memory: "512Mi"cpu: "500m"
Deploy everything:
# Make sure you're in the right namespacekubectl config set-context --current --namespace=summarizer-agent# Apply secret (make sure you've edited it with real credentials!)kubectl apply -f k8s/secret.yaml# Deploy summarizer with GPUkubectl apply -f k8s/summarizer-deployment.yaml# Deploy watcherkubectl apply -f k8s/watcher-deployment.yaml# Check deployment statuskubectl get pods -n summarizer-agentkubectl get services -n summarizer-agent
# Watch pod statuskubectl get pods -n summarizer-agent -w# Check logskubectl logs -f deployment/summarizer -n summarizer-agentkubectl logs -f deployment/watcher -n summarizer-agent
Step 7: End-to-end testing
Time to see your AI pipeline in action!
- Go to Civo Dashboard → Object Stores → incoming-docs → Browse Files
- Click Upload File
- Upload: Sample.pdf - A sample file containing the text content
- Watch logs:
Step 7: End-to-end testingTime to see your AI pipeline in action!Go to Civo Dashboard → Object Stores → incoming-docs → Browse FilesClick Upload FileUpload: Sample.pdf - A sample file containing the text contentWatch logs:
- Refresh bucket — you will see:
Sample.pdfsample.pdf.json
Your automated summarization pipeline works:

Source: Image by author
Troubleshooting
If the summaries aren't being generated:
# Check pod statuskubectl get pods -n summarizer-agent# Check summarizer healthkubectl port-forward deployment/summarizer 8000:8000 -n summarizer-agentcurl http://localhost:8000/health# Check watcher logs for errorskubectl logs deployment/watcher -n summarizer-agent | grep -i error
Summary
This tutorial demonstrates how to build a production-ready AI automation pipeline without the complexity typically associated with enterprise AI platforms. Using Civo’s GPU-powered Kubernetes, GPU resources are provisioned in minutes, enabling fast experimentation and scalable production workflows without long lead times or operational overhead.
By combining Object Store, lightweight Kubernetes pods, and a simple polling architecture with relaxAI for inference, the pipeline remains efficient, cost-effective, and easy to operate at scale. The result is a practical blueprint for AI automation that can grow from small experiments to high-volume document processing, proving that modern AI pipelines can be both powerful and straightforward when built on the right foundation.
Additional resources
If you are interested in learning more about this topic, check out some of these resources:

Software Engineer @ 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
Further Reading
19 December 2025
Automated knowledge-graph generator on Civo
27 November 2025