Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

issue#21 add chat apis #22

Merged
merged 31 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f52f53a
issue#21 add chat apis
ZHallen122 Oct 25, 2024
2dcea74
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 25, 2024
dbc3017
update module
ZHallen122 Oct 25, 2024
97d5541
registerEnumType
ZHallen122 Oct 25, 2024
4e9630a
schema
ZHallen122 Oct 25, 2024
c76078a
Merge branch 'add-chat-apis' of https://github.com/Sma1lboy/Generator…
ZHallen122 Oct 25, 2024
6693d27
fix chat model
ZHallen122 Oct 26, 2024
78a6119
update add input type
ZHallen122 Oct 26, 2024
e23e8d6
input type
ZHallen122 Oct 26, 2024
1f2ab88
update and fix db error
ZHallen122 Oct 26, 2024
aaea83e
update save message and get messages
ZHallen122 Oct 26, 2024
a6fec39
add protection
ZHallen122 Oct 26, 2024
da1463b
add user input message to history
ZHallen122 Oct 26, 2024
06b6dcb
add message guard
ZHallen122 Oct 27, 2024
7af5bdf
comment
ZHallen122 Oct 27, 2024
1f5066d
add /tags api
ZHallen122 Oct 28, 2024
9c8bc0e
fix UpdateChatTitleInput typo, and add getModelTag
ZHallen122 Oct 28, 2024
2735929
do soft delete
ZHallen122 Oct 28, 2024
a3fd075
fix OneToMany
ZHallen122 Oct 28, 2024
e9bc436
fix error meesage
ZHallen122 Oct 28, 2024
f4238b4
update schema
ZHallen122 Oct 28, 2024
f247a09
add getChatWithUser, update saveMessage
ZHallen122 Oct 29, 2024
c512b4c
fix bug: now use getChatWithUser
ZHallen122 Oct 29, 2024
f3210b2
store the big chunk in one
ZHallen122 Oct 29, 2024
74ff944
update to make sure it only get the available chat
ZHallen122 Oct 29, 2024
1573d72
update chatStream
ZHallen122 Oct 29, 2024
bc6ee4b
add a new ChatSubscriptionGuard
ZHallen122 Oct 29, 2024
3e429b4
refactor: Update message model and resolver to use MessageRole enum
Sma1lboy Oct 29, 2024
61535d3
refactor: Update message model and resolver to use MessageRole enum
Sma1lboy Oct 29, 2024
fc8929c
Merge branch 'main' into add-chat-apis
Sma1lboy Oct 29, 2024
5528efe
Merge branch 'main' into add-chat-apis
Sma1lboy Oct 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 40 additions & 7 deletions backend/src/chat/chat.model.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,49 @@
import { Field, InputType, ObjectType } from '@nestjs/graphql';
import {
Field,
InputType,
ObjectType,
ID,
registerEnumType,
} from '@nestjs/graphql';
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
OneToMany,
JoinColumn,
} from 'typeorm';
import { forwardRef } from '@nestjs/common';
import { Message } from 'src/chat/message.model';
import { SystemBaseModel } from 'src/system-base-model/system-base.model';
import { User } from 'src/user/user.model';

@Entity()
@ObjectType()
export class Chat extends SystemBaseModel {
@PrimaryGeneratedColumn('uuid')
@Field(() => ID)
id: string;

@Field({ nullable: true })
@Column({ nullable: true })
title: string;

@Field(() => [Message], { nullable: true })
@OneToMany(() => Message, (message) => message.chat, { cascade: true })
messages: Message[];

@ManyToOne(() => User, (user) => user.chats)
@Field(() => User)
user: User;
}

@ObjectType('ChatCompletionDeltaType')
class ChatCompletionDelta {
@Field({ nullable: true })
content?: string;
}

