mirror of https://github.com/buster-so/buster.git
Enhance chat functionality by adding user last used shortcuts tracking and updating request handling to include metadata. Refactor shortcut listing to prioritize last used shortcuts in the response.
This commit is contained in:
parent
cddd790761
commit
e865dc79c4
|
@ -1,4 +1,4 @@
|
|||
import { getUserOrganizationId } from '@buster/database';
|
||||
import { getUserOrganizationId, updateUserLastUsedShortcuts } from '@buster/database';
|
||||
import type { User } from '@buster/database';
|
||||
import {
|
||||
type ChatCreateHandlerRequest,
|
||||
|
@ -9,7 +9,6 @@ import {
|
|||
import { tasks } from '@trigger.dev/sdk';
|
||||
import { handleAssetChat, handleAssetChatWithPrompt } from './services/chat-helpers';
|
||||
import { initializeChat } from './services/chat-service';
|
||||
import { enhanceMessageWithShortcut } from './services/shortcut-service';
|
||||
|
||||
/**
|
||||
* Handler function for creating a new chat.
|
||||
|
@ -57,20 +56,13 @@ export async function createChatHandler(
|
|||
throw new ChatError(ChatErrorCode.INVALID_REQUEST, 'prompt or asset_id is required', 400);
|
||||
}
|
||||
|
||||
// Process shortcuts if present
|
||||
let enhancedPrompt = request.prompt;
|
||||
if (request.prompt) {
|
||||
// Check for shortcut pattern in message (e.g., /weekly-report)
|
||||
enhancedPrompt = await enhanceMessageWithShortcut(request.prompt, user.id, organizationId);
|
||||
}
|
||||
|
||||
// Set message_analysis_mode if not provided (from staging)
|
||||
if (request.prompt && !request.message_analysis_mode) {
|
||||
request.message_analysis_mode = 'auto';
|
||||
}
|
||||
|
||||
// Update request with enhanced prompt and message_analysis_mode
|
||||
const processedRequest = { ...request, prompt: enhancedPrompt };
|
||||
// Update request with message_analysis_mode
|
||||
const processedRequest = { ...request };
|
||||
// Initialize chat (new or existing)
|
||||
// When we have both asset and prompt, we'll skip creating the initial message
|
||||
// since handleAssetChatWithPrompt will create both the import and prompt messages
|
||||
|
@ -85,6 +77,17 @@ export async function createChatHandler(
|
|||
|
||||
const { chatId, messageId, chat } = await initializeChat(modifiedRequest, user, organizationId);
|
||||
|
||||
// Update user's last used shortcuts if any were provided in metadata
|
||||
if (
|
||||
processedRequest.metadata?.shortcutIds &&
|
||||
processedRequest.metadata.shortcutIds.length > 0
|
||||
) {
|
||||
await updateUserLastUsedShortcuts({
|
||||
userId: user.id,
|
||||
shortcutIds: processedRequest.metadata.shortcutIds,
|
||||
});
|
||||
}
|
||||
|
||||
// Handle asset-based chat if needed
|
||||
let finalChat: ChatWithMessages = chat;
|
||||
let actualMessageId = messageId; // Track the actual message ID to use for triggering
|
||||
|
|
|
@ -18,6 +18,7 @@ import type {
|
|||
ChatMessageResponseMessage,
|
||||
ChatWithMessages,
|
||||
MessageAnalysisMode,
|
||||
MessageMetadata,
|
||||
} from '@buster/server-shared/chats';
|
||||
import { ChatError, ChatErrorCode } from '@buster/server-shared/chats';
|
||||
import { PostProcessingMessageSchema } from '@buster/server-shared/message';
|
||||
|
@ -204,7 +205,8 @@ export async function handleExistingChat(
|
|||
prompt: string | undefined,
|
||||
messageAnalysisMode: MessageAnalysisMode | undefined,
|
||||
user: User,
|
||||
redoFromMessageId?: string
|
||||
redoFromMessageId?: string,
|
||||
metadata?: MessageMetadata
|
||||
): Promise<{
|
||||
chatId: string;
|
||||
messageId: string;
|
||||
|
@ -262,6 +264,7 @@ export async function handleExistingChat(
|
|||
messageAnalysisMode: messageAnalysisMode,
|
||||
userId: user.id,
|
||||
messageId,
|
||||
metadata,
|
||||
})
|
||||
: Promise.resolve(null),
|
||||
getMessagesForChat(chatId),
|
||||
|
@ -295,6 +298,7 @@ export async function handleNewChat({
|
|||
messageAnalysisMode,
|
||||
user,
|
||||
organizationId,
|
||||
metadata,
|
||||
}: {
|
||||
title: string;
|
||||
messageId: string;
|
||||
|
@ -302,6 +306,7 @@ export async function handleNewChat({
|
|||
messageAnalysisMode: MessageAnalysisMode | undefined;
|
||||
user: User;
|
||||
organizationId: string;
|
||||
metadata?: MessageMetadata;
|
||||
}): Promise<{
|
||||
chatId: string;
|
||||
messageId: string;
|
||||
|
@ -346,6 +351,7 @@ export async function handleNewChat({
|
|||
content: prompt,
|
||||
} as ModelMessage,
|
||||
],
|
||||
metadata: metadata || {},
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
|
|
@ -47,7 +47,8 @@ export async function initializeChat(
|
|||
request.prompt,
|
||||
request.message_analysis_mode,
|
||||
user,
|
||||
redoFromMessageId
|
||||
redoFromMessageId,
|
||||
request.metadata
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -59,6 +60,7 @@ export async function initializeChat(
|
|||
messageAnalysisMode: request.message_analysis_mode,
|
||||
user,
|
||||
organizationId,
|
||||
metadata: request.metadata,
|
||||
});
|
||||
} catch (error) {
|
||||
// Log detailed error context
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import type { User } from '@buster/database';
|
||||
import { getUserOrganizationId, getUserShortcuts } from '@buster/database';
|
||||
import { db, getUserOrganizationId, getUserShortcuts, users } from '@buster/database';
|
||||
import { eq } from '@buster/database';
|
||||
import type { ListShortcutsResponse } from '@buster/server-shared/shortcuts';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
|
||||
|
@ -16,14 +17,47 @@ export async function listShortcutsHandler(user: User): Promise<ListShortcutsRes
|
|||
|
||||
const { organizationId } = userOrg;
|
||||
|
||||
// Get all accessible shortcuts (personal + workspace) sorted alphabetically
|
||||
// Get user's lastUsedShortcuts array
|
||||
const [userRecord] = await db
|
||||
.select({ lastUsedShortcuts: users.lastUsedShortcuts })
|
||||
.from(users)
|
||||
.where(eq(users.id, user.id))
|
||||
.limit(1);
|
||||
|
||||
const lastUsedShortcutIds = userRecord?.lastUsedShortcuts || [];
|
||||
|
||||
// Get all accessible shortcuts (personal + workspace)
|
||||
const shortcuts = await getUserShortcuts({
|
||||
userId: user.id,
|
||||
organizationId,
|
||||
});
|
||||
|
||||
// Sort shortcuts by last used order
|
||||
const sortedShortcuts = shortcuts.sort((a, b) => {
|
||||
const aIndex = lastUsedShortcutIds.indexOf(a.id);
|
||||
const bIndex = lastUsedShortcutIds.indexOf(b.id);
|
||||
|
||||
// If both are in lastUsed, sort by their position
|
||||
if (aIndex !== -1 && bIndex !== -1) {
|
||||
return aIndex - bIndex;
|
||||
}
|
||||
|
||||
// If only a is in lastUsed, it comes first
|
||||
if (aIndex !== -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// If only b is in lastUsed, it comes first
|
||||
if (bIndex !== -1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Neither is in lastUsed, sort alphabetically
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
return {
|
||||
shortcuts,
|
||||
shortcuts: sortedShortcuts,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error in listShortcutsHandler:', {
|
||||
|
|
|
@ -53,10 +53,11 @@ export async function updateShortcutHandler(
|
|||
// Only workspace_admin, data_admin, or the creator can update workspace shortcuts
|
||||
const isAdmin = userOrg.role === 'workspace_admin' || userOrg.role === 'data_admin';
|
||||
const isCreator = existingShortcut.createdBy === user.id;
|
||||
|
||||
|
||||
if (!isAdmin && !isCreator) {
|
||||
throw new HTTPException(403, {
|
||||
message: 'Only workspace admins, data admins, or the shortcut creator can update workspace shortcuts',
|
||||
message:
|
||||
'Only workspace admins, data admins, or the shortcut creator can update workspace shortcuts',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,11 +8,11 @@ import { z } from 'zod';
|
|||
import { requireAuth } from '../../../middleware/auth';
|
||||
import '../../../types/hono.types';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
import { createShortcutHandler } from './create-shortcut';
|
||||
import { deleteShortcutHandler } from './delete-shortcut';
|
||||
import { getShortcutHandler } from './get-shortcut';
|
||||
import { listShortcutsHandler } from './list-shortcuts';
|
||||
import { updateShortcutHandler } from './update-shortcut';
|
||||
import { listShortcutsHandler } from './GET';
|
||||
import { createShortcutHandler } from './POST';
|
||||
import { deleteShortcutHandler } from './[id]/DELETE';
|
||||
import { getShortcutHandler } from './[id]/GET';
|
||||
import { updateShortcutHandler } from './[id]/PUT';
|
||||
|
||||
// Schema for path params
|
||||
const shortcutIdParamSchema = z.object({
|
||||
|
|
|
@ -12,7 +12,9 @@ CREATE TABLE "shortcuts" (
|
|||
CONSTRAINT "shortcuts_personal_unique" UNIQUE("name","organization_id","created_by")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "users" ALTER COLUMN "suggested_prompts" SET DEFAULT '{"suggestedPrompts":{"report":["provide a trend analysis of quarterly profits","evaluate product performance across regions"],"dashboard":["create a sales performance dashboard","design a revenue forecast dashboard"],"visualization":["create a metric for monthly sales","show top vendors by purchase volume"],"help":["what types of analyses can you perform?","what questions can I as buster?","what data models are available for queries?","can you explain your forecasting capabilities?"]},"updatedAt":"2025-09-12T06:59:42.400Z"}'::jsonb;--> statement-breakpoint
|
||||
ALTER TABLE "users" ALTER COLUMN "suggested_prompts" SET DEFAULT '{"suggestedPrompts":{"report":["provide a trend analysis of quarterly profits","evaluate product performance across regions"],"dashboard":["create a sales performance dashboard","design a revenue forecast dashboard"],"visualization":["create a metric for monthly sales","show top vendors by purchase volume"],"help":["what types of analyses can you perform?","what questions can I as buster?","what data models are available for queries?","can you explain your forecasting capabilities?"]},"updatedAt":"2025-09-12T16:57:43.191Z"}'::jsonb;--> statement-breakpoint
|
||||
ALTER TABLE "messages" ADD COLUMN "metadata" jsonb DEFAULT '{}'::jsonb NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "users" ADD COLUMN "last_used_shortcuts" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "shortcuts" ADD CONSTRAINT "shortcuts_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "shortcuts" ADD CONSTRAINT "shortcuts_updated_by_fkey" FOREIGN KEY ("updated_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "shortcuts" ADD CONSTRAINT "shortcuts_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"id": "a2ebcfaa-f335-4dc4-8821-9d0bf865d27d",
|
||||
"id": "0613296e-fdd2-4a35-9d79-2275d0c7a78e",
|
||||
"prevId": "f71f7e1c-314d-413d-8da9-a525bd4a3519",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
|
@ -3020,6 +3020,13 @@
|
|||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"metadata": {
|
||||
"name": "metadata",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'{}'::jsonb"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
|
@ -6907,7 +6914,7 @@
|
|||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'{\"suggestedPrompts\":{\"report\":[\"provide a trend analysis of quarterly profits\",\"evaluate product performance across regions\"],\"dashboard\":[\"create a sales performance dashboard\",\"design a revenue forecast dashboard\"],\"visualization\":[\"create a metric for monthly sales\",\"show top vendors by purchase volume\"],\"help\":[\"what types of analyses can you perform?\",\"what questions can I as buster?\",\"what data models are available for queries?\",\"can you explain your forecasting capabilities?\"]},\"updatedAt\":\"2025-09-12T06:59:42.400Z\"}'::jsonb"
|
||||
"default": "'{\"suggestedPrompts\":{\"report\":[\"provide a trend analysis of quarterly profits\",\"evaluate product performance across regions\"],\"dashboard\":[\"create a sales performance dashboard\",\"design a revenue forecast dashboard\"],\"visualization\":[\"create a metric for monthly sales\",\"show top vendors by purchase volume\"],\"help\":[\"what types of analyses can you perform?\",\"what questions can I as buster?\",\"what data models are available for queries?\",\"can you explain your forecasting capabilities?\"]},\"updatedAt\":\"2025-09-12T16:57:43.191Z\"}'::jsonb"
|
||||
},
|
||||
"personalization_enabled": {
|
||||
"name": "personalization_enabled",
|
||||
|
@ -6922,6 +6929,13 @@
|
|||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'{}'::jsonb"
|
||||
},
|
||||
"last_used_shortcuts": {
|
||||
"name": "last_used_shortcuts",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'[]'::jsonb"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
|
|
|
@ -677,8 +677,8 @@
|
|||
{
|
||||
"idx": 96,
|
||||
"version": "7",
|
||||
"when": 1757660382434,
|
||||
"tag": "0096_secret_next_avengers",
|
||||
"when": 1757696263222,
|
||||
"tag": "0096_outgoing_shotgun",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
|
|
@ -35,6 +35,7 @@ export const CreateMessageInputSchema = z.object({
|
|||
userId: z.string().uuid(),
|
||||
messageId: z.string().uuid().optional(),
|
||||
messageAnalysisMode: z.enum(messageAnalysisModeEnum.enumValues).optional(),
|
||||
metadata: z.record(z.any()).optional(),
|
||||
});
|
||||
|
||||
export type CreateChatInput = z.infer<typeof CreateChatInputSchema>;
|
||||
|
@ -146,6 +147,7 @@ export async function createMessage(input: CreateMessageInput): Promise<Message>
|
|||
content: validated.content,
|
||||
},
|
||||
],
|
||||
metadata: validated.metadata || {},
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
|
|
@ -5,3 +5,4 @@ export * from './get-user-organizations';
|
|||
export * from './user-queries';
|
||||
export * from './user-suggested-prompts';
|
||||
export * from './get-user-personalization';
|
||||
export * from './update-user-shortcuts';
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import { eq } from 'drizzle-orm';
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../connection';
|
||||
import { users } from '../../schema';
|
||||
|
||||
export const UpdateUserLastUsedShortcutsInputSchema = z.object({
|
||||
userId: z.string().uuid(),
|
||||
shortcutIds: z.array(z.string().uuid()),
|
||||
});
|
||||
|
||||
export type UpdateUserLastUsedShortcutsInput = z.infer<
|
||||
typeof UpdateUserLastUsedShortcutsInputSchema
|
||||
>;
|
||||
|
||||
/**
|
||||
* Updates the user's lastUsedShortcuts array with new shortcut IDs.
|
||||
* New shortcuts are prepended to the front of the array.
|
||||
* Duplicates are removed while maintaining the most recent usage order.
|
||||
*
|
||||
* @param input - Object containing userId and shortcutIds to add
|
||||
* @returns Success status
|
||||
*/
|
||||
export async function updateUserLastUsedShortcuts(
|
||||
input: UpdateUserLastUsedShortcutsInput
|
||||
): Promise<{ success: boolean }> {
|
||||
const validated = UpdateUserLastUsedShortcutsInputSchema.parse(input);
|
||||
|
||||
try {
|
||||
// First, get the current lastUsedShortcuts array
|
||||
const [currentUser] = await db
|
||||
.select({ lastUsedShortcuts: users.lastUsedShortcuts })
|
||||
.from(users)
|
||||
.where(eq(users.id, validated.userId))
|
||||
.limit(1);
|
||||
|
||||
if (!currentUser) {
|
||||
throw new Error(`User not found: ${validated.userId}`);
|
||||
}
|
||||
|
||||
const currentShortcuts = currentUser.lastUsedShortcuts || [];
|
||||
|
||||
// Remove any of the new shortcuts from their current positions
|
||||
const filteredShortcuts = currentShortcuts.filter((id) => !validated.shortcutIds.includes(id));
|
||||
|
||||
// Prepend the new shortcuts to the front
|
||||
const updatedShortcuts = [...validated.shortcutIds, ...filteredShortcuts];
|
||||
|
||||
// Update the user record
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
lastUsedShortcuts: updatedShortcuts,
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
.where(eq(users.id, validated.userId));
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to update user last used shortcuts:', error);
|
||||
return { success: false };
|
||||
}
|
||||
}
|
|
@ -16,8 +16,13 @@ export const UserPersonalizationConfigSchema = z.object({
|
|||
additionalInformation: z.string().optional(),
|
||||
});
|
||||
|
||||
export const UserShortcutTrackingSchema = z.object({
|
||||
lastUsedShortcuts: z.array(z.string().uuid()).default([]),
|
||||
});
|
||||
|
||||
export type UserSuggestedPromptsType = z.infer<typeof UserSuggestedPromptsSchema>;
|
||||
export type UserPersonalizationConfigType = z.infer<typeof UserPersonalizationConfigSchema>;
|
||||
export type UserShortcutTrackingType = z.infer<typeof UserShortcutTrackingSchema>;
|
||||
|
||||
export const DEFAULT_USER_SUGGESTED_PROMPTS: UserSuggestedPromptsType = {
|
||||
suggestedPrompts: {
|
||||
|
|
|
@ -22,9 +22,11 @@ import {
|
|||
import type {
|
||||
OrganizationColorPalettes,
|
||||
UserPersonalizationConfigType,
|
||||
UserShortcutTrackingType,
|
||||
UserSuggestedPromptsType,
|
||||
} from './schema-types';
|
||||
import { DEFAULT_USER_SUGGESTED_PROMPTS } from './schema-types/user';
|
||||
import type { MessageMetadata } from './schemas/message-schemas';
|
||||
|
||||
export const assetPermissionRoleEnum = pgEnum('asset_permission_role_enum', [
|
||||
'owner',
|
||||
|
@ -879,6 +881,7 @@ export const users = pgTable(
|
|||
.$type<UserPersonalizationConfigType>()
|
||||
.default({})
|
||||
.notNull(),
|
||||
lastUsedShortcuts: jsonb('last_used_shortcuts').$type<string[]>().default([]).notNull(),
|
||||
},
|
||||
(table) => [unique('users_email_key').on(table.email)]
|
||||
);
|
||||
|
@ -907,6 +910,7 @@ export const messages = pgTable(
|
|||
isCompleted: boolean('is_completed').default(false).notNull(),
|
||||
postProcessingMessage: jsonb('post_processing_message'),
|
||||
triggerRunId: text('trigger_run_id'),
|
||||
metadata: jsonb().$type<MessageMetadata>().default({}).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('messages_chat_id_idx').using('btree', table.chatId.asc().nullsLast().op('uuid_ops')),
|
||||
|
|
|
@ -139,3 +139,10 @@ export type ChatMessageResponseMessage_File = z.infer<typeof ResponseMessage_Fil
|
|||
export type ReasoningFileType = z.infer<typeof ReasoningFileTypeSchema>;
|
||||
export type ResponseMessageFileType = z.infer<typeof ResponseMessageFileTypeSchema>;
|
||||
export type ReasoingMessage_ThoughtFileType = z.infer<typeof ReasoingMessage_ThoughtFileTypeSchema>;
|
||||
|
||||
// Message metadata schema
|
||||
export const MessageMetadataSchema = z.object({
|
||||
shortcutIds: z.array(z.string().uuid()).nullable().optional(),
|
||||
});
|
||||
|
||||
export type MessageMetadata = z.infer<typeof MessageMetadataSchema>;
|
||||
|
|
|
@ -4,6 +4,13 @@ import type { AssetPermissionRoleSchema } from '../share';
|
|||
import { ShareConfigSchema } from '../share';
|
||||
import { ChatMessageSchema } from './chat-message.types';
|
||||
|
||||
// Message metadata schema (mirrored from database package)
|
||||
export const MessageMetadataSchema = z.object({
|
||||
shortcutIds: z.array(z.string().uuid()).nullable().optional(),
|
||||
});
|
||||
|
||||
export type MessageMetadata = z.infer<typeof MessageMetadataSchema>;
|
||||
|
||||
// Asset Permission Role enum (matching database enum)
|
||||
export const ChatAssetTypeSchema = AssetTypeSchema.exclude(['chat', 'collection']);
|
||||
|
||||
|
@ -34,6 +41,7 @@ export const ChatCreateRequestSchema = z
|
|||
message_analysis_mode: MessageAnalysisModeSchema.optional(),
|
||||
asset_id: z.string().optional(),
|
||||
asset_type: ChatAssetTypeSchema.optional(),
|
||||
metadata: MessageMetadataSchema.optional(),
|
||||
// Legacy fields for backward compatibility
|
||||
metric_id: z.string().optional(),
|
||||
dashboard_id: z.string().optional(),
|
||||
|
@ -51,6 +59,7 @@ export const ChatCreateHandlerRequestSchema = z.object({
|
|||
message_id: z.string().optional(),
|
||||
asset_id: z.string().optional(),
|
||||
asset_type: ChatAssetTypeSchema.optional(),
|
||||
metadata: MessageMetadataSchema.optional(),
|
||||
});
|
||||
|
||||
// Cancel chat params schema
|
||||
|
|
Loading…
Reference in New Issue