@@ -12,6 +12,7 @@ import config from '../../config/config.js'
1212import adapterService from '../adapter.service.js'
1313import channelService from '../channel.service.js'
1414import { ConversationDocument } from '../../models/conversation.model.js'
15+ import { TopicDocument } from '../../models/topic.model.js'
1516import { getConversationType } from '../../conversations/index.js'
1617import adapterTypes from '../../adapters/index.js'
1718import resolveConversationType from '../../conversations/resolver.js'
@@ -60,7 +61,7 @@ const startConversation = async (conversationOrId, user) => {
6061 await conversation . populate ( [ 'topic' , 'agents' , 'adapters' ] )
6162 if (
6263 user . _id . toString ( ) !== conversation . owner . _id . toString ( ) &&
63- user . _id . toString ( ) !== conversation . topic . owner . _id . toString ( )
64+ user . _id . toString ( ) !== conversation . topic ? .owner ? ._id . toString ( )
6465 ) {
6566 throw new ApiError ( httpStatus . FORBIDDEN , 'Only conversation or topic owner can start conversation' )
6667 }
@@ -76,7 +77,10 @@ const stopConversation = async (conversationOrId, user) => {
7677 }
7778 }
7879 await conversation . populate ( [ 'topic' , 'agents' , 'adapters' ] )
79- if ( user . _id . toString ( ) !== conversation . owner . toString ( ) && user . _id . toString ( ) !== conversation . topic . owner . toString ( ) ) {
80+ if (
81+ user . _id . toString ( ) !== conversation . owner . toString ( ) &&
82+ user . _id . toString ( ) !== conversation . topic ?. owner ?. toString ( )
83+ ) {
8084 throw new ApiError ( httpStatus . FORBIDDEN , 'Only conversation or topic owner can stop conversation' )
8185 }
8286 return doStopConversation ( conversation )
@@ -141,17 +145,25 @@ async function scheduleConversationEndingSoon(conversation) {
141145/**
142146 * Create a conversation
143147 * @param {Object } conversationBody
148+ * @param {Object } user
149+ * @param {Object } [options]
150+ * @param {boolean } [options.allowDraft] trusted internal callers only; lets a conversation be created
151+ * with no topic, saved as a draft for the owner to complete (see createConversationFromType)
144152 * @returns {Promise<Conversation> }
145153 */
146- const createConversation = async ( conversationBody , user ) => {
147- if ( ! conversationBody . topicId ) throw new ApiError ( httpStatus . BAD_REQUEST , 'topic id must be passed in request body' )
148- const topicId = new mongoose . Types . ObjectId ( conversationBody . topicId )
149- const topic = await Topic . findById ( topicId )
150- if ( ! topic ) {
151- throw new ApiError ( httpStatus . BAD_REQUEST , 'No such topic' )
152- }
153- if ( ! topic ?. conversationCreationAllowed && user . _id . toString ( ) !== topic ?. owner . toString ( ) ) {
154- throw new ApiError ( httpStatus . FORBIDDEN , 'Conversation creation not allowed.' )
154+ const createConversation = async ( conversationBody , user , { allowDraft = false } = { } ) => {
155+ let topic : TopicDocument | null = null
156+ if ( conversationBody . topicId ) {
157+ const topicId = new mongoose . Types . ObjectId ( conversationBody . topicId )
158+ topic = await Topic . findById ( topicId )
159+ if ( ! topic ) {
160+ throw new ApiError ( httpStatus . BAD_REQUEST , 'No such topic' )
161+ }
162+ if ( ! topic . conversationCreationAllowed && user . _id . toString ( ) !== topic . owner . toString ( ) ) {
163+ throw new ApiError ( httpStatus . FORBIDDEN , 'Conversation creation not allowed.' )
164+ }
165+ } else if ( ! allowDraft ) {
166+ throw new ApiError ( httpStatus . BAD_REQUEST , 'topic id must be passed in request body' )
155167 }
156168
157169 if ( conversationBody . scheduledTime && new Date ( conversationBody . scheduledTime ) <= new Date ( ) ) {
@@ -167,7 +179,7 @@ const createConversation = async (conversationBody, user) => {
167179 const conversation = new Conversation ( {
168180 name : conversationBody . name ,
169181 owner : user ,
170- topic,
182+ ... ( topic && { topic } ) ,
171183 enableAgents : ! ! conversationBody . agentTypes ?. length ,
172184 ...( conversationBody . enableDMs !== undefined && { enableDMs : conversationBody . enableDMs } ) ,
173185 ...( conversationBody . conversationType !== undefined && { conversationType : conversationBody . conversationType } ) ,
@@ -212,8 +224,13 @@ const createConversation = async (conversationBody, user) => {
212224 await channelService . createChannel ( conversation , channelProps )
213225 }
214226
215- topic . conversations . push ( conversation . toObject ( ) )
216- await Promise . all ( [ conversation . save ( ) , topic . save ( ) ] )
227+ // A topicless draft has nothing to link back to, so only touch the topic when one resolved.
228+ if ( topic ) {
229+ topic . conversations . push ( conversation . toObject ( ) )
230+ await Promise . all ( [ conversation . save ( ) , topic . save ( ) ] )
231+ } else {
232+ await conversation . save ( )
233+ }
217234 await transcript . loadEventMetadataIntoVectorStore ( conversation )
218235
219236 websocketGateway . broadcastNewConversation ( conversation )
@@ -253,7 +270,7 @@ const createConversationFromType = async (params, user, { allowDraft = false } =
253270 }
254271
255272 const resolved = resolveConversationType ( params , conversationType , allowDraft )
256- return createConversation ( { ...params , conversationType : type , ...resolved } , user )
273+ return createConversation ( { ...params , conversationType : type , ...resolved } , user , { allowDraft } )
257274}
258275
259276/**
@@ -269,7 +286,7 @@ const updateConversation = async (conversationBody, user) => {
269286 }
270287 if (
271288 user . _id . toString ( ) !== conversationDoc . owner . toString ( ) &&
272- user . _id . toString ( ) !== conversationDoc . topic . owner . toString ( )
289+ user . _id . toString ( ) !== conversationDoc . topic ? .owner ? .toString ( )
273290 ) {
274291 throw new ApiError ( httpStatus . FORBIDDEN , 'Only conversation or topic owner can update.' )
275292 }
@@ -702,7 +719,10 @@ const deleteConversation = async (id, user) => {
702719 if ( ! conversation ) {
703720 throw new ApiError ( httpStatus . NOT_FOUND , `Conversation with id ${ id } not found` )
704721 }
705- if ( user . _id . toString ( ) !== conversation . owner . toString ( ) && user . _id . toString ( ) !== conversation . topic . owner . toString ( ) ) {
722+ if (
723+ user . _id . toString ( ) !== conversation . owner . toString ( ) &&
724+ user . _id . toString ( ) !== conversation . topic ?. owner ?. toString ( )
725+ ) {
706726 throw new ApiError ( httpStatus . FORBIDDEN , 'Only conversation or topic owner can delete.' )
707727 }
708728 if ( conversation . active ) {
@@ -737,7 +757,10 @@ const patchConversationAgent = async (id, agentId, body, user) => {
737757 throw new ApiError ( httpStatus . NOT_FOUND , `Conversation with id ${ id } not found` )
738758 }
739759 const agentIdStr = agentId . toString ( ) ? agentId . toString ( ) : agentId
740- if ( user . _id . toString ( ) !== conversation . owner . toString ( ) && user . _id . toString ( ) !== conversation . topic . owner . toString ( ) ) {
760+ if (
761+ user . _id . toString ( ) !== conversation . owner . toString ( ) &&
762+ user . _id . toString ( ) !== conversation . topic ?. owner ?. toString ( )
763+ ) {
741764 throw new ApiError ( httpStatus . FORBIDDEN , 'Only conversation or topic owner can patch agents' )
742765 }
743766 const agent = conversation . agents . find ( ( a ) => a . _id ! . toString ( ) === agentIdStr )
0 commit comments