This is an automated email from the ASF dual-hosted git repository. voidmatcha pushed a commit to branch temp/seung-00/assistant-transport-base in repository https://gitbox.apache.org/repos/asf/zeppelin.git
commit 313372203a53cb49e48d32b9a82402103194f270 Author: YONGJAE LEE <[email protected]> AuthorDate: Thu Oct 8 16:17:01 2026 +0900 Define shared assistant UI contracts --- .../entities/assistant/model/assistantContract.ts | 84 ++++++++++++++++++++++ .../assistant/model/messageHistory.spec.ts | 63 ++++++++++++++++ .../src/entities/assistant/model/messageHistory.ts | 37 ++++++++++ .../src/{public-api.ts => assistant-ui.ts} | 15 ++-- .../projects/zeppelin-sdk/src/public-api.ts | 2 + 5 files changed, 197 insertions(+), 4 deletions(-) diff --git a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/model/assistantContract.ts b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/model/assistantContract.ts new file mode 100644 index 0000000000..588b4293db --- /dev/null +++ b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/model/assistantContract.ts @@ -0,0 +1,84 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Data and operations the panel needs to display conversations and run replies. + * Implementations can use a server connection or local fixtures. + */ +export interface AssistantConversation { + id: string; + title?: string; + /** Account that created the conversation, shown as "Started by …". */ + ownerId?: string; + /** Whether the requesting user can send messages to the conversation, as the server decides. */ + canSendMessage?: boolean; + /** Last activity (ISO time), shown in the conversation list. */ + updatedAt?: string; +} +export interface AssistantMessage { + id: string; + role: 'user' | 'assistant'; + content: string; + /** From history: the tools the server called before this answer. Live runs report them as events instead. */ + toolCalls?: AssistantToolCallRef[]; +} +export interface AssistantToolCallRef { + id: string; + name: string; +} +/** One page of a conversation's history, oldest first. */ +export interface AssistantMessagePage { + messages: AssistantMessage[]; + /** Pass as `before` to load the page before this one; null when this page starts the conversation. */ + earlierCursor: string | null; +} +export interface AssistantRunBody { + prompt: string; +} +export type AssistantRunEvent = + | { type: 'run.started'; runId?: string } + | { type: 'run.completed'; runId?: string } + | { type: 'run.failed'; runId?: string; message: string } + | { type: 'message.delta'; messageId: string; delta: string } + | { type: 'message.done'; messageId: string; content: string } + | { type: 'tool_call.started'; toolCallId: string; name: string } + // The server does not repeat the tool name on completion. + | { type: 'tool_call.done'; toolCallId: string; name?: string }; + +export type AssistantRunState = 'idle' | 'running' | 'disconnected'; + +export interface AssistantTransport { + /** Conversations of the note the transport was created for. */ + listConversations(): Promise<AssistantConversation[]>; + createConversation(body: { title?: string }): Promise<AssistantConversation>; + deleteConversation(conversationId: string): Promise<void>; + /** The latest page of history, or the page before `before` (an `earlierCursor`). */ + getMessages(conversationId: string, before?: string): Promise<AssistantMessagePage>; + /** Observe a retained request, including lost connections while its server status is being recovered. */ + subscribeRunState?(conversationId: string, listener: (state: AssistantRunState) => void): () => void; + openRun(conversationId: string, body: AssistantRunBody, signal: AbortSignal): AsyncIterable<AssistantRunEvent>; +} + +export class AssistantStreamError extends Error { + constructor() { + super('The assistant stopped responding before the reply finished. Retry the request.'); + this.name = 'AssistantStreamError'; + } +} + +/** The notebook WebSocket closed during a run; its events went with it, so only a reload shows where it got to. */ +export class AssistantConnectionError extends Error { + constructor() { + super('The connection to the server was lost. Retry to reload the conversation and see where the answer got to.'); + this.name = 'AssistantConnectionError'; + } +} diff --git a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/model/messageHistory.spec.ts b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/model/messageHistory.spec.ts new file mode 100644 index 0000000000..f93216f700 --- /dev/null +++ b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/model/messageHistory.spec.ts @@ -0,0 +1,63 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, expect, it } from 'vitest'; + +import type { AssistantMessage } from './assistantContract'; +import { joinPages } from './messageHistory'; + +describe('joinPages', () => { + it('joins a reply split across pages under the later id and retains both action logs', () => { + const earlier: AssistantMessage[] = [ + { id: 'u1', role: 'user', content: 'Read this notebook' }, + { id: 'a1', role: 'assistant', content: 'Looking.', toolCalls: [{ id: 'c1', name: 'read' }] } + ]; + const later: AssistantMessage[] = [ + { id: 'a2', role: 'assistant', content: 'Done.', toolCalls: [{ id: 'c2', name: 'summarize' }] } + ]; + expect(joinPages(earlier, later)).toEqual([ + earlier[0], + { + id: 'a2', + role: 'assistant', + content: 'Looking.\n\nDone.', + toolCalls: [ + { id: 'c1', name: 'read' }, + { id: 'c2', name: 'summarize' } + ] + } + ]); + expect(earlier[1].content).toBe('Looking.'); + expect(later[0].content).toBe('Done.'); + }); + + it('retains a tool-only reply across an empty intermediate page and deduplicates repeated calls', () => { + const call = { id: 'c1', name: 'read' }; + const earlier: AssistantMessage[] = [{ id: 'a1', role: 'assistant', content: '', toolCalls: [call] }]; + const later: AssistantMessage[] = [{ id: 'a2', role: 'assistant', content: 'Done.', toolCalls: [call] }]; + expect(joinPages(joinPages(earlier, []), later)).toEqual([ + { id: 'a2', role: 'assistant', content: 'Done.', toolCalls: [call] } + ]); + }); + + it('keeps separate turns apart and handles empty history', () => { + const earlier: AssistantMessage[] = [{ id: 'a1', role: 'assistant', content: 'First answer' }]; + const later: AssistantMessage[] = [ + { id: 'u2', role: 'user', content: 'Another question' }, + { id: 'a2', role: 'assistant', content: 'Another answer' } + ]; + expect(joinPages(earlier, later)).toEqual([...earlier, ...later]); + expect(joinPages([], later)).toEqual(later); + expect(joinPages(earlier, [])).toEqual(earlier); + expect(joinPages([], [])).toEqual([]); + }); +}); diff --git a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/model/messageHistory.ts b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/model/messageHistory.ts new file mode 100644 index 0000000000..dce36c3f62 --- /dev/null +++ b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/model/messageHistory.ts @@ -0,0 +1,37 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { AssistantMessage } from './assistantContract'; + +export const mergeAnswers = (earlier: AssistantMessage, later: AssistantMessage): AssistantMessage => { + const toolCalls = Array.from( + new Map([...(earlier.toolCalls ?? []), ...(later.toolCalls ?? [])].map(call => [call.id, call])).values() + ); + return { + ...later, + content: [earlier.content, later.content].filter(Boolean).join('\n\n'), + ...(toolCalls.length ? { toolCalls } : {}) + }; +}; + +/** + * Puts an earlier page in front of the loaded history. A turn split across the page boundary + * becomes one bubble again, under the later id. + */ +export const joinPages = (earlier: AssistantMessage[], later: AssistantMessage[]): AssistantMessage[] => { + const last = earlier[earlier.length - 1]; + const first = later[0]; + if (last?.role === 'assistant' && first?.role === 'assistant') { + return [...earlier.slice(0, -1), mergeAnswers(last, first), ...later.slice(1)]; + } + return [...earlier, ...later]; +}; diff --git a/zeppelin-web-angular/projects/zeppelin-sdk/src/public-api.ts b/zeppelin-web-angular/projects/zeppelin-sdk/src/assistant-ui.ts similarity index 52% copy from zeppelin-web-angular/projects/zeppelin-sdk/src/public-api.ts copy to zeppelin-web-angular/projects/zeppelin-sdk/src/assistant-ui.ts index ba14a13c7e..8b10321d82 100644 --- a/zeppelin-web-angular/projects/zeppelin-sdk/src/public-api.ts +++ b/zeppelin-web-angular/projects/zeppelin-sdk/src/assistant-ui.ts @@ -10,7 +10,14 @@ * limitations under the License. */ -export * from './angular-object'; -export * from './interfaces/public-api'; -export * from './message'; -export * from './paragraph-state'; +/** The notebook sidebar's width: its default and its resize range. The assistant panel shares it. */ +export const NOTEBOOK_SIDEBAR_WIDTH = { initial: 370, min: 280, max: 800 } as const; + +/** Result of a host reveal: scrolled to it, already visible, or not rendered. */ +export type AssistantRevealResult = 'shown' | 'visible' | 'missing'; + +/** A notebook paragraph as the remote sees it: enough to label and link it. */ +export interface AssistantParagraphRef { + id: string; + title?: string; +} diff --git a/zeppelin-web-angular/projects/zeppelin-sdk/src/public-api.ts b/zeppelin-web-angular/projects/zeppelin-sdk/src/public-api.ts index ba14a13c7e..dcf432e844 100644 --- a/zeppelin-web-angular/projects/zeppelin-sdk/src/public-api.ts +++ b/zeppelin-web-angular/projects/zeppelin-sdk/src/public-api.ts @@ -11,6 +11,8 @@ */ export * from './angular-object'; +export * from './assistant-ui'; export * from './interfaces/public-api'; export * from './message'; + export * from './paragraph-state';