@ObjectType('ChatCompletionChunkType')
export class ChatCompletionChunk {
@Field()
Expand Down Expand Up @@ -37,9 +76,3 @@ class ChatCompletionChoice {
@Field({ nullable: true })
finish_reason: string | null;
}

@InputType('ChatInputType')
export class ChatInput {
@Field()
message: string;
}
24 changes: 21 additions & 3 deletions backend/src/chat/chat.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,28 @@ import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { HttpModule } from '@nestjs/axios';
import { ChatResolver } from './chat.resolver';
import { ChatProxyService } from './chat.service';
import { ChatProxyService, ChatService } from './chat.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from 'src/user/user.model';
import { Chat } from './chat.model';
import { Message } from 'src/chat/message.model';
import { ChatGuard } from '../guard/chat.guard';
import { AuthModule } from '../auth/auth.module';
import { UserService } from 'src/user/user.service';
Sma1lboy marked this conversation as resolved.
Show resolved Hide resolved

@Module({
imports: [HttpModule],
providers: [ChatResolver, ChatProxyService],
imports: [
HttpModule,
TypeOrmModule.forFeature([Chat, User, Message]),
AuthModule,
],
providers: [
ChatResolver,
ChatProxyService,
ChatService,
ChatGuard,
UserService,
],
exports: [ChatService, ChatGuard],
})
export class ChatModule {}
93 changes: 88 additions & 5 deletions backend/src/chat/chat.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,42 @@
import { Resolver, Subscription, Args } from '@nestjs/graphql';
import { ChatCompletionChunk, ChatInput } from './chat.model';
import { ChatProxyService } from './chat.service';
import { Resolver, Subscription, Args, Query, Mutation } from '@nestjs/graphql';
import { ChatCompletionChunk } from './chat.model';
import { ChatProxyService, ChatService } from './chat.service';
import { UserService } from 'src/user/user.service';
import { Chat } from './chat.model';
import { Message, Role } from 'src/chat/message.model';
import {
NewChatInput,
UpateChatTitleInput,
ChatInput,
} from 'src/chat/dto/chat.input';
import { UseGuards } from '@nestjs/common';
ZHallen122 marked this conversation as resolved.
Show resolved Hide resolved
import { ChatGuard, MessageGuard } from '../guard/chat.guard';
import { GetUserIdFromToken } from '../decorator/get-auth-token';

@Resolver('Chat')
export class ChatResolver {
constructor(private chatProxyService: ChatProxyService) {}
constructor(
private chatProxyService: ChatProxyService,
private chatService: ChatService,
private userService: UserService,
) {}

@Subscription(() => ChatCompletionChunk, {
nullable: true,
resolve: (value) => value,
})
async *chatStream(@Args('input') input: ChatInput) {
const iterator = this.chatProxyService.streamChat(input.message);

this.chatService.saveMessage(input.chatId, null, input.message, Role.User);
try {
for await (const chunk of iterator) {
if (chunk) {
await this.chatService.saveMessage(
input.chatId,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ont message for one response

chunk.id,
chunk.choices[0].delta.content,
Role.Model,
);
yield chunk;
Sma1lboy marked this conversation as resolved.
Show resolved Hide resolved
}
}
Expand All @@ -24,4 +45,66 @@ export class ChatResolver {
throw new Error('Chat stream failed');
}
}

@Query(() => [Chat], { nullable: true })
async getUserChats(@GetUserIdFromToken() userId: string): Promise<Chat[]> {
const user = await this.userService.getUserChats(userId);
return user ? user.chats : []; // Return chats if user exists, otherwise return an empty array
}

@UseGuards(MessageGuard)
@Query(() => Message, { nullable: true })
async getMessageDetail(
@GetUserIdFromToken() userId: string,
@Args('messageId') messageId: string,
): Promise<Message> {
return this.chatService.getMessageById(messageId);
}

Sma1lboy marked this conversation as resolved.
Show resolved Hide resolved
// To do: message need a update resolver

@UseGuards(ChatGuard)
@Query(() => [Message])
async getChatHistory(@Args('chatId') chatId: string): Promise<Message[]> {
return this.chatService.getChatHistory(chatId);
}

@UseGuards(ChatGuard)
@Query(() => Chat, { nullable: true })
async getChatDetails(@Args('chatId') chatId: string): Promise<Chat> {
return this.chatService.getChatDetails(chatId);
}

// @Query(() => [Message])
// getAvailableModelTags(@Args('chatId') chatId: string): Message[] {
// return this.chatService.getChatHistory(chatId);
// }

@Mutation(() => Chat)
async createChat(
@GetUserIdFromToken() userId: string,
@Args('newChatInput') newChatInput: NewChatInput,
): Promise<Chat> {
return this.chatService.createChat(userId, newChatInput);
}

@UseGuards(ChatGuard)
@Mutation(() => Boolean)
async deleteChat(@Args('chatId') chatId: string): Promise<boolean> {
return this.chatService.deleteChat(chatId);
}

@UseGuards(ChatGuard)
@Mutation(() => Boolean)
async clearChatHistory(@Args('chatId') chatId: string): Promise<boolean> {
return this.chatService.clearChatHistory(chatId);
}

@UseGuards(ChatGuard)
@Mutation(() => Chat, { nullable: true })
async updateChatTitle(
@Args('upateChatTitleInput') upateChatTitleInput: UpateChatTitleInput,
): Promise<Chat> {
return this.chatService.updateChatTitle(upateChatTitleInput);
ZHallen122 marked this conversation as resolved.
Show resolved Hide resolved
}
}
116 changes: 115 additions & 1 deletion backend/src/chat/chat.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { Injectable, Logger } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { ChatCompletionChunk } from './chat.model';
import { ChatCompletionChunk, Chat } from './chat.model';
import { Message, Role } from 'src/chat/message.model';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from 'src/user/user.model';
import { NewChatInput, UpateChatTitleInput } from 'src/chat/dto/chat.input';

type CustomAsyncIterableIterator<T> = AsyncIterator<T> & {
[Symbol.asyncIterator](): AsyncIterableIterator<T>;
Expand Down Expand Up @@ -132,3 +137,112 @@ export class ChatProxyService {
);
}
}

@Injectable()
export class ChatService {
constructor(
@InjectRepository(Chat)
private chatRepository: Repository<Chat>,
@InjectRepository(User)
private userRepository: Repository<User>,
@InjectRepository(Message)
private messageRepository: Repository<Message>,
) {}

async getChatHistory(chatId: string): Promise<Message[]> {
const chat = await this.chatRepository.findOne({
where: { id: chatId },
order: { createdAt: 'ASC' },
relations: ['messages'],
});
return chat ? chat.messages : [];
}
ZHallen122 marked this conversation as resolved.
Show resolved Hide resolved

async getMessageById(messageId: string): Promise<Message> {
return await this.messageRepository.findOne({
where: { id: messageId },
});
}

async getChatDetails(chatId: string): Promise<Chat> {
return this.chatRepository.findOne({
where: { id: chatId },
relations: ['messages'],
});
}

async createChat(userId: string, newChatInput: NewChatInput): Promise<Chat> {
// Fetch the user entity using the userId
const user = await this.userRepository.findOne({ where: { id: userId } });
if (!user) {
throw new Error('User not found');
}

// Create a new chat and associate it with the user
const newChat = this.chatRepository.create({
title: newChatInput.title,
messages: [],
createdAt: new Date(),
updatedAt: new Date(),
user: user, // Associate the user with the chat
});

return await this.chatRepository.save(newChat);
}
Comment on lines +205 to +222
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add input validation for chat creation

The createChat method should validate the input title before creating a new chat.

