Kynetra FX

Documentation

RAG — Retrieval-Augmented Generation

createRag composes AiPort with VectorPort into a two-method RAG pipeline: index(docs) embeds and stores documents, and search(query, topK?) embeds the query and returns the most semantically similar chunks. Swap AI providers and vector stores without touching the pipeline.

API#

NameTypeDescription
aiAiPortThe AI port used for embedding (required). Use mockAi in tests, openAiAdapter / workersAiAdapter in production.
vectorsVectorPort?Vector store for upsert and query. Defaults to inMemoryVector().
idIdPort?ID generator for documents without an explicit id. Defaults to sequentialId("doc").

createRag(options) returns an object with two methods:

NameTypeDescription
index(docs)(docs: { id?: string; text: string; metadata?: Record<string, unknown> }[]) → Promise<string[]>Embeds each document and upserts its vector. Returns the list of document IDs (generated if not provided).
search(query, topK?)(query: string, topK?: number) → Promise<VectorMatch[]>Embeds the query string and returns the topK most similar documents sorted by descending cosine similarity. Defaults to topK = 10.

Quick start with in-memory fakes#

The fastest way to build a RAG pipeline is with mockAi and the default inMemoryVector. No API keys, no infrastructure — ideal for wiring up the route before committing to a provider.

examples/rag-basic.ts
import { createRag, mockAi } from '@kynetra/fx'
 
// mockAi returns zero vectors — fine for wiring up the API shape
const rag = createRag({ ai: mockAi({ dim: 3 }) })
 
// Index some documents
const ids = await rag.index([
{ text: 'Ports decouple business logic from infrastructure.' },
{ text: 'Adapters implement ports for a specific backend.' },
{ text: 'In-memory fakes enable deterministic unit tests.' },
])
console.log(ids) // ['doc-1', 'doc-2', 'doc-3']
 
// Search
const results = await rag.search('how do I test without a database?', 2)
console.log(results[0].id) // the most relevant doc ID
console.log(results[0].score) // cosine similarity

Full example — route with real embeddings#

Below is a complete route that lets clients ask questions against a knowledge base. On startup it indexes a set of documentation chunks; on each request it retrieves the top-3 relevant chunks and passes them to the LLM as context.

src/routes/kb.ts
import { createFX } from '@kynetra/fx'
import { openAiAdapter } from '@kynetra/fx-ai'
import { createRag } from '@kynetra/fx-ai'
 
interface Answer { question: string; answer: string; sources: string[] }
 
const app = createFX()
 
const ai = openAiAdapter({ apiKey: process.env.OPENAI_API_KEY!, model: 'gpt-4o-mini' })
const rag = createRag({ ai })
 
// Index your knowledge base at startup (or on a schedule)
app.hook('onBoot', async () => {
await rag.index([
{ id: 'ports', text: 'Ports decouple logic from infrastructure. See the ports page.', metadata: { url: '/docs/ports' } },
{ id: 'kv', text: 'KvPort is a string key/value store with optional TTL expiry.', metadata: { url: '/docs/kv-cache' } },
{ id: 'queues', text: 'QueuePort models a FIFO queue with enqueue and drain methods.', metadata: { url: '/docs/queues' } },
{ id: 'ai-port', text: 'AiPort has two methods: complete for chat and embed for vectors.', metadata: { url: '/docs/ai' } },
])
})
 
app.get('/ask', async (ctx) => {
const question = ctx.query('q') ?? ''
if (!question) return ctx.status(400).json({ error: 'missing q' })
 
// 1. Retrieve relevant context
const hits = await rag.search(question, 3)
const context = hits.map(h => h.metadata?.text ?? '').join('\n\n')
 
// 2. Generate an answer grounded in the retrieved context
const { text: answer } = await ai.complete({
messages: [
{
role: 'system',
content: `You are a helpful documentation assistant. Answer using only the context below.\n\nContext:\n${context}`,
},
{ role: 'user', content: question },
],
})
 
const sources = hits.map(h => (h.metadata?.url as string) ?? '')
 
return ctx.json({ question, answer, sources } satisfies Answer)
})

Testing RAG pipelines#

Use mockAi with a fixed dim and the default inMemoryVector. Because mockAi returns zero vectors, all stored documents will have equal cosine similarity — that is fine for testing route plumbing. For testing ranking logic specifically, inject hand-crafted vectors via inMemoryVector directly.

