Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export class AiChatMessageEntity {
@Column({ nullable: true, default: null, type: 'enum', enum: MessageRole })
role: MessageRole;

@Column({ nullable: true, default: null, type: 'varchar', length: 255 })
response_id: string;

@CreateDateColumn({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
created_at: Date;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ export const aiChatMessageRepositoryExtension: IAiChatMessageRepository = {
.getMany();
},

async findLastAiMessageForChat(chatId: string): Promise<AiChatMessageEntity | null> {
return await this.createQueryBuilder('ai_chat_message')
.where('ai_chat_message.ai_chat_id = :chatId', { chatId })
.andWhere('ai_chat_message.role = :role', { role: MessageRole.ai })
.orderBy('ai_chat_message.created_at', 'DESC')
.getOne();
},

async deleteMessagesForChat(chatId: string): Promise<void> {
await this.createQueryBuilder()
.delete()
Expand All @@ -18,11 +26,17 @@ export const aiChatMessageRepositoryExtension: IAiChatMessageRepository = {
.execute();
},

async saveMessage(chatId: string, message: string, role: MessageRole): Promise<AiChatMessageEntity> {
async saveMessage(
chatId: string,
message: string,
role: MessageRole,
responseId?: string,
): Promise<AiChatMessageEntity> {
const newMessage = this.create({
ai_chat_id: chatId,
message,
role,
response_id: responseId,
});
return await this.save(newMessage);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { MessageRole } from '../message-role.enum.js';

export interface IAiChatMessageRepository {
findMessagesForChat(chatId: string): Promise<AiChatMessageEntity[]>;
findLastAiMessageForChat(chatId: string): Promise<AiChatMessageEntity | null>;
deleteMessagesForChat(chatId: string): Promise<void>;
saveMessage(chatId: string, message: string, role: MessageRole): Promise<AiChatMessageEntity>;
saveMessage(chatId: string, message: string, role: MessageRole, responseId?: string): Promise<AiChatMessageEntity>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
isValidMongoDbCommand,
wrapQueryWithLimit,
AIProviderType,
AIProviderConfig,
} from '../../../ai-core/index.js';
import { UserAiChatEntity } from '../ai-conversation-history/user-ai-chat/user-ai-chat.entity.js';
import { MessageRole } from '../ai-conversation-history/ai-chat-messages/message-role.enum.js';
Expand Down Expand Up @@ -94,24 +95,36 @@ export class RequestInfoFromTableWithAIUseCaseV7
});
}

const messages = new MessageBuilder().system(systemPrompt).human(user_message).build();
const { messages, previousResponseId } = await this.buildMessagesWithHistory(
systemPrompt,
user_message,
foundUserAiChat.id,
isNewChat,
);

try {
const { accumulatedResponse } = await this.processWithToolLoop(
const config: AIProviderConfig = {};
if (this.aiProvider === AIProviderType.OPENAI && previousResponseId) {
config.previousResponseId = previousResponseId;
}
Comment on lines +106 to +109

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

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

The aiProvider is hardcoded to AIProviderType.BEDROCK (line 40), but there's conditional logic checking for AIProviderType.OPENAI (lines 107-109). Unless the aiProvider can be changed at runtime or through configuration, this OpenAI-specific code will never execute. If OpenAI support is intended, consider making the aiProvider configurable. If not, the OpenAI-specific logic should be removed to avoid confusion.

Copilot uses AI. Check for mistakes.

const { accumulatedResponse, lastResponseId } = await this.processWithToolLoop(
messages,
tools,
response,
dataAccessObject,
tableName,
userEmail,
foundConnection,
config,
);

if (accumulatedResponse) {
await this._dbContext.aiChatMessageRepository.saveMessage(
foundUserAiChat.id,
accumulatedResponse,
MessageRole.ai,
lastResponseId,
);
Comment on lines 122 to 128

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

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

The lastResponseId could be null if no response chunks contain a responseId. Saving a null response_id to the database is valid per the schema (nullable: true), but consider whether this scenario should be logged or handled specially, as it might indicate an unexpected issue with the AI provider's response format.

Copilot uses AI. Check for mistakes.
}

Expand All @@ -133,15 +146,22 @@ export class RequestInfoFromTableWithAIUseCaseV7
inputTableName: string,
userEmail: string,
foundConnection: ConnectionEntity,
config: AIProviderConfig = {},
): Promise<{ lastResponseId: string | null; accumulatedResponse: string }> {
let currentMessages = [...messages];
let lastResponseId: string | null = null;
let depth = 0;
let totalAccumulatedResponse = '';
let currentConfig = { ...config };

while (depth < this.maxDepth) {
try {
const stream = await this.aiCoreService.streamChatWithToolsAndProvider(this.aiProvider, currentMessages, tools);
const stream = await this.aiCoreService.streamChatWithToolsAndProvider(
this.aiProvider,
currentMessages,
tools,
currentConfig,
);

let pendingToolCalls: AIToolCall[] = [];
let accumulatedContent = '';
Expand Down Expand Up @@ -174,6 +194,10 @@ export class RequestInfoFromTableWithAIUseCaseV7
foundConnection,
);

if (this.aiProvider === AIProviderType.OPENAI && lastResponseId) {

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

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

This conditional check for OpenAI will never be true since aiProvider is hardcoded to AIProviderType.BEDROCK (line 40). The same issue exists on lines 107-109. Consider removing this dead code or making the aiProvider configurable.

Suggested change
if (this.aiProvider === AIProviderType.OPENAI && lastResponseId) {
if (lastResponseId) {

Copilot uses AI. Check for mistakes.
currentConfig = { ...currentConfig, previousResponseId: lastResponseId };
}

const continuationBuilder = MessageBuilder.fromMessages(currentMessages);
continuationBuilder.ai(accumulatedContent, pendingToolCalls);
for (const result of toolResults) {
Expand Down Expand Up @@ -347,6 +371,40 @@ export class RequestInfoFromTableWithAIUseCaseV7
return { foundConnection, dataAccessObject, databaseType, isMongoDb, userEmail };
}

private async buildMessagesWithHistory(
systemPrompt: string,
userMessage: string,
chatId: string,
isNewChat: boolean,
): Promise<{ messages: BaseMessage[]; previousResponseId: string | null }> {
if (isNewChat) {
const messages = new MessageBuilder().system(systemPrompt).human(userMessage).build();
return { messages, previousResponseId: null };
}

if (this.aiProvider === AIProviderType.OPENAI) {
const lastAiMessage = await this._dbContext.aiChatMessageRepository.findLastAiMessageForChat(chatId);
const previousResponseId = lastAiMessage?.response_id || null;
const messages = new MessageBuilder().system(systemPrompt).human(userMessage).build();
return { messages, previousResponseId };
}

Comment on lines +385 to +391

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

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

This conditional check for OpenAI will never be true since aiProvider is hardcoded to AIProviderType.BEDROCK (line 40). The entire if-block (lines 385-390) will never execute, meaning this OpenAI-specific optimization is dead code.

Suggested change
if (this.aiProvider === AIProviderType.OPENAI) {
const lastAiMessage = await this._dbContext.aiChatMessageRepository.findLastAiMessageForChat(chatId);
const previousResponseId = lastAiMessage?.response_id || null;
const messages = new MessageBuilder().system(systemPrompt).human(userMessage).build();
return { messages, previousResponseId };
}

Copilot uses AI. Check for mistakes.
const previousMessages = await this._dbContext.aiChatMessageRepository.findMessagesForChat(chatId);
const builder = new MessageBuilder().system(systemPrompt);

for (const msg of previousMessages) {
if (msg.role === MessageRole.user) {
builder.human(msg.message);
} else if (msg.role === MessageRole.ai) {
builder.ai(msg.message);
}
}

builder.human(userMessage);

Comment on lines +403 to +404

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

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

For non-OpenAI providers with existing chats, the user message is saved to the database first (line 90), then retrieved as part of the history (line 392), and then added again to the message builder (line 403). This results in the current user message being included twice in the conversation context sent to the AI provider. This duplication could confuse the AI and waste tokens. Consider either excluding the most recent user message from the history retrieval, or not saving it until after building the messages.

Suggested change
builder.human(userMessage);

Copilot uses AI. Check for mistakes.
return { messages: builder.build(), previousResponseId: null };
}

private async generateAndUpdateChatName(chatId: string, userMessage: string): Promise<void> {
try {
const CHAT_NAME_GENERATION_PROMPT = `Generate a very short, concise title (max 5-6 words) for a chat conversation based on the user's first question.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddResponseIdToAiChatMessage1769790101930 implements MigrationInterface {
name = 'AddResponseIdToAiChatMessage1769790101930';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "ai_chat_message" ADD "response_id" character varying(255)`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "ai_chat_message" DROP COLUMN "response_id"`);
}
}
Loading