  async createChat(userId: string, newChatInput: NewChatInput): Promise<Chat> {
    const user = await this.userRepository.findOne({ where: { id: userId } });
    if (!user) {
      throw new Error('User not found');
    }

+   if (!newChatInput.title?.trim()) {
+     throw new Error('Chat title cannot be empty');
+   }

    const newChat = this.chatRepository.create({
      title: newChatInput.title,
      messages: [],
      createdAt: new Date(),
      updatedAt: new Date(),
      user: user,
    });

    return await this.chatRepository.save(newChat);
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async createChat(userId: string, newChatInput: NewChatInput): Promise<Chat> {
// Fetch the user entity using the userId
const user = await this.userRepository.findOne({ where: { id: userId } });
if (!user) {
throw new Error('User not found');
}
// Create a new chat and associate it with the user
const newChat = this.chatRepository.create({
title: newChatInput.title,
messages: [],
createdAt: new Date(),
updatedAt: new Date(),
user: user, // Associate the user with the chat
});
return await this.chatRepository.save(newChat);
}
async createChat(userId: string, newChatInput: NewChatInput): Promise<Chat> {
// Fetch the user entity using the userId
const user = await this.userRepository.findOne({ where: { id: userId } });
if (!user) {
throw new Error('User not found');
}
if (!newChatInput.title?.trim()) {
throw new Error('Chat title cannot be empty');
}
// Create a new chat and associate it with the user
const newChat = this.chatRepository.create({
title: newChatInput.title,
messages: [],
createdAt: new Date(),
updatedAt: new Date(),
user: user, // Associate the user with the chat
});
return await this.chatRepository.save(newChat);
}


async deleteChat(chatId: string): Promise<boolean> {
const result = await this.chatRepository.delete(chatId);
return result.affected > 0;
}

async clearChatHistory(chatId: string): Promise<boolean> {
const chat = await this.chatRepository.findOne({
where: { id: chatId },
relations: ['messages'],
});
if (chat) {
chat.messages = [];
chat.updatedAt = new Date();
await this.chatRepository.save(chat);
return true;
}
return false;
}
ZHallen122 marked this conversation as resolved.
Show resolved Hide resolved

async updateChatTitle(
upateChatTitleInput: UpateChatTitleInput,
ZHallen122 marked this conversation as resolved.
Show resolved Hide resolved
): Promise<Chat> {
const chat = await this.chatRepository.findOne({
where: { id: upateChatTitleInput.chatId },
});
if (chat) {
chat.title = upateChatTitleInput.title;
chat.updatedAt = new Date();
return await this.chatRepository.save(chat);
}
return null;
}

async saveMessage(
chatId: string,
chunkId: string | null,
messageContent: string,
role: Role,
): Promise<Message> {
// Find the chat instance
const chat = await this.chatRepository.findOne({ where: { id: chatId } });
if (!chat) throw new Error('Chat not found');

// Create a new message associated with the chat
const message = this.messageRepository.create({
id: chunkId || null,
content: messageContent,
role: role,
chat,
createdAt: new Date(),
});

// Save the message to the database
return await this.messageRepository.save(message);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add input validation for message content

The saveMessage method should validate the message content before saving.

  async saveMessage(
    chatId: string,
    chunkId: string | null,
    messageContent: string,
    role: Role,
  ): Promise<Message> {
+   if (!messageContent?.trim()) {
+     throw new Error('Message content cannot be empty');
+   }

    const chat = await this.chatRepository.findOne({ where: { id: chatId } });
    if (!chat) throw new Error('Chat not found');

    const message = this.messageRepository.create({
      id: chunkId || null,
      content: messageContent,
      role: role,
      chat,
      createdAt: new Date(),
    });

    return await this.messageRepository.save(message);
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async saveMessage(
chatId: string,
chunkId: string | null,
messageContent: string,
role: Role,
): Promise<Message> {
// Find the chat instance
const chat = await this.chatRepository.findOne({ where: { id: chatId } });
if (!chat) throw new Error('Chat not found');
// Create a new message associated with the chat
const message = this.messageRepository.create({
id: chunkId || null,
content: messageContent,
role: role,
chat,
createdAt: new Date(),
});
// Save the message to the database
return await this.messageRepository.save(message);
}
async saveMessage(
chatId: string,
chunkId: string | null,
messageContent: string,
role: Role,
): Promise<Message> {
if (!messageContent?.trim()) {
throw new Error('Message content cannot be empty');
}
// Find the chat instance
const chat = await this.chatRepository.findOne({ where: { id: chatId } });
if (!chat) throw new Error('Chat not found');
// Create a new message associated with the chat
const message = this.messageRepository.create({
id: chunkId || null,
content: messageContent,
role: role,
chat,
createdAt: new Date(),
});
// Save the message to the database
return await this.messageRepository.save(message);
}

}
27 changes: 27 additions & 0 deletions backend/src/chat/dto/chat.input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// DTOs for Project APIs
import { InputType, Field, ID } from '@nestjs/graphql';
import { Message } from 'src/chat/message.model';

ZHallen122 marked this conversation as resolved.
Show resolved Hide resolved
@InputType()
export class NewChatInput {
@Field({ nullable: true })
title: string;
}

@InputType()
export class UpateChatTitleInput {
@Field()
chatId: string;

@Field({ nullable: true })
title: string;
}
ZHallen122 marked this conversation as resolved.
Show resolved Hide resolved

@InputType('ChatInputType')
export class ChatInput {
@Field()
chatId: string;

@Field()
message: string;
}
ZHallen122 marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading