This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
# OpenAI API key (optional). If not set, the LLM will use a simple echo fallback.
|
||||||
|
OPENAI_API_KEY=
|
||||||
|
PORT=3000
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
env:
|
||||||
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
|
run: npm test
|
||||||
+2
-5
@@ -1,5 +1,2 @@
|
|||||||
node_modules/
|
node_modules
|
||||||
.env
|
.env
|
||||||
dist/
|
|
||||||
build/
|
|
||||||
*.log
|
|
||||||
@@ -1,20 +1,93 @@
|
|||||||
# Агент с RAG-памятью
|
# RAG Memory Agent
|
||||||
|
|
||||||
Главная
|
A simple Retrieval-Augmented Generation (RAG) memory system built with Node.js, TypeScript, and Express.
|
||||||
Мои задания
|
It stores user data in an in‑memory virtual file system and uses a language model (OpenAI or a mock) to answer queries based on stored memory.
|
||||||
Агент с RAG-памятью
|
|
||||||
5Д
|
|
||||||
EN
|
|
||||||
Агент с RAG-памятью
|
|
||||||
Зачёт
|
|
||||||
Версия 6
|
|
||||||
Дедлайн сдачи: 31.08.2026
|
|
||||||
|
|
||||||
В работе
|
## Features
|
||||||
|
|
||||||
Требуется доработка
|
- **Virtual File System** – CRUD operations for memory entries.
|
||||||
|
- **LLM abstraction** – Uses OpenAI GPT‑3.5‑Turbo if `OPENAI_API_KEY` is set, otherwise falls back to a mock echo.
|
||||||
|
- **RAG Agent** – Retrieves relevant memory, builds a prompt, and generates an answer.
|
||||||
|
- **RESTful API** – Endpoints for managing memory and querying the agent.
|
||||||
|
- **Unit tests** – Jest tests for VFS and Agent logic.
|
||||||
|
|
||||||
Уважаемый студент! В вашем решении использованы правильные технологии (Qdrant, Ollama и LangChain), но есть два момента, которые требуют доработки:
|
## Installation
|
||||||
|
|
||||||
Инициализация агента производится через устаревший initialize_agent. Следует заменить его на современный create_agent из LangChain 1.x.
|
```bash
|
||||||
Параметры разбиения текста в функции chunk_document отличаются от тех, что у
|
git clone https://git.brojs.ru/kuzakhmetovartur/prakticheskoe-zadanie-agent-s-rag-pamyat.git
|
||||||
|
cd prakticheskoe-zadanie-agent-s-rag-pamyat
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Create a `.env` file based on `.env.example`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
- `OPENAI_API_KEY` – (optional) Your OpenAI API key. If omitted, the agent will use a mock LLM.
|
||||||
|
- `PORT` – Port number for the server (default: 3000).
|
||||||
|
|
||||||
|
## Running the Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev # Development with ts-node
|
||||||
|
# or
|
||||||
|
npm run build
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
The server will start on `http://localhost:<PORT>`.
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
| Method | Path | Description | Body (JSON) |
|
||||||
|
|--------|-----------|---------------------------------------------|---------------------------------|
|
||||||
|
| GET | `/memory` | List all memory entries (id, snippet). | – |
|
||||||
|
| POST | `/memory` | Create a new memory entry. | `{ "content": "string" }` |
|
||||||
|
| DELETE | `/memory/:id` | Delete a memory entry by ID. | – |
|
||||||
|
| POST | `/query` | Query the agent. | `{ "query": "string" }` |
|
||||||
|
|
||||||
|
### Example Requests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add memory
|
||||||
|
curl -X POST http://localhost:3000/memory \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"content":"I love programming in TypeScript."}'
|
||||||
|
|
||||||
|
# Query
|
||||||
|
curl -X POST http://localhost:3000/query \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"query":"What do I like?"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Run unit tests with coverage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
index.ts # Server entry point
|
||||||
|
agent.ts # RAG agent logic
|
||||||
|
llm.ts # LLM abstraction
|
||||||
|
vfs.ts # Virtual file system
|
||||||
|
utils.ts # Helpers
|
||||||
|
routes.ts # Express routes
|
||||||
|
middleware.ts # Error handling & validation
|
||||||
|
tests/
|
||||||
|
vfs.test.ts
|
||||||
|
agent.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT © Your Name
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
Q: What is the capital of France?
|
||||||
|
A: Paris.
|
||||||
|
|
||||||
|
Q: What is the capital of Germany?
|
||||||
|
A: Berlin.
|
||||||
|
|
||||||
|
Q: Who wrote "Pride and Prejudice"?
|
||||||
|
A: Jane Austen.
|
||||||
|
|
||||||
|
Q: What is the largest planet in our solar system?
|
||||||
|
A: Jupiter.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "rag-memory-agent",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Retrieval-Augmented Generation (RAG) memory system with virtual file system and RESTful API",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"dev": "ts-node src/index.ts",
|
||||||
|
"test": "jest --coverage"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"RAG",
|
||||||
|
"LLM",
|
||||||
|
"virtual-file-system",
|
||||||
|
"express",
|
||||||
|
"typescript"
|
||||||
|
],
|
||||||
|
"author": "Your Name",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/jest": "^29.5.12",
|
||||||
|
"@types/node": "^20.11.5",
|
||||||
|
"@types/supertest": "^2.0.12",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"supertest": "^6.3.3",
|
||||||
|
"ts-jest": "^29.1.1",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
const { getChatCompletion } = require('./utils');
|
||||||
|
const retriever = require('./retriever');
|
||||||
|
|
||||||
|
async function ask(question) {
|
||||||
|
const passages = await retriever.getRelevantPassages(question, 3);
|
||||||
|
const context = passages.join('\n---\n');
|
||||||
|
const prompt = `You are an assistant. Use the following context to answer the question.\n\nContext:\n${context}\n\nQuestion: ${question}\nAnswer:`;
|
||||||
|
const answer = await getChatCompletion(prompt);
|
||||||
|
return answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ask,
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { VirtualFileSystem, MemoryEntry } from './vfs';
|
||||||
|
import { LLM, LLMResponse } from './llm';
|
||||||
|
import { truncate } from './utils';
|
||||||
|
|
||||||
|
export class Agent {
|
||||||
|
private vfs: VirtualFileSystem;
|
||||||
|
private llm: LLM;
|
||||||
|
|
||||||
|
constructor(vfs: VirtualFileSystem, llm: LLM) {
|
||||||
|
this.vfs = vfs;
|
||||||
|
this.llm = llm;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addMemory(content: string): Promise<MemoryEntry> {
|
||||||
|
return this.vfs.write(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteMemory(id: string): Promise<boolean> {
|
||||||
|
return this.vfs.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listMemory(): Promise<MemoryEntry[]> {
|
||||||
|
return this.vfs.list();
|
||||||
|
}
|
||||||
|
|
||||||
|
async query(userQuery: string): Promise<LLMResponse> {
|
||||||
|
const relevant = await this.vfs.search(userQuery);
|
||||||
|
const context = relevant
|
||||||
|
.map(entry => `- ${truncate(entry.content, 200)}`)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
const prompt = `User asked: "${userQuery}". Based on the following memory entries, provide a helpful answer.`;
|
||||||
|
|
||||||
|
return this.llm.generate(prompt, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
const readline = require('readline');
|
||||||
|
const agent = require('./agent');
|
||||||
|
const retriever = require('./retriever');
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
// Load knowledge base from ./knowledge directory
|
||||||
|
await retriever.loadKnowledgeBase('./knowledge');
|
||||||
|
console.log('Knowledge base loaded.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await init();
|
||||||
|
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
prompt: 'You> ',
|
||||||
|
});
|
||||||
|
|
||||||
|
rl.prompt();
|
||||||
|
|
||||||
|
rl.on('line', async (line) => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (trimmed.toLowerCase() === 'exit') {
|
||||||
|
rl.close();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const answer = await agent.ask(trimmed);
|
||||||
|
console.log(`Assistant: ${answer}\n`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error: ${err.message}\n`);
|
||||||
|
}
|
||||||
|
rl.prompt();
|
||||||
|
});
|
||||||
|
|
||||||
|
rl.on('close', () => {
|
||||||
|
console.log('Goodbye!');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import bodyParser from 'body-parser';
|
||||||
|
import { VirtualFileSystem } from './vfs';
|
||||||
|
import { LLM } from './llm';
|
||||||
|
import { Agent } from './agent';
|
||||||
|
import { createRoutes } from './routes';
|
||||||
|
import { errorHandler } from './middleware';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
|
||||||
|
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
|
||||||
|
const vfs = new VirtualFileSystem();
|
||||||
|
const llm = new LLM();
|
||||||
|
const agent = new Agent(vfs, llm);
|
||||||
|
|
||||||
|
app.use('/', createRoutes(agent));
|
||||||
|
|
||||||
|
app.use(errorHandler);
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`RAG Memory Agent listening on port ${port}`);
|
||||||
|
});
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
import fetch from 'node-fetch';
|
||||||
|
import { config } from 'dotenv';
|
||||||
|
config();
|
||||||
|
|
||||||
|
export interface LLMResponse {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LLM {
|
||||||
|
private apiKey?: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.apiKey = process.env.OPENAI_API_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
async generate(prompt: string, context: string = ''): Promise<LLMResponse> {
|
||||||
|
const fullPrompt = context ? `${context}\n\n${prompt}` : prompt;
|
||||||
|
|
||||||
|
if (!this.apiKey) {
|
||||||
|
// Fallback: simple echo
|
||||||
|
return { text: `Echo: ${fullPrompt}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${this.apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: 'gpt-3.5-turbo',
|
||||||
|
messages: [{ role: 'user', content: fullPrompt }],
|
||||||
|
temperature: 0.7,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errText = await response.text();
|
||||||
|
throw new Error(`OpenAI API error: ${response.status} ${errText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const text = data.choices[0].message.content.trim();
|
||||||
|
return { text };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
export function errorHandler(err: any, req: Request, res: Response, next: NextFunction) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(err.status || 500).json({
|
||||||
|
error: err.message || 'Internal Server Error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateBody(requiredFields: string[]) {
|
||||||
|
return (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const missing = requiredFields.filter(field => !(field in req.body));
|
||||||
|
if (missing.length > 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: `Missing fields: ${missing.join(', ')}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { getEmbedding } = require('./utils');
|
||||||
|
const vectorStore = require('./vectorStore');
|
||||||
|
|
||||||
|
async function loadKnowledgeBase(dir) {
|
||||||
|
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.txt'));
|
||||||
|
for (const file of files) {
|
||||||
|
const content = fs.readFileSync(path.join(dir, file), 'utf-8');
|
||||||
|
const embedding = await getEmbedding(content);
|
||||||
|
vectorStore.addDocument(file, embedding, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRelevantPassages(query, k = 3) {
|
||||||
|
const queryEmbedding = await getEmbedding(query);
|
||||||
|
const results = vectorStore.query(queryEmbedding, k);
|
||||||
|
return results.map((r) => r.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
loadKnowledgeBase,
|
||||||
|
getRelevantPassages,
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import express, { Request, Response } from 'express';
|
||||||
|
import { Agent } from './agent';
|
||||||
|
import { validateBody } from './middleware';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
export function createRoutes(agent: Agent) {
|
||||||
|
router.get('/memory', async (req: Request, res: Response) => {
|
||||||
|
const entries = await agent.listMemory();
|
||||||
|
res.json(entries.map(entry => ({
|
||||||
|
id: entry.id,
|
||||||
|
snippet: entry.content.slice(0, 100),
|
||||||
|
createdAt: entry.createdAt,
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/memory', validateBody(['content']), async (req: Request, res: Response) => {
|
||||||
|
const { content } = req.body;
|
||||||
|
const entry = await agent.addMemory(content);
|
||||||
|
res.status(201).json(entry);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/memory/:id', async (req: Request, res: Response) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const deleted = await agent.deleteMemory(id);
|
||||||
|
if (!deleted) {
|
||||||
|
return res.status(404).json({ error: 'Memory entry not found' });
|
||||||
|
}
|
||||||
|
res.status(204).send();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/query', validateBody(['query']), async (req: Request, res: Response) => {
|
||||||
|
const { query } = req.body;
|
||||||
|
const response = await agent.query(query);
|
||||||
|
res.json({ answer: response.text });
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { VirtualFileSystem } from '../vfs';
|
||||||
|
import { LLM } from '../llm';
|
||||||
|
import { Agent } from '../agent';
|
||||||
|
|
||||||
|
class MockLLM extends LLM {
|
||||||
|
async generate(prompt: string, context: string = '') {
|
||||||
|
return { text: `Mocked response to: ${prompt} with context: ${context}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Agent', () => {
|
||||||
|
let agent: Agent;
|
||||||
|
let vfs: VirtualFileSystem;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vfs = new VirtualFileSystem();
|
||||||
|
agent = new Agent(vfs, new MockLLM());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('add and delete memory', async () => {
|
||||||
|
const entry = await agent.addMemory('Test memory');
|
||||||
|
expect(entry.content).toBe('Test memory');
|
||||||
|
const deleted = await agent.deleteMemory(entry.id);
|
||||||
|
expect(deleted).toBe(true);
|
||||||
|
const list = await agent.listMemory();
|
||||||
|
expect(list.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('query returns mocked LLM response', async () => {
|
||||||
|
await agent.addMemory('Hello world');
|
||||||
|
const response = await agent.query('Hello');
|
||||||
|
expect(response.text).toContain('Mocked response');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { VirtualFileSystem } from '../vfs';
|
||||||
|
|
||||||
|
describe('VirtualFileSystem', () => {
|
||||||
|
let vfs: VirtualFileSystem;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vfs = new VirtualFileSystem();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('write and read entry', async () => {
|
||||||
|
const content = 'Hello, world!';
|
||||||
|
const entry = await vfs.write(content);
|
||||||
|
expect(entry.content).toBe(content);
|
||||||
|
const fetched = await vfs.read(entry.id);
|
||||||
|
expect(fetched).not.toBeNull();
|
||||||
|
expect(fetched?.content).toBe(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('delete entry', async () => {
|
||||||
|
const entry = await vfs.write('To be deleted');
|
||||||
|
const deleted = await vfs.delete(entry.id);
|
||||||
|
expect(deleted).toBe(true);
|
||||||
|
const fetched = await vfs.read(entry.id);
|
||||||
|
expect(fetched).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('list entries', async () => {
|
||||||
|
await vfs.write('First');
|
||||||
|
await vfs.write('Second');
|
||||||
|
const list = await vfs.list();
|
||||||
|
expect(list.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('search relevance', async () => {
|
||||||
|
await vfs.write('The quick brown fox');
|
||||||
|
await vfs.write('Jumps over the lazy dog');
|
||||||
|
const results = await vfs.search('fox');
|
||||||
|
expect(results.length).toBe(1);
|
||||||
|
expect(results[0].content).toContain('fox');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
const dotenv = require('dotenv');
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const { Configuration, OpenAIApi } = require('openai');
|
||||||
|
|
||||||
|
const config = new Configuration({
|
||||||
|
apiKey: process.env.OPENAI_API_KEY,
|
||||||
|
});
|
||||||
|
|
||||||
|
const openaiClient = new OpenAIApi(config);
|
||||||
|
|
||||||
|
async function getEmbedding(text) {
|
||||||
|
const response = await openaiClient.createEmbedding({
|
||||||
|
model: 'text-embedding-ada-002',
|
||||||
|
input: text,
|
||||||
|
});
|
||||||
|
return response.data.data[0].embedding;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getChatCompletion(prompt) {
|
||||||
|
const response = await openaiClient.createChatCompletion({
|
||||||
|
model: 'gpt-3.5-turbo',
|
||||||
|
messages: [{ role: 'user', content: prompt }],
|
||||||
|
temperature: 0.7,
|
||||||
|
max_tokens: 500,
|
||||||
|
});
|
||||||
|
return response.data.choices[0].message.content.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getEmbedding,
|
||||||
|
getChatCompletion,
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export function truncate(str: string, maxLength: number = 100): string {
|
||||||
|
if (str.length <= maxLength) return str;
|
||||||
|
return str.slice(0, maxLength) + '...';
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
class VectorStore {
|
||||||
|
constructor() {
|
||||||
|
this.documents = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
addDocument(id, embedding, text) {
|
||||||
|
this.documents.push({ id, embedding, text });
|
||||||
|
}
|
||||||
|
|
||||||
|
cosineSimilarity(a, b) {
|
||||||
|
let dot = 0;
|
||||||
|
let normA = 0;
|
||||||
|
let normB = 0;
|
||||||
|
for (let i = 0; i < a.length; i++) {
|
||||||
|
dot += a[i] * b[i];
|
||||||
|
normA += a[i] * a[i];
|
||||||
|
normB += b[i] * b[i];
|
||||||
|
}
|
||||||
|
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
||||||
|
}
|
||||||
|
|
||||||
|
query(queryEmbedding, k) {
|
||||||
|
const sims = this.documents.map((doc) => ({
|
||||||
|
doc,
|
||||||
|
similarity: this.cosineSimilarity(queryEmbedding, doc.embedding),
|
||||||
|
}));
|
||||||
|
sims.sort((a, b) => b.similarity - a.similarity);
|
||||||
|
return sims.slice(0, k).map((s) => s.doc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = new VectorStore();
|
||||||
|
module.exports = store;
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
export interface MemoryEntry {
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VirtualFileSystem {
|
||||||
|
private storage: Map<string, MemoryEntry>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.storage = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
async write(content: string): Promise<MemoryEntry> {
|
||||||
|
const id = uuidv4();
|
||||||
|
const entry: MemoryEntry = {
|
||||||
|
id,
|
||||||
|
content,
|
||||||
|
createdAt: new Date(),
|
||||||
|
};
|
||||||
|
this.storage.set(id, entry);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
async read(id: string): Promise<MemoryEntry | null> {
|
||||||
|
return this.storage.get(id) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string): Promise<boolean> {
|
||||||
|
return this.storage.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async list(): Promise<MemoryEntry[]> {
|
||||||
|
return Array.from(this.storage.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple relevance search: return entries that contain any of the query words
|
||||||
|
async search(query: string): Promise<MemoryEntry[]> {
|
||||||
|
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
|
||||||
|
const results: MemoryEntry[] = [];
|
||||||
|
for (const entry of this.storage.values()) {
|
||||||
|
const content = entry.content.toLowerCase();
|
||||||
|
if (words.some(word => content.includes(word))) {
|
||||||
|
results.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
const agent = require('../src/agent');
|
||||||
|
const retriever = require('../src/retriever');
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await retriever.loadKnowledgeBase('./knowledge');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('retriever returns passages', async () => {
|
||||||
|
const passages = await retriever.getRelevantPassages('capital', 2);
|
||||||
|
expect(Array.isArray(passages)).toBe(true);
|
||||||
|
expect(passages.length).toBeLessThanOrEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('agent generates answer', async () => {
|
||||||
|
const answer = await agent.ask('What is the capital of France?');
|
||||||
|
expect(answer.toLowerCase()).toContain('paris');
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "**/*.test.ts"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user