Skip to content

Commit 460756f

Browse files
committed
feat(AI Assistant): improved state management and performance
1 parent 6f0fae0 commit 460756f

File tree

6 files changed

+114
-30
lines changed

6 files changed

+114
-30
lines changed

admin/app/controllers/ollama_controller.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ChatService } from '#services/chat_service'
12
import { OllamaService } from '#services/ollama_service'
23
import { RagService } from '#services/rag_service'
34
import { modelNameSchema } from '#validators/download'
@@ -11,6 +12,7 @@ import type { Message } from 'ollama'
1112
@inject()
1213
export default class OllamaController {
1314
constructor(
15+
private chatService: ChatService,
1416
private ollamaService: OllamaService,
1517
private ragService: RagService
1618
) { }
@@ -87,19 +89,59 @@ export default class OllamaController {
8789
const thinkingCapability = await this.ollamaService.checkModelHasThinking(reqData.model)
8890
const think: boolean | 'medium' = thinkingCapability ? (reqData.model.startsWith('gpt-oss') ? 'medium' : true) : false
8991

92+
// Separate sessionId from the Ollama request payload — Ollama rejects unknown fields
93+
const { sessionId, ...ollamaRequest } = reqData
94+
95+
// Save user message to DB before streaming if sessionId provided
96+
let userContent: string | null = null
97+
if (sessionId) {
98+
const lastUserMsg = [...reqData.messages].reverse().find((m) => m.role === 'user')
99+
if (lastUserMsg) {
100+
userContent = lastUserMsg.content
101+
await this.chatService.addMessage(sessionId, 'user', userContent)
102+
}
103+
}
104+
90105
if (reqData.stream) {
91106
logger.debug(`[OllamaController] Initiating streaming response for model: "${reqData.model}" with think: ${think}`)
92107
// Headers already flushed above
93-
const stream = await this.ollamaService.chatStream({ ...reqData, think })
108+
const stream = await this.ollamaService.chatStream({ ...ollamaRequest, think })
109+
let fullContent = ''
94110
for await (const chunk of stream) {
111+
if (chunk.message?.content) {
112+
fullContent += chunk.message.content
113+
}
95114
response.response.write(`data: ${JSON.stringify(chunk)}\n\n`)
96115
}
97116
response.response.end()
117+
118+
// Save assistant message and optionally generate title
119+
if (sessionId && fullContent) {
120+
await this.chatService.addMessage(sessionId, 'assistant', fullContent)
121+
const messageCount = await this.chatService.getMessageCount(sessionId)
122+
if (messageCount <= 2 && userContent) {
123+
this.chatService.generateTitle(sessionId, userContent, fullContent).catch((err) => {
124+
logger.error(`[OllamaController] Title generation failed: ${err instanceof Error ? err.message : err}`)
125+
})
126+
}
127+
}
98128
return
99129
}
100130

101131
// Non-streaming (legacy) path
102-
return await this.ollamaService.chat({ ...reqData, think })
132+
const result = await this.ollamaService.chat({ ...ollamaRequest, think })
133+
134+
if (sessionId && result?.message?.content) {
135+
await this.chatService.addMessage(sessionId, 'assistant', result.message.content)
136+
const messageCount = await this.chatService.getMessageCount(sessionId)
137+
if (messageCount <= 2 && userContent) {
138+
this.chatService.generateTitle(sessionId, userContent, result.message.content).catch((err) => {
139+
logger.error(`[OllamaController] Title generation failed: ${err instanceof Error ? err.message : err}`)
140+
})
141+
}
142+
}
143+
144+
return result
103145
} catch (error) {
104146
if (reqData.stream) {
105147
response.response.write(`data: ${JSON.stringify({ error: true })}\n\n`)

admin/app/services/chat_service.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import logger from '@adonisjs/core/services/logger'
44
import { DateTime } from 'luxon'
55
import { inject } from '@adonisjs/core'
66
import { OllamaService } from './ollama_service.js'
7-
import { SYSTEM_PROMPTS } from '../../constants/ollama.js'
7+
import { DEFAULT_QUERY_REWRITE_MODEL, SYSTEM_PROMPTS } from '../../constants/ollama.js'
88
import { toTitleCase } from '../utils/misc.js'
99

1010
@inject()
@@ -220,6 +220,59 @@ export class ChatService {
220220
}
221221
}
222222

223+
async getMessageCount(sessionId: number): Promise<number> {
224+
try {
225+
const count = await ChatMessage.query().where('session_id', sessionId).count('* as total')
226+
return Number(count[0].$extras.total)
227+
} catch (error) {
228+
logger.error(
229+
`[ChatService] Failed to get message count for session ${sessionId}: ${error instanceof Error ? error.message : error}`
230+
)
231+
return 0
232+
}
233+
}
234+
235+
async generateTitle(sessionId: number, userMessage: string, assistantMessage: string) {
236+
try {
237+
const models = await this.ollamaService.getModels()
238+
const titleModelAvailable = models?.some((m) => m.name === DEFAULT_QUERY_REWRITE_MODEL)
239+
240+
let title: string
241+
242+
if (!titleModelAvailable) {
243+
title = userMessage.slice(0, 57) + (userMessage.length > 57 ? '...' : '')
244+
} else {
245+
const response = await this.ollamaService.chat({
246+
model: DEFAULT_QUERY_REWRITE_MODEL,
247+
messages: [
248+
{ role: 'system', content: SYSTEM_PROMPTS.title_generation },
249+
{ role: 'user', content: userMessage },
250+
{ role: 'assistant', content: assistantMessage },
251+
],
252+
})
253+
254+
title = response?.message?.content?.trim()
255+
if (!title) {
256+
title = userMessage.slice(0, 57) + (userMessage.length > 57 ? '...' : '')
257+
}
258+
}
259+
260+
await this.updateSession(sessionId, { title })
261+
logger.info(`[ChatService] Generated title for session ${sessionId}: "${title}"`)
262+
} catch (error) {
263+
logger.error(
264+
`[ChatService] Failed to generate title for session ${sessionId}: ${error instanceof Error ? error.message : error}`
265+
)
266+
// Fall back to truncated user message
267+
try {
268+
const fallbackTitle = userMessage.slice(0, 57) + (userMessage.length > 57 ? '...' : '')
269+
await this.updateSession(sessionId, { title: fallbackTitle })
270+
} catch {
271+
// Silently fail - session keeps "New Chat" title
272+
}
273+
}
274+
}
275+
223276
async deleteAllSessions() {
224277
try {
225278
await ChatSession.query().delete()

admin/app/validators/ollama.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const chatSchema = vine.compile(
1010
})
1111
),
1212
stream: vine.boolean().optional(),
13+
sessionId: vine.number().positive().optional(),
1314
})
1415
)
1516

