diff --git a/README.md b/README.md index 7542b2e..0051428 100644 --- a/README.md +++ b/README.md @@ -15,5 +15,6 @@ This repo contains a collection of tutorials, demos, and how-to guides on how to | [Step-back prompting in Langchain RAG](./langchain-qdrant-step-back-prompting) | Step-back prompting for RAG, implemented in Langchain | OpenAI, Qdrant, Cohere, Langchain | | [Collaborative Filtering and MovieLens](./sparse-vectors-movies-reco) | A notebook demonstrating how to build a collaborative filtering system using Qdrant | Sparse Vectors, Qdrant | | [Use semantic search to navigate your codebase](./code-search/) | Implement semantic search application for code search task | Qdrant, Python, sentence-transformers, Jina | +| [AgentsKit RAG with Qdrant](./agentskit-rag-typescript) | Build a provider-neutral RAG pipeline in TypeScript with Qdrant vector memory | AgentsKit, TypeScript, Qdrant | | [Incremental Embedding Updates](./temporal-data-drift) | Sync embeddings with changing raw text data | Qdrant Cloud Inference, Python | diff --git a/agentskit-rag-typescript/.gitignore b/agentskit-rag-typescript/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/agentskit-rag-typescript/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/agentskit-rag-typescript/README.md b/agentskit-rag-typescript/README.md new file mode 100644 index 0000000..20facf6 --- /dev/null +++ b/agentskit-rag-typescript/README.md @@ -0,0 +1,43 @@ +# AgentsKit RAG with Qdrant + +This example builds a small retrieval-augmented generation pipeline in TypeScript with +[`@agentskit/rag`](https://www.npmjs.com/package/@agentskit/rag) and Qdrant-backed vector +memory from [`@agentskit/memory`](https://www.npmjs.com/package/@agentskit/memory). + +The included embedder is local, deterministic, and credential-free so the complete +ingest/retrieve flow is easy to reproduce. Replace `src/embed.ts` with any production +embedding provider without changing the RAG or Qdrant integration. + +## Run locally + +Start Qdrant: + +```bash +docker run --rm -p 6333:6333 qdrant/qdrant:v1.15.4 +``` + +In another terminal: + +```bash +npm install +npm run demo +``` + +To use Qdrant Cloud, provide the cluster URL and API key: + +```bash +QDRANT_URL="https://your-cluster.cloud.qdrant.io" \ +QDRANT_API_KEY="your-api-key" \ +npm run demo +``` + +## What the example demonstrates + +- creating a cosine collection through the Qdrant REST API; +- chunking and ingesting documents with AgentsKit; +- preserving source IDs and payload metadata in Qdrant; +- retrieving ranked context through the standard AgentsKit `Retriever` contract; +- swapping the embedder independently of the vector database. + +Run `npm test` for the credential-free embedder checks and `npm run check` for strict +TypeScript validation. diff --git a/agentskit-rag-typescript/package.json b/agentskit-rag-typescript/package.json new file mode 100644 index 0000000..e058340 --- /dev/null +++ b/agentskit-rag-typescript/package.json @@ -0,0 +1,20 @@ +{ + "name": "agentskit-rag-qdrant-example", + "private": true, + "type": "module", + "scripts": { + "check": "tsc --noEmit", + "demo": "tsx src/demo.ts", + "test": "vitest run" + }, + "dependencies": { + "@agentskit/memory": "0.11.4", + "@agentskit/rag": "^0.5.0" + }, + "devDependencies": { + "@types/node": "^25.0.0", + "tsx": "^4.20.0", + "typescript": "^6.0.0", + "vitest": "^4.0.0" + } +} diff --git a/agentskit-rag-typescript/src/demo.ts b/agentskit-rag-typescript/src/demo.ts new file mode 100644 index 0000000..ab90640 --- /dev/null +++ b/agentskit-rag-typescript/src/demo.ts @@ -0,0 +1,50 @@ +import { qdrant } from '@agentskit/memory' +import { createRAG } from '@agentskit/rag' +import { embed } from './embed.js' + +const qdrantUrl = process.env.QDRANT_URL ?? 'http://localhost:6333' +const collection = process.env.QDRANT_COLLECTION ?? 'agentskit_docs' + +async function ensureCollection(): Promise { + const response = await fetch(`${qdrantUrl}/collections/${collection}`, { + method: 'PUT', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + vectors: { size: 64, distance: 'Cosine' }, + }), + }) + + if (!response.ok) { + throw new Error(`Could not create Qdrant collection: ${await response.text()}`) + } +} + +await ensureCollection() + +const rag = createRAG({ + embed, + store: qdrant({ + url: qdrantUrl, + apiKey: process.env.QDRANT_API_KEY, + collection, + }), + chunkSize: 400, + chunkOverlap: 40, + topK: 3, +}) + +await rag.ingest([ + { + id: 'agentskit-overview', + content: 'AgentsKit is a modular TypeScript toolkit for agents, memory, tools, RAG, evaluation, and observability.', + metadata: { source: 'overview' }, + }, + { + id: 'qdrant-overview', + content: 'Qdrant stores and searches high-dimensional vectors with payload metadata and cosine similarity.', + metadata: { source: 'qdrant' }, + }, +]) + +const results = await rag.search('Which toolkit provides TypeScript RAG and agent memory?') +console.log(results) diff --git a/agentskit-rag-typescript/src/embed.ts b/agentskit-rag-typescript/src/embed.ts new file mode 100644 index 0000000..67bb9f6 --- /dev/null +++ b/agentskit-rag-typescript/src/embed.ts @@ -0,0 +1,18 @@ +const dimensions = 64 + +export async function embed(text: string): Promise { + const vector = Array.from({ length: dimensions }, () => 0) + const tokens = text.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [] + + for (const token of tokens) { + let hash = 2166136261 + for (const character of token) { + hash ^= character.codePointAt(0) ?? 0 + hash = Math.imul(hash, 16777619) + } + vector[(hash >>> 0) % dimensions]! += 1 + } + + const magnitude = Math.hypot(...vector) + return magnitude === 0 ? vector : vector.map(value => value / magnitude) +} diff --git a/agentskit-rag-typescript/tests/embed.test.ts b/agentskit-rag-typescript/tests/embed.test.ts new file mode 100644 index 0000000..dc30492 --- /dev/null +++ b/agentskit-rag-typescript/tests/embed.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest' +import { embed } from '../src/embed.js' + +describe('local example embedder', () => { + it('is deterministic and normalized', async () => { + const [first, second] = await Promise.all([ + embed('AgentsKit Qdrant RAG'), + embed('AgentsKit Qdrant RAG'), + ]) + + expect(first).toEqual(second) + expect(first).toHaveLength(64) + expect(Math.hypot(...first!)).toBeCloseTo(1) + }) + + it('puts related text closer than unrelated text', async () => { + const [query, related, unrelated] = await Promise.all([ + embed('typescript rag memory'), + embed('agentskit typescript rag memory'), + embed('cooking pasta recipe'), + ]) + const similarity = (left: number[], right: number[]) => + left.reduce((sum, value, index) => sum + value * right[index]!, 0) + + expect(similarity(query, related)).toBeGreaterThan(similarity(query, unrelated)) + }) +}) diff --git a/agentskit-rag-typescript/tsconfig.json b/agentskit-rag-typescript/tsconfig.json new file mode 100644 index 0000000..52aea9f --- /dev/null +++ b/agentskit-rag-typescript/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "lib": ["ES2022", "DOM"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022", + "types": ["node"] + }, + "include": ["src", "tests"] +}