@things-factory/ai-assistant 10.1.24 → 10.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/components/assistant-chat.ts +90 -18
- package/client/components/assistant-mode.test.ts +97 -0
- package/client/components/assistant-mode.ts +130 -0
- package/client/index.ts +1 -1
- package/dist-client/components/assistant-chat.d.ts +15 -4
- package/dist-client/components/assistant-chat.js +73 -18
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/components/assistant-mode.d.ts +97 -0
- package/dist-client/components/assistant-mode.js +36 -0
- package/dist-client/components/assistant-mode.js.map +1 -0
- package/dist-client/components/assistant-mode.test.d.ts +1 -0
- package/dist-client/components/assistant-mode.test.js +85 -0
- package/dist-client/components/assistant-mode.test.js.map +1 -0
- package/dist-client/index.d.ts +1 -1
- package/dist-client/index.js +1 -1
- package/dist-client/index.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/index.d.ts +1 -0
- package/dist-server/index.js +1 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/service/assistant-chat-resolver.js +3 -1
- package/dist-server/service/assistant-chat-resolver.js.map +1 -1
- package/dist-server/service/index.d.ts +1 -0
- package/dist-server/service/index.js +1 -0
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/tool-access.d.ts +4 -0
- package/dist-server/service/tool-access.js +23 -0
- package/dist-server/service/tool-access.js.map +1 -0
- package/dist-server/tool-registry-boot-report.d.ts +3 -0
- package/dist-server/tool-registry-boot-report.js +29 -0
- package/dist-server/tool-registry-boot-report.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/docs/session-controller.md +1 -1
- package/package.json +5 -5
- package/server/index.ts +1 -0
- package/server/service/assistant-chat-resolver.ts +3 -1
- package/server/service/index.ts +1 -0
- package/server/service/tool-access.test.ts +48 -0
- package/server/service/tool-access.ts +27 -0
- package/server/tool-registry-boot-report.test.ts +18 -0
- package/server/tool-registry-boot-report.ts +26 -0
- package/test/assistant-request-context.test.ts +2 -2
- package/test/assistant-session-controller.test.ts +0 -1
- package/test/assistant-session-toolbar.test.ts +2 -2
- package/client/utils/board-edit-patch.ts +0 -374
- package/dist-client/utils/board-edit-patch.d.ts +0 -157
- package/dist-client/utils/board-edit-patch.js +0 -284
- package/dist-client/utils/board-edit-patch.js.map +0 -1
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* - scopes / knownTypes / categories: 도메인 컨텍스트
|
|
8
8
|
*
|
|
9
9
|
* 출력 (이벤트):
|
|
10
|
-
* - `
|
|
11
|
-
* 호스트가 받아서 보드 모델에 적용 (
|
|
10
|
+
* - `scene-edit-patch` { detail: { patch, summary, confidence, patchId } }
|
|
11
|
+
* 호스트가 받아서 보드 모델에 적용 (applyScenePatch from @operato/scene-ops).
|
|
12
12
|
* - `chat-followup` { detail: { question } }
|
|
13
13
|
*
|
|
14
14
|
* 모드 전환은 컨테이너 (워크스페이스) 의 책임. 이 컴포넌트는 자체로 풀 채팅 UX.
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
* 뜨지 않는 것이 맞다.
|
|
60
60
|
*/
|
|
61
61
|
import { chatDefaults } from './chat-defaults.js'
|
|
62
|
+
import { isDifferentConversation, resolveConversation, type AssistantMode } from './assistant-mode.js'
|
|
62
63
|
import { availableTriggers, type TriggerChip } from './chat-triggers.js'
|
|
63
64
|
|
|
64
65
|
interface ChatLine {
|
|
@@ -365,6 +366,18 @@ export class OxAssistantChat extends LitElement {
|
|
|
365
366
|
* Who the assistant is on this surface, in the host's words. Required by `assistantChat`; the
|
|
366
367
|
* board conversation carries its own persona on the server and ignores this.
|
|
367
368
|
*/
|
|
369
|
+
/**
|
|
370
|
+
* What this conversation is — see `AssistantMode`.
|
|
371
|
+
*
|
|
372
|
+
* A host that sets this does not set `systemPrompt`, `toolCategories`, `hostContext` or
|
|
373
|
+
* `chatEndpoint` separately, and does not rely on `registerChatDefaults` for its `@` and `/`
|
|
374
|
+
* lists: the mode carries all of them together, so they cannot disagree with each other.
|
|
375
|
+
*
|
|
376
|
+
* When `mode.id` changes the conversation on screen is closed and a new one begins.
|
|
377
|
+
*/
|
|
378
|
+
@property({ attribute: false })
|
|
379
|
+
mode?: AssistantMode
|
|
380
|
+
|
|
368
381
|
@property({ type: String, attribute: 'system-prompt' })
|
|
369
382
|
systemPrompt?: string
|
|
370
383
|
|
|
@@ -594,7 +607,7 @@ export class OxAssistantChat extends LitElement {
|
|
|
594
607
|
The screen still shows nothing either way. There is no sentence to show, and inventing one is
|
|
595
608
|
what this whole correction removed.
|
|
596
609
|
*/
|
|
597
|
-
if (!
|
|
610
|
+
if (!this.conversation.stagedNoticeKey && !OxAssistantChat.warnedMissingStagedNotice) {
|
|
598
611
|
OxAssistantChat.warnedMissingStagedNotice = true
|
|
599
612
|
console.warn(
|
|
600
613
|
'[assistant-chat] a host reported a proposal staged but registered no `stagedNoticeKey`, ' +
|
|
@@ -692,12 +705,41 @@ export class OxAssistantChat extends LitElement {
|
|
|
692
705
|
/** centerTo 디바운스 timer — arrow nav / hover 빠를 때 카메라 애니메이션 race 방지 */
|
|
693
706
|
private centerDebounceTimer?: ReturnType<typeof setTimeout>
|
|
694
707
|
|
|
695
|
-
|
|
708
|
+
/*
|
|
709
|
+
* ── What this conversation is, resolved ──────────────────────────────────────────────
|
|
710
|
+
*
|
|
711
|
+
* A host that declares a mode gets the mode and nothing else — a field it leaves out is empty,
|
|
712
|
+
* never filled from these properties or the registered defaults. A host that has not declared
|
|
713
|
+
* one keeps the separate properties and the global defaults it had before.
|
|
714
|
+
*/
|
|
715
|
+
private get conversation() {
|
|
716
|
+
return resolveConversation(
|
|
717
|
+
this.mode,
|
|
718
|
+
{
|
|
719
|
+
systemPrompt: this.systemPrompt,
|
|
720
|
+
toolCategories: this.toolCategories,
|
|
721
|
+
hostContext: this.hostContext,
|
|
722
|
+
chatEndpoint: this.chatEndpoint
|
|
723
|
+
},
|
|
724
|
+
chatDefaults
|
|
725
|
+
)
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/*
|
|
729
|
+
* The `@` and `/` candidates, built once per mode.
|
|
730
|
+
*
|
|
731
|
+
* These were built at field initialisation, which meant a host could not change what the
|
|
732
|
+
* panel offers without replacing the element. A mode change rebuilds them below.
|
|
733
|
+
*/
|
|
696
734
|
private cachedCatalogCandidates: MentionCandidate[] = buildCatalogCandidates(chatDefaults.catalogEntries)
|
|
697
735
|
|
|
698
|
-
/** Default slash template 후보 캐시 — chat panel mount 시 1회 빌드 */
|
|
699
736
|
private cachedSlashCandidates: MentionCandidate[] = buildSlashCandidates(chatDefaults.slashTemplates)
|
|
700
737
|
|
|
738
|
+
private rebuildMentionCaches() {
|
|
739
|
+
this.cachedCatalogCandidates = buildCatalogCandidates(this.conversation.catalogEntries)
|
|
740
|
+
this.cachedSlashCandidates = buildSlashCandidates(this.conversation.slashTemplates)
|
|
741
|
+
}
|
|
742
|
+
|
|
701
743
|
/** `@` 마지막 query 의 도메인 사용자 후보 캐시 — userProvider 응답 도착 시 갱신.
|
|
702
744
|
* query 별 캐시가 아니라 단순 "최근 결과" — async race 회피. */
|
|
703
745
|
private cachedUserCandidates: MentionCandidate[] = []
|
|
@@ -2214,6 +2256,36 @@ export class OxAssistantChat extends LitElement {
|
|
|
2214
2256
|
this.send()
|
|
2215
2257
|
}
|
|
2216
2258
|
|
|
2259
|
+
/*
|
|
2260
|
+
* The subject changed, so the conversation is over.
|
|
2261
|
+
*
|
|
2262
|
+
* A conversation about station 3 is not a conversation about station 7. Leaving the old one
|
|
2263
|
+
* on screen means the user reads an answer about a screen they are no longer looking at,
|
|
2264
|
+
* and every host that met this wrote its own guard: plant re-checked the context on each
|
|
2265
|
+
* send and threw when it had moved, twin closed its session when the anchor changed. The
|
|
2266
|
+
* mode carries an id, so the component can do it once, here.
|
|
2267
|
+
*
|
|
2268
|
+
* The host still owns `sessionId`; it hears `assistant-mode-changed` and decides whether
|
|
2269
|
+
* this subject gets a stored session of its own.
|
|
2270
|
+
*/
|
|
2271
|
+
if (changed.has('mode') && isDifferentConversation(changed.get('mode'), this.mode)) {
|
|
2272
|
+
this.rebuildMentionCaches()
|
|
2273
|
+
this.lines = []
|
|
2274
|
+
this.pickedMentions.clear()
|
|
2275
|
+
this.pickedUserMentions.clear()
|
|
2276
|
+
this.cachedUserCandidates = []
|
|
2277
|
+
this.errorMessage = undefined
|
|
2278
|
+
this.input = ''
|
|
2279
|
+
this._seenMessageIds.clear()
|
|
2280
|
+
this.dispatchEvent(
|
|
2281
|
+
new CustomEvent('assistant-mode-changed', {
|
|
2282
|
+
detail: { mode: this.mode },
|
|
2283
|
+
bubbles: true,
|
|
2284
|
+
composed: true
|
|
2285
|
+
})
|
|
2286
|
+
)
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2217
2289
|
if (changed.has('sessionId') && this.adoptingSession) {
|
|
2218
2290
|
this.adoptingSession = false
|
|
2219
2291
|
this._startPresence()
|
|
@@ -2815,7 +2887,7 @@ export class OxAssistantChat extends LitElement {
|
|
|
2815
2887
|
|
|
2816
2888
|
private dispatchActions(actions: any[]) {
|
|
2817
2889
|
this.dispatchEvent(
|
|
2818
|
-
new CustomEvent('
|
|
2890
|
+
new CustomEvent('scene-action-execute', {
|
|
2819
2891
|
detail: { actions, sessionId: this.sessionId },
|
|
2820
2892
|
bubbles: true,
|
|
2821
2893
|
composed: true
|
|
@@ -2946,7 +3018,7 @@ export class OxAssistantChat extends LitElement {
|
|
|
2946
3018
|
return availableTriggers({
|
|
2947
3019
|
hasMentionableThings: !!(this.boardProvider || this.currentBoard || this.knownTypes?.length),
|
|
2948
3020
|
hasSession: !!this.sessionId,
|
|
2949
|
-
slashTemplateCount:
|
|
3021
|
+
slashTemplateCount: this.conversation.slashTemplates.length
|
|
2950
3022
|
})
|
|
2951
3023
|
}
|
|
2952
3024
|
|
|
@@ -3082,8 +3154,8 @@ export class OxAssistantChat extends LitElement {
|
|
|
3082
3154
|
<span class="badge">${i18next.t('ai-assistant.label.korean-supported')}</span>
|
|
3083
3155
|
<span class="badge">${i18next.t('ai-assistant.label.multi-command')}</span>
|
|
3084
3156
|
<span class="badge">${i18next.t('ai-assistant.label.review-able')}</span>
|
|
3085
|
-
${
|
|
3086
|
-
? html`<br />${i18next.t(
|
|
3157
|
+
${this.conversation.footerNoticeKey
|
|
3158
|
+
? html`<br />${i18next.t(this.conversation.footerNoticeKey)}`
|
|
3087
3159
|
: ''}
|
|
3088
3160
|
</div>
|
|
3089
3161
|
</div>
|
|
@@ -3242,7 +3314,7 @@ export class OxAssistantChat extends LitElement {
|
|
|
3242
3314
|
to live here named a 3D modeller's operations in hardcoded Korean, and it reached the
|
|
3243
3315
|
plant and twin chats too.
|
|
3244
3316
|
*/
|
|
3245
|
-
const rawChoices: any[] = Array.isArray(p.choices) ? p.choices :
|
|
3317
|
+
const rawChoices: any[] = Array.isArray(p.choices) ? p.choices : this.conversation.proposalChoices
|
|
3246
3318
|
|
|
3247
3319
|
return html`
|
|
3248
3320
|
<div class="proposal ${sent ? 'sent' : ''}">
|
|
@@ -3271,10 +3343,10 @@ export class OxAssistantChat extends LitElement {
|
|
|
3271
3343
|
This badge used to read "there are chips" as "the model is in the 3D viewer" and
|
|
3272
3344
|
say so, in a component that plant and twin also use.
|
|
3273
3345
|
*/ ''}
|
|
3274
|
-
${this.stagedProposals.has(key) &&
|
|
3346
|
+
${this.stagedProposals.has(key) && this.conversation.stagedNoticeKey ? html`
|
|
3275
3347
|
<span class="staged-badge">
|
|
3276
3348
|
<md-icon>check_circle</md-icon>
|
|
3277
|
-
<span>${i18next.t(
|
|
3349
|
+
<span>${i18next.t(this.conversation.stagedNoticeKey!)}</span>
|
|
3278
3350
|
</span>
|
|
3279
3351
|
` : nothing}
|
|
3280
3352
|
</div>
|
|
@@ -3668,7 +3740,7 @@ export class OxAssistantChat extends LitElement {
|
|
|
3668
3740
|
let requestBase: any
|
|
3669
3741
|
try {
|
|
3670
3742
|
const provided = this.contextProvider?.() || {}
|
|
3671
|
-
requestContext = structuredClone({ systemPrompt: this.systemPrompt, hostContext: this.hostContext, ...provided })
|
|
3743
|
+
requestContext = structuredClone({ systemPrompt: this.conversation.systemPrompt, hostContext: this.conversation.hostContext, ...provided })
|
|
3672
3744
|
liveBoard = structuredClone(this.boardProvider ? this.boardProvider() : this.currentBoard)
|
|
3673
3745
|
requestBase = { version: 1, sessionId: this.sessionId, hostContext: requestContext.hostContext,
|
|
3674
3746
|
modelFingerprint: assistantModelFingerprint(liveBoard) }
|
|
@@ -3745,7 +3817,7 @@ export class OxAssistantChat extends LitElement {
|
|
|
3745
3817
|
// 라이브 보드 우선 — 호스트의 캔버스가 사용자 수작업 편집을 들고 있을 수 있음.
|
|
3746
3818
|
// boardProvider 가 있으면 send 시점에 그것을 pull, 없으면 정적 currentBoard.
|
|
3747
3819
|
// mentions 는 위에서 이미 계산해 user line 에 thread — 그대로 mutation 입력으로 재사용.
|
|
3748
|
-
const neutral = this.chatEndpoint === 'assistantChat'
|
|
3820
|
+
const neutral = this.conversation.chatEndpoint === 'assistantChat'
|
|
3749
3821
|
const result = await client.mutate({
|
|
3750
3822
|
mutation: neutral ? ASSISTANT_CHAT_MUTATION : BOARD_AI_CHAT_MUTATION,
|
|
3751
3823
|
variables: {
|
|
@@ -3756,7 +3828,7 @@ export class OxAssistantChat extends LitElement {
|
|
|
3756
3828
|
systemPrompt: requestContext.systemPrompt!,
|
|
3757
3829
|
truncateAfterMessageId: this._truncateAfterId,
|
|
3758
3830
|
hostContext: requestContext.hostContext,
|
|
3759
|
-
toolCategories: this.toolCategories,
|
|
3831
|
+
toolCategories: this.conversation.toolCategories,
|
|
3760
3832
|
requireGroundingTools: this.requireGroundingTools
|
|
3761
3833
|
})
|
|
3762
3834
|
: buildChatMutationInput({
|
|
@@ -3766,7 +3838,7 @@ export class OxAssistantChat extends LitElement {
|
|
|
3766
3838
|
scopes: this.scopes,
|
|
3767
3839
|
truncateAfterMessageId: this._truncateAfterId,
|
|
3768
3840
|
hostContext: requestContext.hostContext,
|
|
3769
|
-
toolCategories: this.toolCategories,
|
|
3841
|
+
toolCategories: this.conversation.toolCategories,
|
|
3770
3842
|
boardTools: this.boardTools,
|
|
3771
3843
|
requireGroundingTools: this.requireGroundingTools,
|
|
3772
3844
|
knownTypes: this.knownTypes,
|
|
@@ -3821,7 +3893,7 @@ export class OxAssistantChat extends LitElement {
|
|
|
3821
3893
|
// 호스트로 patch 이벤트 전파
|
|
3822
3894
|
if (out.patch) {
|
|
3823
3895
|
this.dispatchEvent(
|
|
3824
|
-
new CustomEvent('
|
|
3896
|
+
new CustomEvent('scene-edit-patch', {
|
|
3825
3897
|
detail: {
|
|
3826
3898
|
patch: out.patch,
|
|
3827
3899
|
requestBase,
|
|
@@ -3838,7 +3910,7 @@ export class OxAssistantChat extends LitElement {
|
|
|
3838
3910
|
// 호스트로 ephemeral scene 조작 actions 전파 (selection / view / mode)
|
|
3839
3911
|
if (Array.isArray(out.actions) && out.actions.length > 0) {
|
|
3840
3912
|
this.dispatchEvent(
|
|
3841
|
-
new CustomEvent('
|
|
3913
|
+
new CustomEvent('scene-action-execute', {
|
|
3842
3914
|
detail: { actions: out.actions, sessionId: out.sessionId, requestBase },
|
|
3843
3915
|
bubbles: true,
|
|
3844
3916
|
composed: true
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A mode is one value, and the point of it is that the parts of a conversation cannot
|
|
3
|
+
* disagree with each other.
|
|
4
|
+
*
|
|
5
|
+
* The defect this guards against shipped: a screen whose systemPrompt said "you do not edit
|
|
6
|
+
* boards here" offered `/add-monitor` in the same panel, because the prompt was an element
|
|
7
|
+
* property and the slash list was a module global. Any resolution that lets a mode supply one
|
|
8
|
+
* of them while something else supplies the other brings it back.
|
|
9
|
+
*/
|
|
10
|
+
import { isDifferentConversation, resolveConversation, type AssistantMode } from './assistant-mode'
|
|
11
|
+
|
|
12
|
+
const MODE: AssistantMode = {
|
|
13
|
+
id: 'twin:space:S-12',
|
|
14
|
+
systemPrompt: 'You are looking at one space. You do not edit boards here.',
|
|
15
|
+
toolCategories: ['twin'],
|
|
16
|
+
hostContext: { spaceId: 'S-12' },
|
|
17
|
+
slashTemplates: [{ name: 'status', template: '이 공간의 지금 상태를 요약해줘' }]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const HOST_PROPERTIES = {
|
|
21
|
+
systemPrompt: 'a prompt the host set the old way',
|
|
22
|
+
toolCategories: ['board-ai'],
|
|
23
|
+
hostContext: { boardId: 'B-1' },
|
|
24
|
+
chatEndpoint: 'boardAIChat' as const
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const GLOBAL_DEFAULTS = {
|
|
28
|
+
catalogEntries: [{ type: 'rect' }],
|
|
29
|
+
slashTemplates: [{ name: 'add-monitor', template: '모니터를 추가해줘' }],
|
|
30
|
+
footerNoticeKey: 'text.previewed-on-the-board'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('resolveConversation', () => {
|
|
34
|
+
test('a mode answers before the host properties it overlaps', () => {
|
|
35
|
+
const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS)
|
|
36
|
+
expect(c.systemPrompt).toBe(MODE.systemPrompt)
|
|
37
|
+
expect(c.toolCategories).toEqual(['twin'])
|
|
38
|
+
expect(c.hostContext).toEqual({ spaceId: 'S-12' })
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('a mode answers before the global defaults — the prompt and the slash list cannot disagree', () => {
|
|
42
|
+
const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS)
|
|
43
|
+
expect(c.slashTemplates).toEqual(MODE.slashTemplates)
|
|
44
|
+
/* A mode that says it does not edit boards must not offer a board edit. */
|
|
45
|
+
expect(JSON.stringify(c.slashTemplates)).not.toContain('add-monitor')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('★ what a mode leaves out stays out — the defaults another package registered do not fill it', () => {
|
|
49
|
+
const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS)
|
|
50
|
+
/* The board's `@` catalog and its "previewed on the board" line belong to board authoring. */
|
|
51
|
+
expect(c.catalogEntries).toEqual([])
|
|
52
|
+
expect(c.footerNoticeKey).toBeUndefined()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('★ a mode that declares no slash list offers none, even when a board package registered its own', () => {
|
|
56
|
+
/* plant's factory mode declares no `slashTemplates`; board-ai registers `/add-monitor` globally. */
|
|
57
|
+
const { slashTemplates, ...withoutSlash } = MODE
|
|
58
|
+
const c = resolveConversation({ ...withoutSlash, id: 'plant:factory:D-1' }, HOST_PROPERTIES, GLOBAL_DEFAULTS)
|
|
59
|
+
expect(c.slashTemplates).toEqual([])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('★ a mode that leaves out a field the host also set as a property does not take the property either', () => {
|
|
63
|
+
const c = resolveConversation({ id: 'twin:none' }, HOST_PROPERTIES, GLOBAL_DEFAULTS)
|
|
64
|
+
expect(c.systemPrompt).toBeUndefined()
|
|
65
|
+
expect(c.toolCategories).toBeUndefined()
|
|
66
|
+
expect(c.hostContext).toBeUndefined()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('a host with no mode gets exactly what it had before modes existed', () => {
|
|
70
|
+
const c = resolveConversation(undefined, HOST_PROPERTIES, GLOBAL_DEFAULTS)
|
|
71
|
+
expect(c.systemPrompt).toBe(HOST_PROPERTIES.systemPrompt)
|
|
72
|
+
expect(c.toolCategories).toEqual(HOST_PROPERTIES.toolCategories)
|
|
73
|
+
expect(c.slashTemplates).toEqual(GLOBAL_DEFAULTS.slashTemplates)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('nothing anywhere resolves to empty lists and the board endpoint, never undefined', () => {
|
|
77
|
+
const c = resolveConversation(undefined, {}, {})
|
|
78
|
+
expect(c.catalogEntries).toEqual([])
|
|
79
|
+
expect(c.slashTemplates).toEqual([])
|
|
80
|
+
expect(c.proposalChoices).toEqual([])
|
|
81
|
+
expect(c.chatEndpoint).toBe('boardAIChat')
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe('isDifferentConversation', () => {
|
|
86
|
+
const station3: AssistantMode = { id: 'plant:station:3' }
|
|
87
|
+
|
|
88
|
+
test('a different subject ends the conversation', () => {
|
|
89
|
+
expect(isDifferentConversation(station3, { id: 'plant:station:7' })).toBe(true)
|
|
90
|
+
expect(isDifferentConversation(undefined, station3)).toBe(true)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('rewording the same subject does not', () => {
|
|
94
|
+
expect(isDifferentConversation(station3, { ...station3, systemPrompt: 'reworded' })).toBe(false)
|
|
95
|
+
expect(isDifferentConversation(undefined, undefined)).toBe(false)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A mode is what a conversation *is*: who the assistant is, what it can do, what the talk is
|
|
3
|
+
* about, what it offers to start with, and when it is over.
|
|
4
|
+
*
|
|
5
|
+
* Those five used to be four separate properties on the chat element plus one global registry,
|
|
6
|
+
* and every host wired them one at a time. Separate means they can disagree, and they did: a
|
|
7
|
+
* screen whose `systemPrompt` said "you do not edit boards here" offered `/add-monitor` in the
|
|
8
|
+
* same panel, because the prompt was a property and the slash list was global.
|
|
9
|
+
*
|
|
10
|
+
* So a host declares one value and hands it over. What cannot be split cannot fall out of step.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* const SPACE_MODE = (spaceId: string): AssistantMode => ({
|
|
14
|
+
* id: `twin:space:${spaceId}`,
|
|
15
|
+
* systemPrompt: '...',
|
|
16
|
+
* toolCategories: ['twin'],
|
|
17
|
+
* hostContext: { spaceId },
|
|
18
|
+
* slashTemplates: TWIN_SLASH_TEMPLATES
|
|
19
|
+
* })
|
|
20
|
+
*
|
|
21
|
+
* html`<ox-assistant-chat .mode=${SPACE_MODE(this.spaceId)}></ox-assistant-chat>`
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export interface AssistantMode {
|
|
25
|
+
/**
|
|
26
|
+
* What this conversation is about, as a string the host can compare.
|
|
27
|
+
*
|
|
28
|
+
* **The conversation's lifetime hangs on this.** When it changes, the chat closes what is on
|
|
29
|
+
* screen and starts again — because a conversation about station 3 is not a conversation
|
|
30
|
+
* about station 7, and leaving the old one up means the user reads an answer about a screen
|
|
31
|
+
* they are no longer looking at.
|
|
32
|
+
*
|
|
33
|
+
* Hosts built this by hand before, each in their own way: plant kept a module-global registry
|
|
34
|
+
* with subscribers and re-checked the context on every send, twin closed its session when the
|
|
35
|
+
* anchor moved. Both were writing the same guard around the same absence.
|
|
36
|
+
*
|
|
37
|
+
* Include whatever makes the subject specific — `twin:space:S-12`, `plant:station:3`.
|
|
38
|
+
*/
|
|
39
|
+
id: string
|
|
40
|
+
|
|
41
|
+
/** Who the assistant is here, and what it must not claim to do. */
|
|
42
|
+
systemPrompt?: string
|
|
43
|
+
|
|
44
|
+
/** Which tool categories the server may expose for this conversation. */
|
|
45
|
+
toolCategories?: string[]
|
|
46
|
+
|
|
47
|
+
/** What the host knows about the subject; travels with each request. */
|
|
48
|
+
hostContext?: any
|
|
49
|
+
|
|
50
|
+
/** Which server-side chat entry point answers. */
|
|
51
|
+
chatEndpoint?: 'boardAIChat' | 'assistantChat'
|
|
52
|
+
|
|
53
|
+
/** What `@` offers. */
|
|
54
|
+
catalogEntries?: any[]
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* What `/` offers.
|
|
58
|
+
*
|
|
59
|
+
* These are sentences a person could have typed, not tool names — "이 공간의 지금 상태를
|
|
60
|
+
* 요약해줘" cannot be generated from `summarizeStatus`. But **a mode must not offer work it
|
|
61
|
+
* cannot do**: everything here should be answerable by the tools `toolCategories` opens.
|
|
62
|
+
*/
|
|
63
|
+
slashTemplates?: any[]
|
|
64
|
+
|
|
65
|
+
/** The i18n key for the line under an empty conversation. */
|
|
66
|
+
footerNoticeKey?: string
|
|
67
|
+
|
|
68
|
+
/** Follow-up chips for a proposal that arrived without its own. */
|
|
69
|
+
proposalChoices?: any[]
|
|
70
|
+
|
|
71
|
+
/** The i18n key for the line shown once the host reports a proposal staged. */
|
|
72
|
+
stagedNoticeKey?: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** True when these two describe different conversations, so the old one should be closed. */
|
|
76
|
+
export function isDifferentConversation(a?: AssistantMode, b?: AssistantMode): boolean {
|
|
77
|
+
return (a?.id ?? '') !== (b?.id ?? '')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Everything a conversation needs, after the mode, the host's own properties and the
|
|
81
|
+
* registered defaults have been reconciled. */
|
|
82
|
+
export interface ConversationSettings {
|
|
83
|
+
systemPrompt?: string
|
|
84
|
+
toolCategories?: string[]
|
|
85
|
+
hostContext?: any
|
|
86
|
+
chatEndpoint: 'boardAIChat' | 'assistantChat'
|
|
87
|
+
catalogEntries: any[]
|
|
88
|
+
slashTemplates: any[]
|
|
89
|
+
footerNoticeKey?: string
|
|
90
|
+
proposalChoices: any[]
|
|
91
|
+
stagedNoticeKey?: string
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Reconcile the places a setting can come from.
|
|
96
|
+
*
|
|
97
|
+
* **A host that declares a mode gets the mode and nothing else.** A field the mode leaves out is
|
|
98
|
+
* empty: it is not filled from the element's own properties, and it is not filled from the
|
|
99
|
+
* defaults another package registered. Filling it back in is how the defect this type removes
|
|
100
|
+
* returned — board-ai registers board authoring's `/add-monitor`, its component `@` catalog and
|
|
101
|
+
* its "previewed on the board" line globally, and plant's factory mode, which declares no slash
|
|
102
|
+
* list, offered them on a factory screen once the board modeller had been loaded. A mode that
|
|
103
|
+
* wants those declares them.
|
|
104
|
+
*
|
|
105
|
+
* A host that declares no mode gets exactly what it got before modes existed — its own
|
|
106
|
+
* properties, falling back to the registered defaults.
|
|
107
|
+
*
|
|
108
|
+
* `chatEndpoint` still falls back to `boardAIChat` when nothing names one; that default is a
|
|
109
|
+
* separate question (standard A2).
|
|
110
|
+
*/
|
|
111
|
+
export function resolveConversation(
|
|
112
|
+
mode: AssistantMode | undefined,
|
|
113
|
+
own: Partial<ConversationSettings>,
|
|
114
|
+
defaults: Partial<ConversationSettings>
|
|
115
|
+
): ConversationSettings {
|
|
116
|
+
const pick = <K extends keyof ConversationSettings>(key: K): ConversationSettings[K] =>
|
|
117
|
+
mode ? (mode as any)[key] : ((own as any)[key] ?? (defaults as any)[key])
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
systemPrompt: pick('systemPrompt'),
|
|
121
|
+
toolCategories: pick('toolCategories'),
|
|
122
|
+
hostContext: pick('hostContext'),
|
|
123
|
+
chatEndpoint: pick('chatEndpoint') ?? 'boardAIChat',
|
|
124
|
+
catalogEntries: pick('catalogEntries') ?? [],
|
|
125
|
+
slashTemplates: pick('slashTemplates') ?? [],
|
|
126
|
+
footerNoticeKey: pick('footerNoticeKey'),
|
|
127
|
+
proposalChoices: pick('proposalChoices') ?? [],
|
|
128
|
+
stagedNoticeKey: pick('stagedNoticeKey')
|
|
129
|
+
}
|
|
130
|
+
}
|
package/client/index.ts
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export * from './components/assistant-chat.js'
|
|
7
7
|
export * from './components/chat-defaults.js'
|
|
8
|
+
export * from './components/assistant-mode.js'
|
|
8
9
|
export * from './components/markdown.js'
|
|
9
10
|
export * from './components/chat-echo-dedup.js'
|
|
10
11
|
export * from './components/chat-input-builder.js'
|
|
11
12
|
export * from './components/mention-popup.js'
|
|
12
13
|
export * from './components/mention-popup-helpers.js'
|
|
13
|
-
export * from './utils/board-edit-patch.js'
|
|
14
14
|
export * from './utils/assistant-session-controller.js'
|
|
15
15
|
export * from './utils/assistant-session-transport.js'
|
|
16
16
|
export * from './components/assistant-session-toolbar.js'
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* - scopes / knownTypes / categories: 도메인 컨텍스트
|
|
8
8
|
*
|
|
9
9
|
* 출력 (이벤트):
|
|
10
|
-
* - `
|
|
11
|
-
* 호스트가 받아서 보드 모델에 적용 (
|
|
10
|
+
* - `scene-edit-patch` { detail: { patch, summary, confidence, patchId } }
|
|
11
|
+
* 호스트가 받아서 보드 모델에 적용 (applyScenePatch from @operato/scene-ops).
|
|
12
12
|
* - `chat-followup` { detail: { question } }
|
|
13
13
|
*
|
|
14
14
|
* 모드 전환은 컨테이너 (워크스페이스) 의 책임. 이 컴포넌트는 자체로 풀 채팅 UX.
|
|
@@ -19,6 +19,7 @@ import { type AssistantRequestContext } from '../utils/assistant-request-context
|
|
|
19
19
|
import { LitElement } from 'lit';
|
|
20
20
|
import './mention-popup.js';
|
|
21
21
|
import { type FilterResult } from './mention-popup.js';
|
|
22
|
+
import { type AssistantMode } from './assistant-mode.js';
|
|
22
23
|
export interface ChatAttachment {
|
|
23
24
|
id: string;
|
|
24
25
|
file: File;
|
|
@@ -118,6 +119,16 @@ export declare class OxAssistantChat extends LitElement {
|
|
|
118
119
|
* Who the assistant is on this surface, in the host's words. Required by `assistantChat`; the
|
|
119
120
|
* board conversation carries its own persona on the server and ignores this.
|
|
120
121
|
*/
|
|
122
|
+
/**
|
|
123
|
+
* What this conversation is — see `AssistantMode`.
|
|
124
|
+
*
|
|
125
|
+
* A host that sets this does not set `systemPrompt`, `toolCategories`, `hostContext` or
|
|
126
|
+
* `chatEndpoint` separately, and does not rely on `registerChatDefaults` for its `@` and `/`
|
|
127
|
+
* lists: the mode carries all of them together, so they cannot disagree with each other.
|
|
128
|
+
*
|
|
129
|
+
* When `mode.id` changes the conversation on screen is closed and a new one begins.
|
|
130
|
+
*/
|
|
131
|
+
mode?: AssistantMode;
|
|
121
132
|
systemPrompt?: string;
|
|
122
133
|
/**
|
|
123
134
|
* 라이브 상태를 다루는 대화면인가 — true 면 **첫 턴에 도구 호출을 강제**한다.
|
|
@@ -279,10 +290,10 @@ export declare class OxAssistantChat extends LitElement {
|
|
|
279
290
|
private inputDebounceTimer?;
|
|
280
291
|
/** centerTo 디바운스 timer — arrow nav / hover 빠를 때 카메라 애니메이션 race 방지 */
|
|
281
292
|
private centerDebounceTimer?;
|
|
282
|
-
|
|
293
|
+
private get conversation();
|
|
283
294
|
private cachedCatalogCandidates;
|
|
284
|
-
/** Default slash template 후보 캐시 — chat panel mount 시 1회 빌드 */
|
|
285
295
|
private cachedSlashCandidates;
|
|
296
|
+
private rebuildMentionCaches;
|
|
286
297
|
/** `@` 마지막 query 의 도메인 사용자 후보 캐시 — userProvider 응답 도착 시 갱신.
|
|
287
298
|
* query 별 캐시가 아니라 단순 "최근 결과" — async race 회피. */
|
|
288
299
|
private cachedUserCandidates;
|