Skip to content
This repository has been archived by the owner on Sep 9, 2024. It is now read-only.

Commit

Permalink
Merge pull request #67 from OcularEngineering/AddOpenAIPlugin
Browse files Browse the repository at this point in the history
Add Open AI Service As Second LLM Provider
  • Loading branch information
MichaelMoyoMushabati authored May 2, 2024
2 parents f84f377 + 41257f8 commit 032dc6d
Show file tree
Hide file tree
Showing 16 changed files with 325 additions and 11 deletions.
6 changes: 6 additions & 0 deletions env.dev.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ DATABASE_NAME=ocular
QDRANT_DB_URL=http://localhost:6333
UI_CORS=http://localhost:9000
REDIS_URL=redis://localhost:6379
KAFKA_URL=localhost:9092


# Required Azure Open AI credentials
Expand All @@ -15,6 +16,11 @@ AZURE_OPEN_AI_EMBEDDING_MODEL=
AZURE_OPEN_AI_CHAT_DEPLOYMENT_NAME=
AZURE_OPEN_AI_CHAT_MODEL=

# Required Open AI credentials
OPEN_AI_KEY=
OPEN_AI_EMBEDDING_MODEL=
OPEN_AI_CHAT_MODEL=

# Add these if you want to enable index data from apps

# Google (Gmail + Google Drive)
Expand Down
15 changes: 14 additions & 1 deletion packages/ocular/core-config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const { PluginNameDefinitions } = require('@ocular/types');
const dotenv = require("dotenv");

