Skip to content

Commit 6f50082

Browse files
authored
Merge pull request #1890 from emqx/ysf/desktop
feat(copilot): introduce session management for AI interactions
2 parents cb470d6 + a2397da commit 6f50082

File tree

4 files changed

+391
-2
lines changed

4 files changed

+391
-2
lines changed

src/components/ai/Copilot.vue

+11-2
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,12 @@ import CopilotHeader from './CopilotHeader.vue'
3232
import CopilotMessages from './CopilotMessages.vue'
3333
import CopilotInput from './CopilotInput.vue'
3434
import { streamText } from 'ai'
35-
import { loadSystemPrompt, getModelProvider } from '@/utils/ai/copilot'
35+
import { getModelProvider } from '@/utils/ai/copilot'
3636
import { throttle } from 'lodash'
3737
import ConnectionsIndex from '@/views/connections/index.vue'
3838
import { CopilotMessage, CopilotRole, CopilotPresetPrompt, StreamError } from '@/types/copilot'
3939
import { needsUserInput } from '@/utils/ai/preset'
40+
import { SessionManager } from '@/utils/ai/SessionManager'
4041
4142
@Component({
4243
components: {
@@ -63,6 +64,8 @@ export default class Copilot extends Vue {
6364
private currPresetPrompt = ''
6465
private abortController: AbortController | null = null
6566
67+
private sessionManager = new SessionManager()
68+
6669
private systemMessage = {
6770
id: 'system-id',
6871
role: 'system',
@@ -117,6 +120,8 @@ export default class Copilot extends Vue {
117120
const content = msg || this.currentPublishMsg
118121
if (!content) return
119122
123+
this.sessionManager.startSession()
124+
120125
await this.saveUserMessage(content)
121126
await this.generateAIResponse()
122127
}
@@ -151,10 +156,12 @@ export default class Copilot extends Vue {
151156
}
152157
153158
private buildMessageHistory(): Array<{ role: CopilotRole; content: string }> {
159+
const systemPrompt = this.sessionManager.getSystemPrompt(this.currentLang)
160+
154161
return [
155162
{
156163
role: this.systemMessage.role as 'system',
157-
content: loadSystemPrompt(this.currentLang, this.currPresetPrompt),
164+
content: systemPrompt,
158165
},
159166
...this.messages.slice(-20).map(({ role, content }) => {
160167
if (content.includes('@connection')) {
@@ -275,6 +282,7 @@ export default class Copilot extends Vue {
275282
this.responseStreamText = ''
276283
const { copilotService } = useServices()
277284
await copilotService.deleteAll()
285+
this.sessionManager.resetSession()
278286
this.loadMessages({ reset: true })
279287
}
280288
@@ -294,6 +302,7 @@ export default class Copilot extends Vue {
294302
295303
await this.clearAllMessages()
296304
this.currPresetPrompt = data.prompt
305+
this.sessionManager.updatePreset(data.prompt)
297306
298307
const promptValue = data.promptMap[this.currPresetPrompt]
299308

src/utils/ai/SessionManager.ts

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { loadSystemPrompt } from './copilot'
2+
// Language type is globally defined
3+
// No need to explicitly import Language type
4+
5+
/**
6+
* Session state interface
7+
*/
8+
export interface SessionState {
9+
systemPrompt: string // Current system prompt for the session
10+
presetPrompt: string // Current preset prompt being used
11+
isNewSession: boolean // Whether this is a new session
12+
lastPresetChangeTime: number // Timestamp of the last preset change
13+
}
14+
15+
/**
16+
* Copilot Session Manager
17+
*
18+
* Manages conversation session state including system prompts, preset selection, and session lifecycle
19+
*/
20+
export class SessionManager {
21+
private state: SessionState = {
22+
systemPrompt: '',
23+
presetPrompt: '',
24+
isNewSession: true,
25+
lastPresetChangeTime: 0,
26+
}
27+
28+
/**
29+
* Get current session state
30+
*/
31+
public getState(): SessionState {
32+
return { ...this.state }
33+
}
34+
35+
/**
36+
* Reset session state
37+
*/
38+
public resetSession(): void {
39+
this.state = {
40+
systemPrompt: '',
41+
presetPrompt: '',
42+
isNewSession: true,
43+
lastPresetChangeTime: 0,
44+
}
45+
}
46+
47+
/**
48+
* Reset session while preserving preset
49+
* Useful for language switching scenarios
50+
*/
51+
public resetSessionKeepPreset(): void {
52+
const currentPreset = this.state.presetPrompt
53+
this.resetSession()
54+
this.state.presetPrompt = currentPreset
55+
}
56+
57+
/**
58+
* Mark session as started
59+
* Called when sending the first message
60+
*/
61+
public startSession(): void {
62+
this.state.isNewSession = false
63+
}
64+
65+
/**
66+
* Update session preset prompt
67+
*/
68+
public updatePreset(preset: string): void {
69+
this.state.presetPrompt = preset
70+
this.state.isNewSession = true
71+
this.state.lastPresetChangeTime = Date.now()
72+
this.state.systemPrompt = '' // Force system prompt reload
73+
}
74+
75+
/**
76+
* Get system prompt for current session
77+
* Reloads if necessary
78+
*/
79+
public getSystemPrompt(language: Language): string {
80+
// Reload system prompt if new session or empty
81+
if (this.state.isNewSession || !this.state.systemPrompt) {
82+
this.loadSystemPrompt(language)
83+
}
84+
return this.state.systemPrompt
85+
}
86+
87+
/**
88+
* Load system prompt for current session
89+
*/
90+
private loadSystemPrompt(language: Language): void {
91+
this.state.systemPrompt = loadSystemPrompt(language, this.state.presetPrompt)
92+
this.state.isNewSession = false
93+
}
94+
}
+170
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { expect } from 'chai'
2+
import { SessionManager } from '@/utils/ai/SessionManager'
3+
4+
describe('SessionManager', () => {
5+
let sessionManager: SessionManager
6+
7+
beforeEach(() => {
8+
// Create a new SessionManager instance before each test
9+
sessionManager = new SessionManager()
10+
})
11+
12+
describe('getState', () => {
13+
it('should return a copy of the state', () => {
14+
const state = sessionManager.getState()
15+
16+
// Verify that it returns an object
17+
expect(state).to.be.an('object')
18+
19+
// Verify that the returned state contains expected properties
20+
expect(state).to.have.property('systemPrompt')
21+
expect(state).to.have.property('isNewSession')
22+
expect(state).to.have.property('presetPrompt')
23+
24+
// Modifying the returned state should not affect the original state
25+
state.isNewSession = false
26+
expect(sessionManager.getState().isNewSession).to.be.true
27+
})
28+
})
29+
30+
describe('resetSession', () => {
31+
it('should reset the session to initial values', () => {
32+
// First modify the session state
33+
sessionManager.startSession()
34+
sessionManager.updatePreset('test-preset')
35+
36+
// Then reset
37+
sessionManager.resetSession()
38+
39+
// Verify the state has been reset
40+
const state = sessionManager.getState()
41+
expect(state.isNewSession).to.be.true
42+
expect(state.systemPrompt).to.equal('')
43+
expect(state.presetPrompt).to.equal('')
44+
})
45+
})
46+
47+
describe('resetSessionKeepPreset', () => {
48+
it('should reset the session while keeping preset prompt', () => {
49+
// First modify the session state
50+
sessionManager.startSession()
51+
sessionManager.updatePreset('test-preset')
52+
53+
// Then reset, but keep the preset prompt
54+
sessionManager.resetSessionKeepPreset()
55+
56+
// Verify the state has been reset, but preset prompt is retained
57+
const state = sessionManager.getState()
58+
expect(state.isNewSession).to.be.true
59+
expect(state.systemPrompt).to.equal('')
60+
expect(state.presetPrompt).to.equal('test-preset')
61+
})
62+
})
63+
64+
describe('startSession', () => {
65+
it('should mark the session as not new', () => {
66+
// Initially isNewSession should be true
67+
expect(sessionManager.getState().isNewSession).to.be.true
68+
69+
// Start the session
70+
sessionManager.startSession()
71+
72+
// Verify isNewSession becomes false
73+
expect(sessionManager.getState().isNewSession).to.be.false
74+
})
75+
76+
it('should not affect other state properties', () => {
77+
// Set some initial state
78+
sessionManager.updatePreset('test-preset')
79+
80+
// Record the current system prompt state
81+
const initialSystemPrompt = sessionManager.getState().systemPrompt
82+
83+
// Start the session
84+
sessionManager.startSession()
85+
86+
// Verify only the isNewSession property was modified
87+
const state = sessionManager.getState()
88+
expect(state.systemPrompt).to.equal(initialSystemPrompt)
89+
expect(state.presetPrompt).to.equal('test-preset')
90+
})
91+
})
92+
93+
describe('updatePreset', () => {
94+
it('should update the preset prompt', () => {
95+
sessionManager.updatePreset('new-preset')
96+
97+
expect(sessionManager.getState().presetPrompt).to.equal('new-preset')
98+
})
99+
100+
it('should mark the session as new', () => {
101+
// First mark the session as not new
102+
sessionManager.startSession()
103+
expect(sessionManager.getState().isNewSession).to.be.false
104+
105+
// Update the preset prompt
106+
sessionManager.updatePreset('new-preset')
107+
108+
// Verify the session is marked as new
109+
expect(sessionManager.getState().isNewSession).to.be.true
110+
})
111+
112+
it('should clear the system prompt', () => {
113+
// Simulate having an existing system prompt
114+
sessionManager.getSystemPrompt('en')
115+
expect(sessionManager.getState().systemPrompt).to.not.equal('')
116+
117+
// Update the preset prompt
118+
sessionManager.updatePreset('new-preset')
119+
120+
// Verify the system prompt is cleared
121+
expect(sessionManager.getState().systemPrompt).to.equal('')
122+
})
123+
})
124+
125+
describe('getSystemPrompt', () => {
126+
it('should load system prompt if session is new', () => {
127+
// Using sinon would encounter import issues, so we simply test behavior here
128+
const result = sessionManager.getSystemPrompt('en')
129+
130+
// Verify it returned a non-empty string
131+
expect(result).to.be.a('string')
132+
expect(result).to.not.equal('')
133+
134+
// Verify the state was updated
135+
expect(sessionManager.getState().systemPrompt).to.equal(result)
136+
})
137+
138+
it('should return cached system prompt if session is not new', () => {
139+
// Get the system prompt for the first time
140+
const firstPrompt = sessionManager.getSystemPrompt('en')
141+
142+
// Mark the session as not new
143+
sessionManager.startSession()
144+
145+
// Get the system prompt again
146+
const secondPrompt = sessionManager.getSystemPrompt('en')
147+
148+
// Verify the same prompt is returned
149+
expect(secondPrompt).to.equal(firstPrompt)
150+
})
151+
152+
it('should reload system prompt if empty even if session is not new', () => {
153+
// Get the system prompt for the first time
154+
sessionManager.getSystemPrompt('en')
155+
156+
// Mark the session as not new
157+
sessionManager.startSession()
158+
159+
// Manually clear the system prompt
160+
sessionManager['state'].systemPrompt = ''
161+
162+
// Get the system prompt again
163+
const reloadedPrompt = sessionManager.getSystemPrompt('en')
164+
165+
// Verify it returned a non-empty prompt
166+
expect(reloadedPrompt).to.be.a('string')
167+
expect(reloadedPrompt).to.not.equal('')
168+
})
169+
})
170+
})

0 commit comments

Comments
 (0)