admin/constants/ollama.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,9 @@ IMPORTANT INSTRUCTIONS:
8383
1. If the user's question is directly related to the context above, use this information to provide accurate, detailed answers.
8484
2. Always cite or reference the context when using it (e.g., "According to the information available..." or "Based on the knowledge base...").
8585
3. If the context is only partially relevant, combine it with your general knowledge but be clear about what comes from the knowledge base.
86-
4. If the context is not relevant to the user's question, you can respond using your general knowledge without forcing the context into your answer.
86+
4. If the context is not relevant to the user's question, you can respond using your general knowledge without forcing the context into your answer. Do not mention the context if it's not relevant.
8787
5. Never fabricate information that isn't in the context or your training data.
88-
6. If you're unsure or the context doesn't contain enough information, acknowledge the limitations.
88+
6. If you're unsure or you don't have enough information to answer the user's question, acknowledge the limitations.
8989
9090
Format your response using markdown for readability.
9191
`,
@@ -113,6 +113,7 @@ Ensure that your suggestions are comma-seperated with no conjunctions like "and"
113113
Do not use line breaks, new lines, or extra spacing to separate the suggestions.
114114
Format: suggestion1, suggestion2, suggestion3
115115
`,
116+
title_generation: `You are a title generator. Given the start of a conversation, generate a concise, descriptive title under 60 characters. Return ONLY the title text with no quotes, punctuation wrapping, or extra formatting.`,
116117
query_rewrite: `
117118
You are a query rewriting assistant. Your task is to reformulate the user's latest question to include relevant context from the conversation history.
118119

admin/inertia/components/chat/index.tsx

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,9 @@ export default function Chat({
9090
mutationFn: (request: {
9191
model: string
9292
messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>
93+
sessionId?: number
9394
}) => api.sendChatMessage({ ...request, stream: false }),
94-
onSuccess: async (data, variables) => {
95+
onSuccess: async (data) => {
9596
if (!data || !activeSessionId) {
9697
throw new Error('No response from Ollama')
9798
}
@@ -106,17 +107,9 @@ export default function Chat({
106107

107108
setMessages((prev) => [...prev, assistantMessage])
108109

109-
// Save assistant message to backend
110-
await api.addChatMessage(activeSessionId, 'assistant', assistantMessage.content)
111-
112-
// Update session title if it's a new chat
113-
const currentSession = sessions.find((s) => s.id === activeSessionId)
114-
if (currentSession && currentSession.title === 'New Chat') {
115-
const userContent = variables.messages[variables.messages.length - 1].content
116-
const newTitle = userContent.slice(0, 50) + (userContent.length > 50 ? '...' : '')
117-
await api.updateChatSession(activeSessionId, { title: newTitle })
118-
queryClient.invalidateQueries({ queryKey: ['chatSessions'] })
119-
}
110+
// Refresh sessions to pick up backend-persisted messages and title
111+
queryClient.invalidateQueries({ queryKey: ['chatSessions'] })
112+
setTimeout(() => queryClient.invalidateQueries({ queryKey: ['chatSessions'] }), 3000)
120113
},
121114
onError: (error) => {
122115
console.error('Error sending message:', error)
@@ -230,9 +223,6 @@ export default function Chat({
230223

231224
setMessages((prev) => [...prev, userMessage])
232225

233-
// Save user message to backend
234-
await api.addChatMessage(sessionId, 'user', content)
235-
236226
const chatMessages = [
237227
...messages.map((m) => ({ role: m.role, content: m.content })),
238228
{ role: 'user' as const, content },
@@ -255,7 +245,7 @@ export default function Chat({
255245

256246
try {
257247
await api.streamChatMessage(
258-
{ model: selectedModel || 'llama3.2', messages: chatMessages, stream: true },
248+
{ model: selectedModel || 'llama3.2', messages: chatMessages, stream: true, sessionId: sessionId ? Number(sessionId) : undefined },
259249
(chunkContent, chunkThinking, done) => {
260250
if (chunkThinking.length > 0 && thinkingStartTime === null) {
261251
thinkingStartTime = Date.now()
@@ -336,24 +326,20 @@ export default function Chat({
336326
)
337327
)
338328

339-
await api.addChatMessage(sessionId, 'assistant', fullContent)
340-
341-
const currentSession = sessions.find((s) => s.id === sessionId)
342-
if (currentSession && currentSession.title === 'New Chat') {
343-
const newTitle = content.slice(0, 50) + (content.length > 50 ? '...' : '')
344-
await api.updateChatSession(sessionId, { title: newTitle })
345-
queryClient.invalidateQueries({ queryKey: ['chatSessions'] })
346-
}
329+
// Refresh sessions to pick up backend-persisted messages and title
330+
queryClient.invalidateQueries({ queryKey: ['chatSessions'] })
331+
setTimeout(() => queryClient.invalidateQueries({ queryKey: ['chatSessions'] }), 3000)
347332
}
348333
} else {
349334
// Non-streaming (legacy) path
350335
chatMutation.mutate({
351336
model: selectedModel || 'llama3.2',
352337
messages: chatMessages,
338+
sessionId: sessionId ? Number(sessionId) : undefined,
353339
})
354340
}
355341
},
356-
[activeSessionId, messages, selectedModel, chatMutation, queryClient, streamingEnabled, sessions]
342+
[activeSessionId, messages, selectedModel, chatMutation, queryClient, streamingEnabled]
357343
)
358344

359345
return (

admin/types/ollama.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export type OllamaChatRequest = {
3232
model: string
3333
messages: OllamaChatMessage[]
3434
stream?: boolean
35+
sessionId?: number
3536
}
3637

3738
export type OllamaChatResponse = {

0 commit comments

Comments
 (0)