let ENV_FILE_NAME = "";
Expand Down Expand Up @@ -149,7 +150,7 @@ module.exports = {
},
},
{
resolve: `azure-open-ai`,
resolve: PluginNameDefinitions.AZUREOPENAI,
options: {
open_ai_key: process.env.AZURE_OPEN_AI_KEY,
open_ai_version: "2023-05-15",
Expand All @@ -165,6 +166,18 @@ module.exports = {
},
},
},
// {
// resolve: PluginNameDefinitions.OPENAI,
// options: {
// open_ai_key: process.env.OPEN_AI_KEY,
// embedding_model: process.env.OPEN_AI_EMBEDDING_MODEL,
// chat_model: process.env.OPEN_AI_CHAT_MODEL,
// rate_limiter_opts: {
// requests: 1000000, // Number of Tokens
// interval: 60, // Interval in Seconds
// },
// },
// },
{
resolve: `qdrant-vector-search-service`,
options: {
Expand Down
4 changes: 2 additions & 2 deletions packages/plugins/azure-open-ai/src/loaders/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AppNameDefinitions, RateLimiterOpts} from "@ocular/types";
import { AppNameDefinitions, PluginNameDefinitions, RateLimiterOpts} from "@ocular/types";
import { RateLimiterService } from "@ocular/ocular";

export default async (container, options) => {
Expand All @@ -9,7 +9,7 @@ export default async (container, options) => {
}
const rateLimiterOpts: RateLimiterOpts = options.rate_limiter_opts
const rateLimiterService: RateLimiterService = container.resolve("rateLimiterService")
await rateLimiterService.register("OpenAI",rateLimiterOpts.requests, rateLimiterOpts.interval);
await rateLimiterService.register(PluginNameDefinitions.AZUREOPENAI,rateLimiterOpts.requests, rateLimiterOpts.interval);
} catch (err) {
console.log(err)
}
Expand Down
12 changes: 5 additions & 7 deletions packages/plugins/azure-open-ai/src/services/open-ai.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { IndexableDocument, AbstractLLMService, IndexableDocChunk, Message } from "@ocular/types";
import { IndexableDocument, AbstractLLMService, IndexableDocChunk, Message, PluginNameDefinitions } from "@ocular/types";
import { OpenAI } from 'openai';
import { encoding_for_model, type TiktokenModel } from 'tiktoken';
import { RateLimiterService } from "@ocular/ocular";
import { RateLimiterQueue } from "rate-limiter-flexible"

export default class OpenAIService extends AbstractLLMService {
export default class AzureOpenAIService extends AbstractLLMService {

static identifier = "azure-open-ai"
static identifier = PluginNameDefinitions.AZUREOPENAI

protected openAIKey_: string
protected embeddingsClient_: OpenAI
Expand All @@ -24,11 +24,9 @@ export default class OpenAIService extends AbstractLLMService {
constructor(container, options) {
super(arguments[0],options)

console.log("Azure Open AI: Options",options)

// Rate Limiter Service
this.rateLimiterService_ = container.rateLimiterService;
this.requestQueue_ = this.rateLimiterService_.getRequestQueue("OpenAI");
this.requestQueue_ = this.rateLimiterService_.getRequestQueue(PluginNameDefinitions.AZUREOPENAI);

this.azureOpenAiApiVersion_ = options.open_ai_version,
this.endpoint_= options.endpoint
Expand Down Expand Up @@ -65,7 +63,7 @@ export default class OpenAIService extends AbstractLLMService {
async createEmbeddings(text:string): Promise<number[]> {
try{
const tokenCount = this.getChatModelTokenCount(text)
await this.requestQueue_.removeTokens(tokenCount,"OpenAI")
await this.requestQueue_.removeTokens(tokenCount,PluginNameDefinitions.AZUREOPENAI)
const result = await this.embeddingsClient_.embeddings.create({
input: text,
model: this.embeddingModel_,
Expand Down
9 changes: 9 additions & 0 deletions packages/plugins/open-ai/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/lib
node_modules
.DS_store
.env*
/*.js
!index.js
yarn.lock
.turbo
/dist
13 changes: 13 additions & 0 deletions packages/plugins/open-ai/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/lib
node_modules
.DS_store
.env*
/*.js
!index.js
package-lock.json
yarn.lock
src
.gitignore
.eslintrc
.babelrc
.prettierrc
31 changes: 31 additions & 0 deletions packages/plugins/open-ai/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# GoogleDrive

Intergrate Ocular with Open AI Service.


## Features

- Allow Ocular To Communicate With Open AI Service. Ocular uses an LLMService such as Open AI to generate embeddings and perfom Chat completion.


## How to Install

1\. In `ocular/core-config.js` add the following at the end of the `plugins` array:

```js
const plugins = [
// ...
{
resolve: `open-ai`,
options: {
// open_ai_key: process.env.AZURE_OPEN_AI_KEY,
// open_ai_version: "2023-05-15",
// endpoint: process.env.AZURE_OPEN_AI_ENDPOINT,
// embedding_deployment_name: process.env.AZURE_OPEN_AI_EMBEDDER_DEPLOYMENT_NAME,
// embedding_model: process.env.AZURE_OPEN_AI_EMBEDDING_MODEL,
// chat_deployment_name: process.env.AZURE_OPEN_AI_CHAT_DEPLOYMENT_NAME,
// chat_model: process.env.AZURE_OPEN_AI_CHAT_MODEL,
}
},
]
```
19 changes: 19 additions & 0 deletions packages/plugins/open-ai/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module.exports = {
globals: {
preset: 'ts-jest',
testEnvironment: 'node',
"ts-jest": {
tsconfig: "tsconfig.spec.json",
isolatedModules: false,
},
},
transform: {
"^.+\\.[jt]s?$": "ts-jest",
},
transformIgnorePatterns: [
"/node_modules/(?!(axios)/).*",
"/dist/"
],
testEnvironment: `node`,
moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`],
}
26 changes: 26 additions & 0 deletions packages/plugins/open-ai/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "open-ai",
"version": "0.0.0",
"description": "Ocular Open AI",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"author": "Louis Murerwa",
"devDependencies": {
"cross-env": "^5.2.1",
"typeorm": "^0.3.16",
"typescript": "^4.9.5"
},
"scripts": {
"prepare": "cross-env NODE_ENV=production npm run build",
"test": "jest src",
"build": "tsc",
"watch": "tsc --watch",
"clean": "rimraf dist node_modules"
},
"dependencies": {
"@ocular/types": "*",
"@ocular/utils": "*",
"openai": "^4.29.2",
"tiktoken": "^1.0.13"
}
}
52 changes: 52 additions & 0 deletions packages/plugins/open-ai/src/__tests__/open-ai
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { OpenAI } from 'openai';
import OpenAIService from '../services/open-ai';

describe('OpenAIService', () => {
let service;

beforeEach(() => {
service = new OpenAIService({
rateLimiterService: {
getRequestQueue: jest.fn().mockReturnValue({
removeTokens: jest.fn()
})
}
}, {
embedding_model: process.env.OPEN_AI_EMBEDDING_MODEL,
open_ai_key: process.env.OPEN_AI_KEY,
chat_model: process.env.OPEN_AI_CHAT_MODEL
});
});

it('should create embeddings', async () => {
const doc = { content: 't' };
const embeddings = await service.createEmbeddings("test content");
expect(embeddings).toEqual([1, 2, 3]);
expect(OpenAI).toHaveBeenCalledWith({
apiKey: 'test-key',
defaultQuery: { 'api-version': 'test-version' },
defaultHeaders: { 'api-key': 'test-key' },
baseURL: 'test-endpoint/openai/deployments/test-deployment'
});
expect(service.openai_.embeddings.create).toHaveBeenCalledWith({
input: 'test content',
model: 'test-model'
});
});

it('should return empty array if doc content is not provided', async () => {
const doc = {};
const embeddings = await service.createEmbeddings("test");
expect(embeddings).toEqual([]);
});

// it('should complete chat', async () => {
// const messages = [
// {role: 'system', content: 'You are a helpful assistant.'},
// {role: 'user', content: 'Translate the following English text to French: "Hello, how are you?"'}
// ];
// const result = await service.completeChat(messages);
// expect(result).toEqual( "\"Bonjour, comment vas-tu ?\"");
// });

});
17 changes: 17 additions & 0 deletions packages/plugins/open-ai/src/loaders/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { AppNameDefinitions, RateLimiterOpts, PluginNameDefinitions} from "@ocular/types";
import { RateLimiterService } from "@ocular/ocular";

export default async (container, options) => {
try {
// Register Rate Limiter For Google Drive
if (!options.rate_limiter_opts) {
throw new Error("No options provided for rate limiter")
}
const rateLimiterOpts: RateLimiterOpts = options.rate_limiter_opts
const rateLimiterService: RateLimiterService = container.resolve("rateLimiterService")
console.log("Registering Rate Limiter For OpenAI",options)
await rateLimiterService.register(PluginNameDefinitions.OPENAI,rateLimiterOpts.requests, rateLimiterOpts.interval);
} catch (err) {
console.log(err)
}
}
81 changes: 81 additions & 0 deletions packages/plugins/open-ai/src/services/open-ai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { IndexableDocument, AbstractLLMService, IndexableDocChunk, Message, PluginNameDefinitions } from "@ocular/types";
import { OpenAI } from 'openai';
import { encoding_for_model, type TiktokenModel } from 'tiktoken';
import { RateLimiterService } from "@ocular/ocular";
import { RateLimiterQueue } from "rate-limiter-flexible"

export default class OpenAIService extends AbstractLLMService {

static identifier = PluginNameDefinitions.OPENAI

protected openAIKey_: string
protected openAI_: OpenAI
protected embeddingModel_: string
protected chatModel_: string
protected tokenLimit_:number = 4096
protected rateLimiterService_: RateLimiterService;
protected requestQueue_: RateLimiterQueue

constructor(container, options) {
super(arguments[0],options)

// Rate Limiter Service
this.rateLimiterService_ = container.rateLimiterService;
this.requestQueue_ = this.rateLimiterService_.getRequestQueue(PluginNameDefinitions.OPENAI);

// Models
this.embeddingModel_ = options.embedding_model
this.chatModel_ = options.chat_model

// Chat Deployment
this.openAIKey_ = options.open_ai_key
this.openAI_ = new OpenAI({
apiKey: this.openAIKey_ || ""
})
}

async createEmbeddings(text:string): Promise<number[]> {
try{
// Rate Limiter Limits On Token Count
const tokenCount = this.getChatModelTokenCount(text)
await this.requestQueue_.removeTokens(tokenCount,PluginNameDefinitions.OPENAI)
const result = await this.openAI_.embeddings.create({
model: this.embeddingModel_,
input: text
})
return result.data[0].embedding;
} catch(error){
console.log("Open AI: Error",error)
}
}

async completeChat(messages: Message[]): Promise<string> {
try{
const result = await this.openAI_.chat.completions.create({
model: this.chatModel_,
messages,
temperature: 0.3,
max_tokens: 1024,
n: 1,
});
console.log("Result Open AI",result.choices[0].message.content)
return result.choices[0].message.content;
}catch(error){
console.log("Open AI: Error",error)
}
}

getChatModelTokenCount(content : string): number {
const encoder = encoding_for_model(this.chatModel_ as TiktokenModel);
let tokens = 2;
for (const value of Object.values(content)) {
tokens += encoder.encode(value).length;
}
encoder.free();
return tokens;
}

getTokenLimit(): number {
return this.tokenLimit_;
}
}
Loading

0 comments on commit 032dc6d

Please sign in to comment.