Skip to content

Commit

Permalink
chat: add gemini (fixes #7423) (#7424)
Browse files Browse the repository at this point in the history
Co-authored-by: dogi <[email protected]>
  • Loading branch information
Mutugiii and dogi authored Mar 15, 2024
1 parent 6c84c78 commit ec3d368
Show file tree
Hide file tree
Showing 19 changed files with 303 additions and 56 deletions.
1 change: 1 addition & 0 deletions chatapi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
},
"homepage": "https://github.com/open-learning-exchange/planet#readme",
"dependencies": {
"@google/generative-ai": "^0.3.0",
"@types/cors": "^2.8.13",
"@types/express": "^4.17.17",
"cors": "^2.8.5",
Expand Down
9 changes: 9 additions & 0 deletions chatapi/src/config/gemini.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { GoogleGenerativeAI } from '@google/generative-ai';
import dotenv from 'dotenv';

dotenv.config();

const gemini = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || '');

export default gemini;

9 changes: 5 additions & 4 deletions chatapi/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,20 @@ app.get('/', (req: any, res: any) => {

app.post('/', async (req: any, res: any) => {
try {
const { data, save, usePerplexity } = req.body;
const { data, save, aiProvider } = req.body;

if (typeof data !== 'object' || Array.isArray(data) || Object.keys(data).length === 0) {
res.status(400).json({ 'error': 'Bad Request', 'message': 'The "data" field must be a non-empty object' });
}

if (!save) {
const response = await chatNoSave(data.content, usePerplexity);
const response = await chatNoSave(data.content, aiProvider);
res.status(200).json({
'status': 'Success',
'chat': response
});
} else if (save) {
const response = await chat(data, usePerplexity);
const response = await chat(data, aiProvider);
res.status(201).json({
'status': 'Success',
'chat': response?.completionText,
Expand All @@ -51,7 +51,8 @@ app.post('/', async (req: any, res: any) => {
app.get('/checkproviders', (req: any, res: any) => {
res.status(200).json({
'openai': process.env.OPENAI_API_KEY ? true : false,
'perplexity': process.env.PERPLEXITY_API_KEY ? true : false
'perplexity': process.env.PERPLEXITY_API_KEY ? true : false,
'gemini': process.env.GEMINI_API_KEY ? true : false
});
});

Expand Down
6 changes: 6 additions & 0 deletions chatapi/src/models/ai-providers.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type ProviderName = 'openai' | 'perplexity' | 'gemini';

export interface AIProvider {
name: ProviderName;
model?: string;
}
5 changes: 5 additions & 0 deletions chatapi/src/models/chat-message.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@ export interface ChatMessage {
role: 'user' | 'assistant';
content: string;
}

export interface GeminiMessage {
role: 'user' | 'model';
parts: any[];
}
1 change: 1 addition & 0 deletions chatapi/src/models/db-doc.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export interface DbDoc {
user: any;
title: string;
createdDate: number;
aiProvider: string;
conversations: [];
}
12 changes: 7 additions & 5 deletions chatapi/src/services/chat.service.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { DocumentInsertResponse } from 'nano';

import db from '../config/nano.config';
import { gptChat } from '../utils/gpt-chat.utils';
import { aiChat } from '../utils/chat.utils';
import { retrieveChatHistory } from '../utils/db.utils';
import { handleChatError } from '../utils/chat-error.utils';
import { AIProvider } from '../models/ai-providers.model';
import { ChatMessage } from '../models/chat-message.model';

/**
* Create a chat conversation & save in couchdb
* @param data - Chat data including content and additional information
* @returns Object with completion text and CouchDB save response
*/
export async function chat(data: any, usePerplexity: boolean): Promise<{
export async function chat(data: any, aiProvider: AIProvider): Promise<{
completionText: string;
couchSaveResponse: DocumentInsertResponse;
} | undefined> {
Expand All @@ -28,6 +29,7 @@ export async function chat(data: any, usePerplexity: boolean): Promise<{
dbData.title = content;
dbData.conversations = [];
dbData.createdDate = Date.now();
dbData.aiProvider = aiProvider.name;
}

dbData.conversations.push({ 'query': content, 'response': '' });
Expand All @@ -36,7 +38,7 @@ export async function chat(data: any, usePerplexity: boolean): Promise<{
messages.push({ 'role': 'user', content });

try {
const completionText = await gptChat(messages, usePerplexity);
const completionText = await aiChat(messages, aiProvider);

dbData.conversations[dbData.conversations.length - 1].response = completionText;

Expand All @@ -54,13 +56,13 @@ export async function chat(data: any, usePerplexity: boolean): Promise<{
}
}

export async function chatNoSave(content: any, usePerplexity: boolean): Promise< string | undefined> {
export async function chatNoSave(content: any, aiProvider: AIProvider): Promise< string | undefined> {
const messages: ChatMessage[] = [];

messages.push({ 'role': 'user', content });

try {
const completionText = await gptChat(messages, usePerplexity);
const completionText = await aiChat(messages, aiProvider);
messages.push({
'role': 'assistant', 'content': completionText
});
Expand Down
68 changes: 68 additions & 0 deletions chatapi/src/utils/chat.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import openai from '../config/openai.config';
import perplexity from '../config/perplexity.config';
import gemini from '../config/gemini.config';
import { AIProvider, ProviderName } from '../models/ai-providers.model';
import { ChatMessage, GeminiMessage } from '../models/chat-message.model';

const providers: { [key in ProviderName]: { ai: any; defaultModel: string } } = {
'openai': { 'ai': openai, 'defaultModel': 'gpt-3.5-turbo' },
'perplexity': { 'ai': perplexity, 'defaultModel': 'pplx-7b-online' },
'gemini': { 'ai': gemini, 'defaultModel': 'gemini-pro' },
};

async function handleGemini(messages: ChatMessage[], model: string): Promise<string> {
const geminiModel = gemini.getGenerativeModel({ model });

const msg = messages[messages.length - 1].content;

const geminiMessages: GeminiMessage[] = messages.map((message) => ({
'role': message.role === 'assistant' ? 'model' : message.role,
'parts': [{ 'text': message.content }],
}));

geminiMessages.pop();

const chat = geminiModel.startChat({
'history': geminiMessages,
'generationConfig': {
'maxOutputTokens': 100,
},
});

const result = await chat.sendMessage(msg);
const response = await result.response;
const text = response.text();

return text;
}



/**
* Uses openai's createChatCompletion endpoint to generate chat completions
* @param messages - Array of chat messages
* @returns Completion text
*/
export async function aiChat(messages: ChatMessage[], aiProvider: AIProvider): Promise<string> {
const provider = providers[aiProvider.name];
if (!provider) {
throw new Error('Unsupported AI provider');
}
const model = aiProvider.model ?? provider.defaultModel;

if (aiProvider.name === 'gemini') {
return handleGemini(messages, model);
} else {
const completion = await provider.ai.chat.completions.create({
model,
messages,
});

const completionText = completion.choices[0]?.message?.content;
if (!completionText) {
throw new Error('Unexpected API response');
}

return completionText;
}
}
4 changes: 3 additions & 1 deletion chatapi/src/utils/db.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ async function getChatDocument(id: string) {
'conversations': res.conversations,
'title': res.title,
'createdDate': res.createdDate,
'aiProvider': res.aiProvider
};
// Should return user, team data as well particularly for the "/conversations" endpoint
} catch (error) {
Expand All @@ -25,10 +26,11 @@ async function getChatDocument(id: string) {
}

export async function retrieveChatHistory(dbData: any, messages: ChatMessage[]) {
const { conversations, title, createdDate } = await getChatDocument(dbData._id);
const { conversations, title, createdDate, aiProvider } = await getChatDocument(dbData._id);
dbData.conversations = conversations;
dbData.title = title;
dbData.createdDate = createdDate;
dbData.aiProvider = aiProvider;

for (const { query, response } of conversations) {
messages.push({ 'role': 'user', 'content': query });
Expand Down
23 changes: 0 additions & 23 deletions chatapi/src/utils/gpt-chat.utils.ts

This file was deleted.

1 change: 1 addition & 0 deletions docker/chat.env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
OPENAI_API_KEY=
PERPLEXITY_API_KEY=
GEMINI_API_KEY=
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"name": "planet",
"license": "AGPL-3.0",
"version": "0.14.17",
"version": "0.14.18",
"myplanet": {
"latest": "v0.14.5",
"min": "v0.13.57"
"latest": "v0.14.21",
"min": "v0.13.69"
},
"scripts": {
"ng": "ng",
Expand Down
14 changes: 8 additions & 6 deletions src/app/chat/chat-window/chat-window.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

import { CustomValidators } from '../../validators/custom-validators';
import { ConversationForm } from '../chat.model';
import { ConversationForm, AIProvider } from '../chat.model';
import { ChatService } from '../../shared/chat.service';
import { showFormErrors } from '../../shared/table-helpers';
import { UserService } from '../../shared/user.service';
Expand All @@ -17,7 +17,7 @@ import { UserService } from '../../shared/user.service';
export class ChatWindowComponent implements OnInit, OnDestroy {
private onDestroy$ = new Subject<void>();
spinnerOn = true;
usePerplexity: boolean;
provider: AIProvider;
conversations: any[] = [];
selectedConversationId: any;
promptForm: FormGroup;
Expand Down Expand Up @@ -75,7 +75,9 @@ export class ChatWindowComponent implements OnInit, OnDestroy {
this.chatService.toggleAIService$
.pipe(takeUntil(this.onDestroy$))
.subscribe((aiService => {
this.usePerplexity = aiService === 'perplexity' ? true : false;
this.provider = {
name: aiService
};
}));
}

Expand Down Expand Up @@ -123,9 +125,9 @@ export class ChatWindowComponent implements OnInit, OnDestroy {

postSubmit() {
this.changeDetectorRef.detectChanges();
this.spinnerOn = false;
this.spinnerOn = true;
this.scrollTo('bottom');
this.promptForm.reset();
this.promptForm.controls['prompt'].setValue('');
}

onSubmit() {
Expand All @@ -142,7 +144,7 @@ export class ChatWindowComponent implements OnInit, OnDestroy {

this.setSelectedConversation();

this.chatService.getPrompt(this.data, true, this.usePerplexity).subscribe(
this.chatService.getPrompt(this.data, true, this.provider).subscribe(
(completion: any) => {
this.conversations.push({ query: content, response: completion?.chat });
this.selectedConversationId = {
Expand Down
8 changes: 3 additions & 5 deletions src/app/chat/chat.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@
<button mat-icon-button (click)="goBack()"><mat-icon>arrow_back</mat-icon></button>
<span i18n>AI Chat</span>
<span class="toolbar-fill"></span>
<mat-button-toggle-group *ngIf="displayToggle; else textDisplay" [(ngModel)]="aiService" (change)="toggleAIService()">
<mat-button-toggle value="openai" i18n>Open AI</mat-button-toggle>
<mat-button-toggle value="perplexity" i18n>Perplexity AI</mat-button-toggle>
<mat-button-toggle-group *ngIf="displayToggle; else textDisplay" [(ngModel)]="activeService" (change)="toggleAIService()">
<mat-button-toggle *ngFor="let service of aiServices" [value]="service.value" i18n>{{service.name}}</mat-button-toggle>
</mat-button-toggle-group>
<ng-template #textDisplay>
<span *ngIf="aiService === 'openai'" i18n>Open AI</span>
<span *ngIf="aiService === 'perplexity'" i18n>Perplexity AI</span>
<span i18n>{{activeService}}</span>
</ng-template>

</mat-toolbar>
Expand Down
25 changes: 18 additions & 7 deletions src/app/chat/chat.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ import { catchError } from 'rxjs/operators';
import { of } from 'rxjs';

import { ChatService } from '../shared/chat.service';
import { AIServices } from './chat.model';
import { AIServices, ProviderName } from './chat.model';

@Component({
selector: 'planet-chat',
templateUrl: './chat.component.html',
styleUrls: [ './chat.scss' ]
})
export class ChatComponent implements OnInit {
aiService: 'openai' | 'perplexity';
activeService: string;
aiServices: { name: ProviderName, value: ProviderName }[] = [];
displayToggle: boolean;

constructor(
Expand All @@ -25,11 +26,21 @@ export class ChatComponent implements OnInit {
this.chatService.fetchAIProviders().pipe(
catchError(err => {
console.error(err);
return of({ openai: false, perplexity: false });
return of({ openai: false, perplexity: false, gemini: false });
})
).subscribe((aiServices: AIServices) => {
this.displayToggle = aiServices.openai && aiServices.perplexity;
this.aiService = aiServices.openai ? 'openai' : 'perplexity';
).subscribe((services: AIServices) => {
for (const [ key, value ] of Object.entries(services)) {
if (value === true) {
this.aiServices.push({
name: key as ProviderName,
value: key as ProviderName
});
}
}

this.activeService = this.aiServices[0].value;
this.displayToggle = this.aiServices.length > 0;
this.chatService.toggleAIServiceSignal(this.activeService);
});
}

Expand All @@ -38,7 +49,7 @@ export class ChatComponent implements OnInit {
}

toggleAIService(): void {
this.chatService.toggleAIServiceSignal(this.aiService);
this.chatService.toggleAIServiceSignal(this.activeService);
}

}
8 changes: 8 additions & 0 deletions src/app/chat/chat.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,15 @@ export interface Message {
response: string;
}

export type ProviderName = 'openai' | 'perplexity' | 'gemini';

export interface AIProvider {
name: string;
model?: string;
}

export interface AIServices {
openai: boolean;
perplexity: boolean;
gemini: boolean;
}
Loading

0 comments on commit ec3d368

Please sign in to comment.