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
6 changes: 6 additions & 0 deletions backend/src/ai-core/tools/query-validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,5 +107,11 @@ export function cleanAIJsonResponse(response: string): string {
if (cleanedResponse.endsWith('```')) {
cleanedResponse = cleanedResponse.slice(0, -3);
}
cleanedResponse = cleanedResponse.trim();

cleanedResponse = cleanedResponse.replace(/^\s*\/\/.*$/gm, '');
cleanedResponse = cleanedResponse.replace(/\/\*[\s\S]*?\*\//g, '');
cleanedResponse = cleanedResponse.replace(/,(\s*[}\]])/g, '$1');
Comment on lines +112 to +114
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

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

The block-comment stripping regex (/\/\*[\s\S]*?\*\//g) will also remove /* ... */ sequences that appear inside valid JSON string values, mutating otherwise-correct AI responses (e.g., a description containing those characters). Consider using a comment-tolerant parser (e.g., JSON5) or implementing a small scanner that removes comments only when not inside quoted strings.

Copilot uses AI. Check for mistakes.

return cleanedResponse.trim();
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,12 @@ export const userAiChatRepositoryExtension: IUserAiChatRepository = {
});
return await this.save(newChat);
},

async updateChatName(chatId: string, name: string): Promise<void> {
await this.createQueryBuilder()
.update(UserAiChatEntity)
.set({ name })
.where('id = :chatId', { chatId })
Comment on lines +36 to +40
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

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

This update query scopes only by id. Since chats are user-owned (and other repo methods require userId), this should also include user_id = :userId (and accept userId as a parameter) to prevent updating another user's chat if a UUID is ever reused/leaked/passed incorrectly.

Suggested change
async updateChatName(chatId: string, name: string): Promise<void> {
await this.createQueryBuilder()
.update(UserAiChatEntity)
.set({ name })
.where('id = :chatId', { chatId })
async updateChatName(chatId: string, userId: string, name: string): Promise<void> {
await this.createQueryBuilder()
.update(UserAiChatEntity)
.set({ name })
.where('id = :chatId AND user_id = :userId', { chatId, userId })

Copilot uses AI. Check for mistakes.
.execute();
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export interface IUserAiChatRepository {
findChatByIdAndUserId(chatId: string, userId: string): Promise<UserAiChatEntity | null>;
findChatWithMessagesByIdAndUserId(chatId: string, userId: string): Promise<UserAiChatEntity | null>;
createChatForUser(userId: string, name?: string): Promise<UserAiChatEntity>;
updateChatName(chatId: string, name: string): Promise<void>;
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

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

updateChatName is defined without a userId parameter, unlike the other repository methods that scope operations by chatId + userId. To avoid accidental cross-tenant updates, consider changing this API to updateChatName(chatId: string, userId: string, name: string) (or similar) so implementations can enforce ownership at the data layer.

Suggested change
updateChatName(chatId: string, name: string): Promise<void>;
updateChatName(chatId: string, userId: string, name: string): Promise<void>;

Copilot uses AI. Check for mistakes.
}
5 changes: 4 additions & 1 deletion backend/src/entities/ai/ai.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,10 @@ Respond ONLY with valid JSON in this exact format (no markdown, no explanations)
]
}
IMPORTANT: For each widget, include appropriate widget_params based on the column name, type, and semantics. Use empty {} for widgets that don't need special configuration.`;
IMPORTANT:
- For each widget, include appropriate widget_params based on the column name, type, and semantics. Use empty {} for widgets that don't need special configuration.
- Output ONLY valid JSON. Do NOT include any comments (no // or /* */ comments) in the JSON output.
- All widget_params must be valid JSON objects without any comments or explanatory text.`;
}

private parseAIResponse(aiResponse: string, tablesInformation: Array<TableInformation>): AIResponse {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export class RequestInfoFromTableWithAIUseCaseV7

let chatIdForHeader: string | null = null;
let foundUserAiChat: UserAiChatEntity | null = null;
let isNewChat = false;

if (ai_thread_id) {
foundUserAiChat = await this._dbContext.userAiChatRepository.findChatByIdAndUserId(ai_thread_id, user_id);
Expand All @@ -78,6 +79,7 @@ export class RequestInfoFromTableWithAIUseCaseV7
if (!foundUserAiChat) {
foundUserAiChat = await this._dbContext.userAiChatRepository.createChatForUser(user_id);
chatIdForHeader = foundUserAiChat.id;
isNewChat = true;
}

if (chatIdForHeader) {
Expand All @@ -86,6 +88,12 @@ export class RequestInfoFromTableWithAIUseCaseV7

await this._dbContext.aiChatMessageRepository.saveMessage(foundUserAiChat.id, user_message, MessageRole.user);

if (isNewChat) {
this.generateAndUpdateChatName(foundUserAiChat.id, user_message).catch((error) => {
Sentry.captureException(error);
});
Comment on lines +92 to +94
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

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

generateAndUpdateChatName() already catches and reports errors internally, so the additional .catch(...) here will never run and makes the async fire-and-forget behavior harder to reason about. Either remove the outer .catch and explicitly mark the call as fire-and-forget (e.g., void ...), or remove the internal try/catch and handle errors only at the call site (but avoid double-reporting).

Suggested change
this.generateAndUpdateChatName(foundUserAiChat.id, user_message).catch((error) => {
Sentry.captureException(error);
});
void this.generateAndUpdateChatName(foundUserAiChat.id, user_message);

Copilot uses AI. Check for mistakes.
}

const messages = new MessageBuilder().system(systemPrompt).human(user_message).build();

try {
Expand Down Expand Up @@ -338,4 +346,32 @@ export class RequestInfoFromTableWithAIUseCaseV7

return { foundConnection, dataAccessObject, databaseType, isMongoDb, userEmail };
}

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.
The title should capture the main topic or intent.
Respond ONLY with the title, no quotes, no explanation.
User question: `;
const prompt = CHAT_NAME_GENERATION_PROMPT + userMessage;
const messages = new MessageBuilder().human(prompt).build();

let generatedName = '';
const stream = await this.aiCoreService.streamChatWithToolsAndProvider(this.aiProvider, messages, []);

for await (const chunk of stream) {
if (chunk.type === 'text' && chunk.content) {
generatedName += chunk.content;
}
}

generatedName = generatedName.trim().slice(0, 100);

if (generatedName) {
await this._dbContext.userAiChatRepository.updateChatName(chatId, generatedName);
}
} catch (error) {
Sentry.captureException(error);
}
}
}
Loading