import type { ComputedRef, Ref, ShallowRef } from 'vue'; import { callAdminForthApi } from '@/utils'; import type { Chat } from '../../chat'; import type { IAgentSession, ISessionsListItem, IPart } from '../../types'; import { PRE_SESSION_ID, STEER_PERSIST_PREFIX } from './constants'; import { i18nInstance } from '@/i18n'; type AdminforthLike = { confirm(options: { title?: string; message?: string; yes?: string; no?: string; dangerous?: boolean }): Promise; }; type CreateAgentSessionManagerOptions = { activeSessionId: Ref; currentSession: Ref; sessionList: Ref; sessions: Ref>; currentChat: ShallowRef | null | undefined>; trimmedUserMessage: ComputedRef; isResponseInProgress: ComputedRef; isMessageInputBlocked: ComputedRef; userMessageInput: Ref; lastMessage: Ref; blockCloseOfChat: Ref; adminforth: AdminforthLike; setCurrentChat: (sessionId: string) => void; }; export function createAgentSessionManager({ activeSessionId, currentSession, sessionList, sessions, currentChat, trimmedUserMessage, isResponseInProgress, isMessageInputBlocked, userMessageInput, lastMessage, blockCloseOfChat, adminforth, setCurrentChat, }: CreateAgentSessionManagerOptions) { function sortSessionsListByTimestamp(sessionsListToSort: ISessionsListItem[]) { return [...sessionsListToSort].sort((a: ISessionsListItem, b: ISessionsListItem) => b.timestamp.localeCompare(a.timestamp)); } function t(key: string) { return i18nInstance?.global.t(key) ?? key; } function saveCurrentSessionInCache() { if (currentSession.value) { currentSession.value.messages = currentChat.value?.messages.map((m: any) => { const text = m.parts.map((p: IPart) => p.type === 'text' ? p.text : '').join(''); const turnId = m.metadata?.turnId; if (m.role === 'user' && m.metadata?.steer) { return { role: 'user', text: `${STEER_PERSIST_PREFIX}${text}`, turnId }; } return { role: m.role, text, turnId }; }) || []; sessions.value[currentSession.value.sessionId] = currentSession.value; } } function mapStoredMessage(m: any): any[] { const turnId = m.turnId; if (m.role === 'user' && typeof m.text === 'string' && m.text.includes(STEER_PERSIST_PREFIX)) { const [prompt, ...steers] = m.text.split(STEER_PERSIST_PREFIX); const mapped: any[] = []; if (prompt) { mapped.push({ id: crypto.randomUUID(), role: 'user', metadata: { turnId }, parts: [{ type: 'text', text: prompt, state: 'done' }] }); } for (const steer of steers) { mapped.push({ id: crypto.randomUUID(), role: 'user', metadata: { turnId, steer: true }, parts: [{ type: 'text', text: steer, state: 'done' }], }); } return mapped; } return [{ id: crypto.randomUUID(), role: m.role, metadata: { turnId }, parts: [{ type: 'text', text: m.text, state: 'done' }] }]; } async function fetchSession(sessionId: string) { try { const res = await callAdminForthApi({ method: 'POST', path: '/agent/get-session-info', body: { sessionId }, silentError: true, }); if (res.error) { return; } sessions.value[sessionId] = res.session; setCurrentChat(sessionId); } catch (error) { console.error('Error fetching session', error); } } async function setActiveSession(sessionId: string) { activeSessionId.value = sessionId; saveCurrentSessionInCache(); if (!sessions.value[sessionId]) { await fetchSession(sessionId); } currentSession.value = sessions.value[sessionId]; setCurrentChat(sessionId); if (currentChat.value && currentChat.value.messages.length === 0) { // The session fetch can bail out (deleted/expired session), leaving no stored // messages — keep `messages` an array so consumers can iterate it safely. currentChat.value.messages = (currentSession.value?.messages ?? []).flatMap(mapStoredMessage); } } async function deletePreSession() { sessionList.value = sessionList.value.filter((s: ISessionsListItem) => s.sessionId !== PRE_SESSION_ID); if (activeSessionId.value === PRE_SESSION_ID) { activeSessionId.value = null; currentSession.value = null; } } async function createNewSession(triggerMessage?: string) { try { const res = await callAdminForthApi({ method: 'POST', path: '/agent/create-session', body: { triggerMessage }, }); if (res.error) { console.error('Error creating new session:', res.error); return; } deletePreSession(); sessions.value[res.sessionId] = res; sessionList.value.unshift({ sessionId: res.sessionId, title: res.title, timestamp: new Date().toISOString(), }); setActiveSession(res.sessionId); } catch (error) { console.error('Error creating new session', error); } } async function sendMessage(explicitMessage?: string) { // `explicitMessage` is used to drain a buffered (queued) message; without it we send // whatever is currently typed in the input. const message = (explicitMessage ?? trimmedUserMessage.value).trim(); if (!message || isMessageInputBlocked.value) { return; } if (!currentSession.value || currentSession.value.sessionId === PRE_SESSION_ID) { await createNewSession(message); } currentSession.value!.timestamp = new Date().toISOString(); sessionList.value = sortSessionsListByTimestamp(sessionList.value.map((s: ISessionsListItem) => s.sessionId === currentSession.value?.sessionId ? { ...s, timestamp: currentSession.value?.timestamp || s.timestamp, } : s)); lastMessage.value = message; currentChat.value?.sendMessage({ text: message, }); if (explicitMessage === undefined) { userMessageInput.value = ''; } } async function createPreSession() { saveCurrentSessionInCache(); if (!sessionList.value.some((s: ISessionsListItem) => s.sessionId === PRE_SESSION_ID)) { sessionList.value.unshift({ sessionId: PRE_SESSION_ID, title: 'New Session', timestamp: new Date().toISOString(), }); } activeSessionId.value = PRE_SESSION_ID; currentSession.value = { sessionId: PRE_SESSION_ID, title: 'New Session', timestamp: new Date().toISOString(), messages: [], }; sessions.value[PRE_SESSION_ID] = currentSession.value; setCurrentChat(PRE_SESSION_ID); } async function deleteSession(sessionId: string) { if (sessionId === PRE_SESSION_ID) { deletePreSession(); return; } blockCloseOfChat.value = true; const isConfirmed = await adminforth.confirm({title: t('Are you sure, that you want to delete this session?'), message: t('This process is irreversible.'), yes: 'Yes', no: 'No', dangerous: true}); blockCloseOfChat.value = false; if (!isConfirmed) { return; } try { const res = await callAdminForthApi({ method: 'POST', path: '/agent/delete-session', body: { sessionId }, }); if (res.error) { console.error('Error deleting session:', res.error); return; } delete sessions.value[sessionId]; sessionList.value = sessionList.value.filter((s: ISessionsListItem) => s.sessionId !== sessionId); if (activeSessionId.value === sessionId) { activeSessionId.value = null; currentSession.value = null; } } catch (error) { console.error('Error deleting session', error); } if(sessionId === activeSessionId.value) { activeSessionId.value = sessionList.value.length > 0 ? sessionList.value[0].sessionId : null; if (activeSessionId.value) { currentSession.value = sessions.value[activeSessionId.value] || null; } else { currentSession.value = null; } } createPreSession(); } async function fetchSessionsList() { try { const res = await callAdminForthApi({ method: 'POST', path: '/agent/get-sessions', body: { limit: 100, }, }); if (res.error) { console.error('Error fetching sessions list:', res.error); return; } sessionList.value = res.sessions; } catch (error) { console.error('Error fetching sessions list', error); } } function addDebugMessage(message: string) { const debugMessage = { role: 'assistant', parts: [{ type: 'text', text: message, state: 'done', }] }; currentChat.value?.messages.push(debugMessage); } async function addSystemMessage(message: string) { if (!currentSession.value || currentSession.value.sessionId === PRE_SESSION_ID) { await createNewSession('Audio chat'); } const systemMessage = { role: 'system', parts: [{ type: 'text', text: message, state: 'done', }] }; currentChat.value?.messages.push(systemMessage); try { const res = await callAdminForthApi({ method: 'POST', path: '/agent/add-system-message-to-turns', body: { sessionId: activeSessionId.value, systemMessage: message, }, }); } catch (error) { console.error('Error adding system message', error); } } function addAgentMessage(message: string) { const agentMessage = { role: 'assistant', parts: [{ type: 'text', text: message, state: 'done', }] }; currentChat.value?.messages.push(agentMessage); } function updateLastAgentMessage(message: string) { const lastMsg = currentChat.value?.lastMessage; if (lastMsg && lastMsg.role === 'assistant') { lastMsg.parts = [{ type: 'text', text: message, state: 'done', }]; currentChat.value?.messages.splice(currentChat.value.messages.length - 1, 1, lastMsg); } else { addAgentMessage(message); } } function addUserMessage(message: string) { const userMessage = { role: 'user', parts: [{ type: 'text', text: message, state: 'done', }] }; currentChat.value?.messages.push(userMessage); } function addDataToolCallMessage(data: any) { const lastMessage = currentChat.value?.lastMessage; if (lastMessage.role === 'assistant') { lastMessage.parts.push({ type: 'data-tool-call', data, }); currentChat.value?.messages.splice(currentChat.value.messages.length - 1, 1, lastMessage); } else { const toolCallMessage = { role: 'assistant', parts: [{ type: 'data-tool-call', data, }] }; currentChat.value?.messages.push(toolCallMessage); } } function setCurrentChatStatus(status: any) { // ChatStatus type (currentChat.value as any)?.setStatus({status}); } return { sendMessage, createPreSession, setActiveSession, fetchSessionsList, deleteSession, addDebugMessage, addSystemMessage, addAgentMessage, addUserMessage, addDataToolCallMessage, setCurrentChatStatus, updateLastAgentMessage }; }