Skip to content

Commit 79f9cd5

Browse files
💬 feat: assistant conversation starter (danny-avila#3699)
* feat: initial UI convoStart * fix: ConvoStarter UI * fix: convoStarters bug * feat: Add input field focus on conversation starters * style: conversation starter UI update * feat: apply fixes for starters * style: update conversationStarters UI and fixed typo * general UI update * feat: Add onClick functionality to ConvoStarter component * fix: quick fix test * fix(AssistantSelect): remove object check * fix: updateAssistant `conversation_starters` var * chore: remove starter autofocus * fix: no empty conversation starters, always show input, use Constants value for max count * style: Update defaultTextPropsLabel styles, for a11y placeholder * refactor: Update ConvoStarter component styles and class names for a11y and theme * refactor: convostarter, move plus button to within persistent element * fix: types * chore: Update landing page assistant description styling with theming * chore: assistant types * refactor: documents routes * refactor: optimize conversation starter mutations/queries * refactor: Update listAllAssistants return type to Promise<Array<Assistant>> * feat: edit existing starters * feat(convo-starters): enhance ConvoStarter component and add animations - Update ConvoStarter component styling for better visual appeal - Implement fade-in animation for smoother appearance - Add hover effect with background color change - Improve text overflow handling with line-clamp and text-balance - Ensure responsive design for various screen sizes * feat(assistant): add conversation starters to assistant builder - Add localization strings for conversation starters - Update mobile.css with shake animation for max starters reached - Enhance user experience with tooltips and dynamic input handling * refactor: select specific fields for assistant documents fetch * refactor: remove endpoint query key, fetch all assistant docs for now, add conversation_starters to v1 methods * refactor: add document filters based on endpoint config * fix: starters not applied during creation * refactor: update AssistantSelect component to handle undefined lastSelectedModels --------- Co-authored-by: Danny Avila <[email protected]>
1 parent 63b80c3 commit 79f9cd5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+601
-213
lines changed

api/models/Assistant.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const Assistant = mongoose.model('assistant', assistantSchema);
1212
* @param {string} searchParams.user - The user ID of the assistant's author.
1313
* @param {Object} updateData - An object containing the properties to update.
1414
* @param {mongoose.ClientSession} [session] - The transaction session to use (optional).
15-
* @returns {Promise<Object>} The updated or newly created assistant document as a plain object.
15+
* @returns {Promise<AssistantDocument>} The updated or newly created assistant document as a plain object.
1616
*/
1717
const updateAssistantDoc = async (searchParams, updateData, session = null) => {
1818
const options = { new: true, upsert: true, session };
@@ -25,18 +25,25 @@ const updateAssistantDoc = async (searchParams, updateData, session = null) => {
2525
* @param {Object} searchParams - The search parameters to find the assistant to update.
2626
* @param {string} searchParams.assistant_id - The ID of the assistant to update.
2727
* @param {string} searchParams.user - The user ID of the assistant's author.
28-
* @returns {Promise<Object|null>} The assistant document as a plain object, or null if not found.
28+
* @returns {Promise<AssistantDocument|null>} The assistant document as a plain object, or null if not found.
2929
*/
3030
const getAssistant = async (searchParams) => await Assistant.findOne(searchParams).lean();
3131

3232
/**
3333
* Retrieves all assistants that match the given search parameters.
3434
*
3535
* @param {Object} searchParams - The search parameters to find matching assistants.
36-
* @returns {Promise<Array<Object>>} A promise that resolves to an array of action documents as plain objects.
36+
* @param {Object} [select] - Optional. Specifies which document fields to include or exclude.
37+
* @returns {Promise<Array<AssistantDocument>>} A promise that resolves to an array of assistant documents as plain objects.
3738
*/
38-
const getAssistants = async (searchParams) => {
39-
return await Assistant.find(searchParams).lean();
39+
const getAssistants = async (searchParams, select = null) => {
40+
let query = Assistant.find(searchParams);
41+
42+
if (select) {
43+
query = query.select(select);
44+
}
45+
46+
return await query.lean();
4047
};
4148

4249
/**

api/models/schema/assistant.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ const assistantSchema = mongoose.Schema(
1919
},
2020
default: undefined,
2121
},
22+
conversation_starters: {
23+
type: [String],
24+
default: [],
25+
},
2226
access_level: {
2327
type: Number,
2428
},

api/server/controllers/assistants/helpers.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ const _listAssistants = async ({ req, res, version, query }) => {
6464
* @param {object} params.res - The response object, used for initializing the client.
6565
* @param {string} params.version - The API version to use.
6666
* @param {Omit<AssistantListParams, 'endpoint'>} params.query - The query parameters to list assistants (e.g., limit, order).
67-
* @returns {Promise<object>} A promise that resolves to the response from the `openai.beta.assistants.list` method call.
67+
* @returns {Promise<Array<Assistant>>} A promise that resolves to the response from the `openai.beta.assistants.list` method call.
6868
*/
6969
const listAllAssistants = async ({ req, res, version, query }) => {
7070
/** @type {{ openai: OpenAIClient }} */

api/server/controllers/assistants/v1.js

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ const createAssistant = async (req, res) => {
1818
try {
1919
const { openai } = await getOpenAIClient({ req, res });
2020

21-
const { tools = [], endpoint, ...assistantData } = req.body;
21+
const { tools = [], endpoint, conversation_starters, ...assistantData } = req.body;
22+
delete assistantData.conversation_starters;
23+
2224
assistantData.tools = tools
2325
.map((tool) => {
2426
if (typeof tool !== 'string') {
@@ -41,11 +43,22 @@ const createAssistant = async (req, res) => {
4143
};
4244

4345
const assistant = await openai.beta.assistants.create(assistantData);
44-
const promise = updateAssistantDoc({ assistant_id: assistant.id }, { user: req.user.id });
46+
47+
const createData = { user: req.user.id };
48+
if (conversation_starters) {
49+
createData.conversation_starters = conversation_starters;
50+
}
51+
52+
const document = await updateAssistantDoc({ assistant_id: assistant.id }, createData);
53+
4554
if (azureModelIdentifier) {
4655
assistant.model = azureModelIdentifier;
4756
}
48-
await promise;
57+
58+
if (document.conversation_starters) {
59+
assistant.conversation_starters = document.conversation_starters;
60+
}
61+
4962
logger.debug('/assistants/', assistant);
5063
res.status(201).json(assistant);
5164
} catch (error) {
@@ -88,7 +101,7 @@ const patchAssistant = async (req, res) => {
88101
await validateAuthor({ req, openai });
89102

90103
const assistant_id = req.params.id;
91-
const { endpoint: _e, ...updateData } = req.body;
104+
const { endpoint: _e, conversation_starters, ...updateData } = req.body;
92105
updateData.tools = (updateData.tools ?? [])
93106
.map((tool) => {
94107
if (typeof tool !== 'string') {
@@ -104,6 +117,15 @@ const patchAssistant = async (req, res) => {
104117
}
105118

106119
const updatedAssistant = await openai.beta.assistants.update(assistant_id, updateData);
120+
121+
if (conversation_starters !== undefined) {
122+
const conversationStartersUpdate = await updateAssistantDoc(
123+
{ assistant_id },
124+
{ conversation_starters },
125+
);
126+
updatedAssistant.conversation_starters = conversationStartersUpdate.conversation_starters;
127+
}
128+
107129
res.json(updatedAssistant);
108130
} catch (error) {
109131
logger.error('[/assistants/:id] Error updating assistant', error);
@@ -153,14 +175,58 @@ const listAssistants = async (req, res) => {
153175
}
154176
};
155177

178+
/**
179+
* Filter assistants based on configuration.
180+
*
181+
* @param {object} params - The parameters object.
182+
* @param {string} params.userId - The user ID to filter private assistants.
183+
* @param {AssistantDocument[]} params.assistants - The list of assistants to filter.
184+
* @param {Partial<TAssistantEndpoint>} [params.assistantsConfig] - The assistant configuration.
185+
* @returns {AssistantDocument[]} - The filtered list of assistants.
186+
*/
187+
function filterAssistantDocs({ documents, userId, assistantsConfig = {} }) {
188+
const { supportedIds, excludedIds, privateAssistants } = assistantsConfig;
189+
const removeUserId = (doc) => {
190+
const { user: _u, ...document } = doc;
191+
return document;
192+
};
193+
194+
if (privateAssistants) {
195+
return documents.filter((doc) => userId === doc.user.toString()).map(removeUserId);
196+
} else if (supportedIds?.length) {
197+
return documents.filter((doc) => supportedIds.includes(doc.assistant_id)).map(removeUserId);
198+
} else if (excludedIds?.length) {
199+
return documents.filter((doc) => !excludedIds.includes(doc.assistant_id)).map(removeUserId);
200+
}
201+
return documents.map(removeUserId);
202+
}
203+
156204
/**
157205
* Returns a list of the user's assistant documents (metadata saved to database).
158206
* @route GET /assistants/documents
159207
* @returns {AssistantDocument[]} 200 - success response - application/json
160208
*/
161209
const getAssistantDocuments = async (req, res) => {
162210
try {
163-
res.json(await getAssistants({ user: req.user.id }));
211+
const endpoint = req.query;
212+
const assistantsConfig = req.app.locals[endpoint];
213+
const documents = await getAssistants(
214+
{},
215+
{
216+
user: 1,
217+
assistant_id: 1,
218+
conversation_starters: 1,
219+
createdAt: 1,
220+
updatedAt: 1,
221+
},
222+
);
223+
224+
const docs = filterAssistantDocs({
225+
documents,
226+
userId: req.user.id,
227+
assistantsConfig,
228+
});
229+
res.json(docs);
164230
} catch (error) {
165231
logger.error('[/assistants/documents] Error listing assistant documents', error);
166232
res.status(500).json({ error: error.message });

api/server/controllers/assistants/v2.js

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ const createAssistant = async (req, res) => {
1616
/** @type {{ openai: OpenAIClient }} */
1717
const { openai } = await getOpenAIClient({ req, res });
1818

19-
const { tools = [], endpoint, ...assistantData } = req.body;
19+
const { tools = [], endpoint, conversation_starters, ...assistantData } = req.body;
20+
delete assistantData.conversation_starters;
21+
2022
assistantData.tools = tools
2123
.map((tool) => {
2224
if (typeof tool !== 'string') {
@@ -39,11 +41,22 @@ const createAssistant = async (req, res) => {
3941
};
4042

4143
const assistant = await openai.beta.assistants.create(assistantData);
42-
const promise = updateAssistantDoc({ assistant_id: assistant.id }, { user: req.user.id });
44+
45+
const createData = { user: req.user.id };
46+
if (conversation_starters) {
47+
createData.conversation_starters = conversation_starters;
48+
}
49+
50+
const document = await updateAssistantDoc({ assistant_id: assistant.id }, createData);
51+
4352
if (azureModelIdentifier) {
4453
assistant.model = azureModelIdentifier;
4554
}
46-
await promise;
55+
56+
if (document.conversation_starters) {
57+
assistant.conversation_starters = document.conversation_starters;
58+
}
59+
4760
logger.debug('/assistants/', assistant);
4861
res.status(201).json(assistant);
4962
} catch (error) {
@@ -64,6 +77,17 @@ const createAssistant = async (req, res) => {
6477
const updateAssistant = async ({ req, openai, assistant_id, updateData }) => {
6578
await validateAuthor({ req, openai });
6679
const tools = [];
80+
let conversation_starters = null;
81+
82+
if (updateData?.conversation_starters) {
83+
const conversationStartersUpdate = await updateAssistantDoc(
84+
{ assistant_id: assistant_id },
85+
{ conversation_starters: updateData.conversation_starters },
86+
);
87+
conversation_starters = conversationStartersUpdate.conversation_starters;
88+
89+
delete updateData.conversation_starters;
90+
}
6791

6892
let hasFileSearch = false;
6993
for (const tool of updateData.tools ?? []) {
@@ -108,7 +132,13 @@ const updateAssistant = async ({ req, openai, assistant_id, updateData }) => {
108132
updateData.model = openai.locals.azureOptions.azureOpenAIApiDeploymentName;
109133
}
110134

111-
return await openai.beta.assistants.update(assistant_id, updateData);
135+
const assistant = await openai.beta.assistants.update(assistant_id, updateData);
136+
137+
if (conversation_starters) {
138+
assistant.conversation_starters = conversation_starters;
139+
}
140+
141+
return assistant;
112142
};
113143

114144
/**
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const express = require('express');
2+
const controllers = require('~/server/controllers/assistants/v1');
3+
4+
const router = express.Router();
5+
6+
/**
7+
* Returns a list of the user's assistant documents (metadata saved to database).
8+
* @route GET /assistants/documents
9+
* @returns {AssistantDocument[]} 200 - success response - application/json
10+
*/
11+
router.get('/', controllers.getAssistantDocuments);
12+
13+
module.exports = router;

api/server/routes/assistants/v1.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const multer = require('multer');
22
const express = require('express');
33
const controllers = require('~/server/controllers/assistants/v1');
4+
const documents = require('./documents');
45
const actions = require('./actions');
56
const tools = require('./tools');
67

@@ -20,6 +21,13 @@ router.use('/actions', actions);
2021
*/
2122
router.use('/tools', tools);
2223

24+
/**
25+
* Create an assistant.
26+
* @route GET /assistants/documents
27+
* @returns {AssistantDocument[]} 200 - application/json
28+
*/
29+
router.use('/documents', documents);
30+
2331
/**
2432
* Create an assistant.
2533
* @route POST /assistants
@@ -61,13 +69,6 @@ router.delete('/:id', controllers.deleteAssistant);
6169
*/
6270
router.get('/', controllers.listAssistants);
6371

64-
/**
65-
* Returns a list of the user's assistant documents (metadata saved to database).
66-
* @route GET /assistants/documents
67-
* @returns {AssistantDocument[]} 200 - success response - application/json
68-
*/
69-
router.get('/documents', controllers.getAssistantDocuments);
70-
7172
/**
7273
* Uploads and updates an avatar for a specific assistant.
7374
* @route POST /avatar/:assistant_id

api/server/routes/assistants/v2.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const multer = require('multer');
22
const express = require('express');
33
const v1 = require('~/server/controllers/assistants/v1');
44
const v2 = require('~/server/controllers/assistants/v2');
5+
const documents = require('./documents');
56
const actions = require('./actions');
67
const tools = require('./tools');
78

@@ -21,6 +22,13 @@ router.use('/actions', actions);
2122
*/
2223
router.use('/tools', tools);
2324

25+
/**
26+
* Create an assistant.
27+
* @route GET /assistants/documents
28+
* @returns {AssistantDocument[]} 200 - application/json
29+
*/
30+
router.use('/documents', documents);
31+
2432
/**
2533
* Create an assistant.
2634
* @route POST /assistants
@@ -62,13 +70,6 @@ router.delete('/:id', v1.deleteAssistant);
6270
*/
6371
router.get('/', v1.listAssistants);
6472

65-
/**
66-
* Returns a list of the user's assistant documents (metadata saved to database).
67-
* @route GET /assistants/documents
68-
* @returns {AssistantDocument[]} 200 - success response - application/json
69-
*/
70-
router.get('/documents', v1.getAssistantDocuments);
71-
7273
/**
7374
* Uploads and updates an avatar for a specific assistant.
7475
* @route POST /avatar/:assistant_id

client/src/common/assistants-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export type AssistantForm = {
2222
name: string | null;
2323
description: string | null;
2424
instructions: string | null;
25+
conversation_starters: string[];
2526
model: string;
2627
functions: string[];
2728
} & Actions;

client/src/common/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {
1818
TConversation,
1919
TStartupConfig,
2020
EModelEndpoint,
21+
ActionMetadata,
2122
AssistantsEndpoint,
2223
TMessageContentParts,
2324
AuthorizationTypeEnum,
@@ -146,9 +147,13 @@ export type ActionAuthForm = {
146147
token_exchange_method: TokenExchangeMethodEnum;
147148
};
148149

150+
export type ActionWithNullableMetadata = Omit<Action, 'metadata'> & {
151+
metadata: ActionMetadata | null;
152+
};
153+
149154
export type AssistantPanelProps = {
150155
index?: number;
151-
action?: Action;
156+
action?: ActionWithNullableMetadata;
152157
actions?: Action[];
153158
assistant_id?: string;
154159
activePanel?: string;

0 commit comments

Comments
 (0)