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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Need to deploy your Worker to Cloudflare? Python Workers are in open beta and ha
- [**`durable-objects/`**](durable-objects) — An example with storing state in a [Durable Object](https://developers.cloudflare.com/durable-objects/).
- [**`cron/`**](cron) — shows a simple [cron job](https://developers.cloudflare.com/workers/configuration/cron-triggers/).
- [**`workers-ai/`**](workers-ai) makes a call [Workers AI](https://developers.cloudflare.com/workers-ai/) to run inference on Cloudflare's Global Network.
- [**`vectorize-rag/`**](vectorize-rag) — a RAG example using remote Workers AI and [Vectorize](https://developers.cloudflare.com/vectorize/) bindings.
- [**`workflows/`**](workflows) — shows a durable [Workflows](https://developers.cloudflare.com/workflows/) example.
- [**`opengraph/`**](opengraph) — shows how to use [HTMLRewriter](https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/) to generate OpenGraph images with Python Workers.
- [**`image-gen/`**](image-gen) — shows how to use [Pillow](https://pillow.readthedocs.io/en/stable/) to generate images with Python Workers.
Expand Down
36 changes: 36 additions & 0 deletions vectorize-rag/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Vectorize RAG in Python

This example shows how to build a very simple Vectorized document retrieval system using Cloudflare's Vectorize service and Python workers.

> **Note:** This example uses remote Workers AI and Vectorize resources, which are billable, even in local development.

## Local development

```bash
npx wrangler login
# Select the intended account if Wrangler prompts for one.
npx wrangler vectorize create python-rag-index --dimensions=768 --metric=cosine
uv run pywrangler dev
```

### Seeding

```bash
curl -X POST http://localhost:8787/seed
```

Seed the four-document to the Vectorize index.

### Querying

```bash
curl -X POST http://localhost:8787/query \
-H 'content-type: application/json' \
--data '{"question":"What does Vectorize store?"}'
```

## Endpoints

- `GET /` describes the setup and models.
- `POST /seed` embeds and upserts the documents.
- `POST /query` accepts `{"question":"..."}` and returns a grounded `answer` plus match IDs, titles, scores, and citations.
13 changes: 13 additions & 0 deletions vectorize-rag/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "python-vectorize-rag",
"version": "0.0.0",
"private": true,
"scripts": {
"deploy": "uv run pywrangler deploy",
"dev": "uv run pywrangler dev",
"start": "uv run pywrangler dev"
},
"devDependencies": {
"wrangler": "^4.127.1"
}
}
13 changes: 13 additions & 0 deletions vectorize-rag/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[project]
name = "python-vectorize-rag"
version = "0.1.0"
description = "Python RAG example with Workers AI and Vectorize"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []

[dependency-groups]
dev = [
"workers-py",
"workers-runtime-sdk",
]
176 changes: 176 additions & 0 deletions vectorize-rag/src/entry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
from urllib.parse import urlparse

from workers import Response, WorkerEntrypoint

EMBEDDING_MODEL = "@cf/baai/bge-base-en-v1.5"
GENERATION_MODEL = "@cf/openai/gpt-oss-120b"
DOCUMENTS = (
{
"id": "cloudflare-workers",
"title": "Cloudflare Workers",
"text": "Cloudflare Workers run JavaScript, TypeScript, and Python code close to users on Cloudflare's network.",
"citation": "Cloudflare Workers overview",
},
{
"id": "workers-ai",
"title": "Workers AI",
"text": "Workers AI provides model inference through an AI binding so a Worker can run supported models without managing inference servers.",
"citation": "Workers AI overview",
},
{
"id": "vectorize",
"title": "Vectorize",
"text": "Vectorize is Cloudflare's vector database for storing embeddings and retrieving similar vectors with semantic search.",
"citation": "Vectorize overview",
},
{
"id": "r2",
"title": "R2",
"text": "R2 is Cloudflare's S3-compatible object storage service for storing and retrieving objects without egress fees from Cloudflare.",
"citation": "R2 overview",
},
)


def json_response(payload, status=200, headers=None):
return Response.json(payload, status=status, headers=headers)


def error_response(message, status, headers=None):
return json_response({"error": message}, status, headers)


async def embed(ai, texts):
result = await ai.run(EMBEDDING_MODEL, {"text": texts, "pooling": "cls"})
return result.get("data")


class Default(WorkerEntrypoint):
async def fetch(self, request):
path = urlparse(request.url).path

match path:
case "/":
if request.method != "GET":
return error_response("Method not allowed.", 405, {"Allow": "GET"})

return json_response(
{
"endpoints": {
"GET /": "overview",
"POST /seed": "seed corpus",
"POST /query": "query corpus",
},
"embedding_model": EMBEDDING_MODEL,
"generation_model": GENERATION_MODEL,
"setup": "Create python-rag-index, then POST /seed before POST /query.",
}
)

case "/seed":
if request.method != "POST":
return error_response("Method not allowed.", 405, {"Allow": "POST"})

return await self.seed()

case "/query":
if request.method != "POST":
return error_response("Method not allowed.", 405, {"Allow": "POST"})

return await self.query(request)
case _:
return error_response("Not found.", 404)

async def seed(self):
"""Seed the Vectorize index with documents."""
vectors = await embed(self.env.AI, [document["text"] for document in DOCUMENTS])
if not vectors:
return error_response("Embedding response contained no data.", 502)

records = [
{
"id": document["id"],
"values": vector,
"metadata": {
"title": document["title"],
"text": document["text"],
"citation": document["citation"],
},
}
for document, vector in zip(DOCUMENTS, vectors)
]
mutation = await self.env.VECTORIZE.upsert(records)
return json_response(
{
"mutation_id": mutation["mutationId"],
"document_count": len(DOCUMENTS),
"status": "accepted; Vectorize writes propagate asynchronously, so retry /query shortly if no matches are visible yet.",
},
202,
)

async def query(self, request):
"""Query the Vectorize index with a question."""
try:
payload = await request.json()
except ValueError:
return error_response("Request body must be valid JSON.", 400)

question = payload.get("question")
if not isinstance(question, str) or not question.strip():
return error_response("'question' must be a non-empty string.", 400)

vectors = await embed(self.env.AI, [question.strip()])
if not vectors:
return error_response("Embedding response contained no data.", 502)
result = await self.env.VECTORIZE.query(
vectors[0], {"topK": 3, "returnMetadata": "all", "returnValues": False}
)
matches = []
for match in result["matches"]:
metadata = match.get("metadata")
if not metadata or not metadata.get("text"):
continue
matches.append((match, metadata))
if not matches:
return error_response(
"No indexed documents are visible yet. Seed first or retry shortly.",
409,
)

sources = "\n\n".join(
f"[{metadata.get('citation', match['id'])}] {metadata.get('title', 'Untitled')}: {metadata['text']}"
for match, metadata in matches
)
prompt = (
"Answer only from the supplied sources. If the sources do not answer the "
"question, say so. Cite factual statements using the source labels exactly "
f"as written.\n\nSources:\n{sources}\n\nQuestion: {question.strip()}"
)
generation = await self.env.AI.run(
GENERATION_MODEL,
{
"messages": [
{
"role": "system",
"content": "You answer using only supplied sources and citations.",
},
{"role": "user", "content": prompt},
],
"max_tokens": 300,
},
)
return json_response(
{
"answer": generation["choices"][0]["message"]["content"],
"matches": [
{
"id": match["id"],
"citation": metadata.get("citation", match["id"]),
"title": metadata.get("title", "Untitled"),
"score": match["score"],
}
for match, metadata in matches
],
}
)
23 changes: 23 additions & 0 deletions vectorize-rag/wrangler.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "python-vectorize-rag",
"main": "src/entry.py",
"compatibility_date": "2026-09-01",
"compatibility_flags": [
"python_workers"
],
"ai": {
"binding": "AI",
"remote": true
},
"vectorize": [
{
"binding": "VECTORIZE",
"index_name": "python-rag-index",
"remote": true
}
],
"observability": {
"enabled": true
}
}
Loading