Skip to content

Commit

Permalink
Merge pull request #81 from SAGE-RAI/main
Browse files Browse the repository at this point in the history
WIP
  • Loading branch information
adhityan authored Jun 17, 2024
2 parents 586b781 + 848b4f3 commit f986ade
Show file tree
Hide file tree
Showing 18 changed files with 422 additions and 71 deletions.
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ The author(s) are looking to add core maintainers for this opensource project. R
- [Redis](#redis)
- [Bring your own cache](#bring-your-own-cache)
- [More caches coming soon](#more-caches-coming-soon)
- [Conversation history](#conversation-history)
- [InMemory](#inmemory-default)
- [MongoDb](#mongodb-1)
- [Langsmith Integration](#langsmith-integration)
- [Sample projects](#sample-projects)
- [Contributors](#contributors)
Expand Down Expand Up @@ -1067,6 +1070,48 @@ We really encourage you send in a PR to this library if you are implementing a f

If you want to add support for any other cache providers, please create an [issue](https://github.com/llm-tools/embedjs/issues) and we will add it to the list of supported caches. All PRs are welcome.

# Conversation history

This version of EmbedJS adds an abstraction layer so conversation history can be stored and made persistant between sessions. Like all other aspects of embedJS there is a base interface for conversations and you can create a new plugin that supports different ways to store conversations.

The library supports the following caches -

## InMemory (default)

You can use a simple in-memory cache to store values during testing.

**Note:** This is the default if you don't specify anything.

- Set `MemoryConversations` as your cache provider on `RAGApplicationBuilder`

```TS
import { MemoryConversations } from '@llm-tools/embedjs/conversations/memory';

await new RAGApplicationBuilder()
.setConversations(new MemoryConversations())
```

**Note:** Although this cache can remove duplicate loaders and chunks, its store does not persist between process restarts. You should only be using it for testing.

## MongoDB

Can be used with any version of Mongo.

- Set `MongoConversations` as your cache provider on `RAGApplicationBuilder`

```TS
import { MongoConversations } from '@llm-tools/embedjs/conversations/mongo';

const conversationsdb = new MongoConversations({
uri: MONGODB_URI,
dbName: DB_NAME,
collectionName: CONVERSATIONS_COLLECTION_NAME
});

await new RAGApplicationBuilder()
.setConversations(conversationsdb)
```

# Langsmith Integration

Langsmith allows you to keep track of how you use LLM and embedding models. It logs histories, token uses and other metadata. Follow these three simple steps to enable -
Expand Down
15 changes: 15 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,21 @@
"import": "./dist/cache/redis-cache.js",
"require": "./dist/cache/redis-cache.cjs"
},
"./cache/mongo": {
"types": "./dist/cache/mongo-cache.d.ts",
"import": "./dist/cache/mongo-cache.js",
"require": "./dist/cache/mongo-cache.cjs"
},
"./conversations/memory": {
"types": "./dist/conversations/memory-conversations.d.ts",
"import": "./dist/conversations/memory-conversations.js",
"require": "./dist/conversations/memory-conversations.cjs"
},
"./conversations/mongo": {
"types": "./dist/conversations/mongo-conversations.d.ts",
"import": "./dist/conversations/mongo-conversations.js",
"require": "./dist/conversations/mongo-conversations.cjs"
},
"./package.json": "./package.json"
},
"jest": {
Expand Down
89 changes: 89 additions & 0 deletions src/cache/mongo-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { MongoClient } from 'mongodb';
import { BaseCache } from '../interfaces/base-cache.js';

export class MongoCache implements BaseCache {
private readonly uri: string;
private readonly dbName: string;
private readonly collectionName: string;
private client: MongoClient;

constructor({ uri, dbName, collectionName }: { uri: string, dbName: string, collectionName: string }) {
this.uri = uri;
this.dbName = dbName;
this.collectionName = collectionName;
}

async init(): Promise<void> {
this.client = new MongoClient(this.uri);
await this.client.connect();

// Create index on loaderId field
const collection = this.client.db(this.dbName).collection(this.collectionName);
try {
await collection.createIndex({ loaderId: 1 }, { unique: true });
} catch (error: any) {
//this.debug('Index on loaderId already exists.');
}
}

async addLoader(loaderId: string, chunkCount: number): Promise<void> {
const collection = this.client.db(this.dbName).collection(this.collectionName);
await collection.insertOne({ loaderId, chunkCount });
}

async getLoader(loaderId: string): Promise<{ chunkCount: number }> {
const collection = this.client.db(this.dbName).collection(this.collectionName);
const result = await collection.findOne({ loaderId });
return { chunkCount: result ? result.chunkCount : 0 }; // Assuming a default value of 0 if result is null
}

async hasLoader(loaderId: string): Promise<boolean> {
const collection = this.client.db(this.dbName).collection(this.collectionName);
const result = await collection.findOne({ loaderId });
return !!result;
}

async loaderCustomSet<T extends Record<string, unknown>>(loaderCombinedId: string, value: T): Promise<void> {
const collection = this.client.db(this.dbName).collection(this.collectionName);
const result = await collection.updateOne(
{ loaderId: loaderCombinedId },
{ $setOnInsert: { loaderId: loaderCombinedId, value } },
{ upsert: false }
);

if (result.matchedCount === 0) {
await collection.insertOne({ loaderId: loaderCombinedId, value });
}
}

async loaderCustomGet<T extends Record<string, unknown>>(loaderCombinedId: string): Promise<T> {
const collection = this.client.db(this.dbName).collection(this.collectionName);
const result = await collection.findOne({ loaderId: loaderCombinedId });
return result?.value;
}

async loaderCustomHas(loaderCombinedId: string): Promise<boolean> {
const collection = this.client.db(this.dbName).collection(this.collectionName);
const result = await collection.findOne({ loaderId: loaderCombinedId });
return !!result;
}

async clear(): Promise<void> {
const collection = this.client.db(this.dbName).collection(this.collectionName);
await collection.deleteMany({});
}

async deleteLoader(loaderId: string): Promise<void> {
const collection = this.client.db(this.dbName).collection(this.collectionName);
await collection.deleteOne({ loaderId });
}

async loaderCustomDelete(loaderCombinedId: string): Promise<void> {
const collection = this.client.db(this.dbName).collection(this.collectionName);
await collection.deleteOne({ loaderId: loaderCombinedId });
}

async close(): Promise<void> {
await this.client.close();
}
}
44 changes: 44 additions & 0 deletions src/conversations/memory-conversations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// InMemoryConversations.ts
import { Conversation, ConversationEntry } from '../global/types.js';
import { BaseConversations } from '../interfaces/base-conversations.js';

class InMemoryConversations implements BaseConversations {
private conversations: Map<string, Conversation> = new Map();

async init(): Promise<void> {
this.conversations.clear();
}

async addConversation(conversationId: string): Promise<void> {
if (!this.conversations.has(conversationId)) {
this.conversations.set(conversationId, { conversationId, entries: [] });
}
}

async getConversation(conversationId: string): Promise<Conversation> {
if (!this.conversations.has(conversationId)) {
// Automatically create a new conversation if it does not exist
this.conversations.set(conversationId, { conversationId, entries: [] });
}
return this.conversations.get(conversationId)!;
}

async hasConversation(conversationId: string): Promise<boolean> {
return this.conversations.has(conversationId);
}

async deleteConversation(conversationId: string): Promise<void> {
this.conversations.delete(conversationId);
}

async addEntryToConversation(conversationId: string, entry: ConversationEntry): Promise<void> {
const conversation = await this.getConversation(conversationId);
conversation.entries.push(entry);
}

async clearConversations(): Promise<void> {
this.conversations.clear();
}
}

export { InMemoryConversations };
81 changes: 81 additions & 0 deletions src/conversations/mongo-conversations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { MongoClient } from 'mongodb';
import { Conversation, ConversationEntry } from '../global/types.js';
import { BaseConversations } from '../interfaces/base-conversations.js';

interface ConversationDocument {
_id?: string; // optional MongoDB ID field
conversationId: string;
entries: ConversationEntry[]; // Explicitly stating this is an array of ConversationHistory
}

export class MongoConversations implements BaseConversations {
private readonly uri: string;
private readonly dbName: string;
private readonly collectionName: string;
private client: MongoClient;

constructor({ uri, dbName, collectionName }: { uri: string, dbName: string, collectionName: string }) {
this.uri = uri;
this.dbName = dbName;
this.collectionName = collectionName;
}

async init(): Promise<void> {
this.client = new MongoClient(this.uri);
await this.client.connect();
const collection = this.client.db(this.dbName).collection<ConversationDocument>(this.collectionName);
await collection.createIndex({ conversationId: 1 });
await collection.createIndex({ "entries._id": 1 });
}

async addConversation(conversationId: string): Promise<void> {
const collection = this.client.db(this.dbName).collection(this.collectionName);
// Check if conversation already exists to prevent duplication
const exists = await this.hasConversation(conversationId);
if (!exists) {
await collection.insertOne({ conversationId, entries: [] });
}
}

async getConversation(conversationId: string): Promise<Conversation> {
const collection = this.client.db(this.dbName).collection(this.collectionName);
const document = await collection.findOne({ conversationId });
if (!document) {
// If not found, create a new one automatically
await this.addConversation(conversationId);
return { conversationId, entries: [] };
}
return {
conversationId: document.conversationId,
entries: document.entries as ConversationEntry[]
};
}

async hasConversation(conversationId: string): Promise<boolean> {
const collection = this.client.db(this.dbName).collection(this.collectionName);
const result = await collection.findOne({ conversationId });
return !!result;
}

async deleteConversation(conversationId: string): Promise<void> {
const collection = this.client.db(this.dbName).collection(this.collectionName);
await collection.deleteOne({ conversationId });
}

async addEntryToConversation(conversationId: string, entry: ConversationEntry): Promise<void> {
const collection = this.client.db(this.dbName).collection<ConversationDocument>(this.collectionName);
await collection.updateOne(
{ conversationId },
{ $push: { entries: entry } } // Correctly structured $push operation
);
}

async clearConversations(): Promise<void> {
const collection = this.client.db(this.dbName).collection(this.collectionName);
await collection.deleteMany({});
}

async close(): Promise<void> {
await this.client.close();
}
}
15 changes: 13 additions & 2 deletions src/core/rag-application-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { BaseModel } from '../interfaces/base-model.js';
import { SIMPLE_MODELS } from '../global/constants.js';
import { OpenAi } from '../models/openai-model.js';
import { LoaderParam } from './dynamic-loader-selector.js';
import { BaseConversations } from '../interfaces/base-conversations.js';

export class RAGApplicationBuilder {
private temperature: number;
Expand All @@ -17,15 +18,16 @@ export class RAGApplicationBuilder {
private embeddingRelevanceCutOff: number;
private loaders: LoaderParam[];
private vectorDb: BaseDb;
private conversations: BaseConversations;

constructor() {
this.loaders = [];
this.temperature = 0.1;
this.searchResultCount = 7;

this.queryTemplate = `You are a helpful human like chat bot. Use relevant provided context and chat history to answer the query at the end. Answer in full.
If you don't know the answer, just say that you don't know, don't try to make up an answer.
If you don't know the answer, just say that you don't know, don't try to make up an answer.
Do not use words like context or training data when responding. You can say you do not have all the information but do not indicate that you are not a reliable source.`;

this.setModel(SIMPLE_MODELS.OPENAI_GPT4_O);
Expand Down Expand Up @@ -131,4 +133,13 @@ export class RAGApplicationBuilder {
getModel() {
return this.model;
}

setConversations(conversations: BaseConversations) {
this.conversations = conversations;
return this;
}

getConversations() {
return this.conversations;
}
}
Loading

0 comments on commit f986ade

Please sign in to comment.