src/routes/kb.test.ts
import { describe, it, expect } from 'vitest'
import { mockAi, createRag, inMemoryVector } from '@kynetra/fx'
import app from './kb'
 
describe('knowledge base route', () => {
it('returns an answer and sources', async () => {
// Override the RAG instance with fakes
const vectors = inMemoryVector()
const ai = mockAi({ reply: 'Use the ports page.', dim: 3 })
const rag = createRag({ ai, vectors })
 
await rag.index([{ id: 'ports', text: 'Ports page.', metadata: { url: '/docs/ports' } }])
app.decorateContext('rag', rag)
app.decorateContext('ai', ai)
 
const res = await app.fetch(new Request('http://x/ask?q=how+do+ports+work'))
expect(res.status).toBe(200)
 
const body = await res.json() as { answer: string; sources: string[] }
expect(body.answer).toBe('Use the ports page.')
expect(body.sources).toContain('/docs/ports')
})
 
it('returns 400 when q is missing', async () => {
const res = await app.fetch(new Request('http://x/ask'))
expect(res.status).toBe(400)
})
})

Providing document IDs#

If you do not pass an id field, createRag generates one using the injected IdPort (default: sequentialId("doc")). Pass your own IDs to make vectors deterministic and idempotent — re-indexing the same document replaces its vector rather than duplicating it.

examples/rag-ids.ts
import { createRag } from '@kynetra/fx'
import { openAiAdapter } from '@kynetra/fx-ai'
 
const rag = createRag({ ai: openAiAdapter({ apiKey: '...' }) })
 
// Stable IDs mean re-indexing is idempotent
await rag.index([
{ id: 'faq-billing', text: 'Billing happens on the 1st of each month.' },
{ id: 'faq-refunds', text: 'Refunds are issued within 5 business days.' },
{ id: 'faq-cancels', text: 'You can cancel any time from account settings.' },
])

Swapping in production adapters#

1

Choose an embedding provider

openAiAdapter (text-embedding-3-small) or workersAiAdapter (@cf/baai/bge-small-en-v1.5) for embedding. See AI providers.
2

Choose a vector store

Write a VectorPort adapter for Cloudflare Vectorize, pgvector, or Pinecone and pass it as vectors. The inMemoryVector remains the default for tests.
3

Inject at the composition root

Pass real adapters in your worker entrypoint or app factory. Nothing inside the RAG pipeline changes.
src/worker.ts
import { cloudflare } from '@kynetra/fx-cloudflare'
import { workersAiAdapter, createRag } from '@kynetra/fx-ai'
import app from './app'
 
interface Env {
AI: Ai
VECTORIZE: VectorizeIndex
}
 
// Minimal VectorPort wrapper around Cloudflare Vectorize
function vectorizePort(index: VectorizeIndex) {
return {
async upsert(records: { id: string; vector: number[]; metadata?: unknown }[]) {
await index.upsert(records.map(r => ({ id: r.id, values: r.vector, metadata: r.metadata })))
},
async query(vector: number[], topK = 10) {
const res = await index.query(vector, { topK, returnMetadata: true })
return res.matches.map(m => ({ id: m.id, score: m.score, metadata: m.metadata }))
},
async delete(ids: string[]) {
await index.deleteByIds(ids)
},
}
}
 
return {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
const ai = workersAiAdapter({ binding: env.AI })
const vectors = vectorizePort(env.VECTORIZE)
app.decorateContext('rag', createRag({ ai, vectors }))
return cloudflare(app).fetch(request, env, ctx)
}
}

Tip

Vectorize and pgvector adapters can be published as separate packages that implement VectorPort. Because your pipeline only depends on the port interface, the adapter can be swapped with zero changes to application code.

How indexing works internally#

index(docs) calls ai.embed({ input: docs.map(d => d.text) }) in a single batch request, then calls vectors.upsert([...]) with the returned vectors zipped to the document IDs and metadata. Batching minimises round-trips to the embedding endpoint.

search(query, topK) calls ai.embed({ input: query }), takes vectors[0] from the result, and calls vectors.query(vector, topK). The returned VectorMatch[] contains the stored metadata — typically the original text, URL, or any other fields you indexed.

See also#

RAG — Retrieval-Augmented Generation · Kynetra FX