diff --git a/packages/backend/server/src/plugins/copilot/providers/utils.ts b/packages/backend/server/src/plugins/copilot/providers/utils.ts index 98bfe832c5..0d9092a35a 100644 --- a/packages/backend/server/src/plugins/copilot/providers/utils.ts +++ b/packages/backend/server/src/plugins/copilot/providers/utils.ts @@ -390,19 +390,19 @@ export interface CustomAITools extends ToolSet { type ChunkType = TextStreamPart['type']; -export function parseUnknownError(error: unknown) { +export function toError(error: unknown): Error { if (typeof error === 'string') { - throw new Error(error); + return new Error(error); } else if (error instanceof Error) { - throw error; + return error; } else if ( typeof error === 'object' && error !== null && 'message' in error ) { - throw new Error(String(error.message)); + return new Error(String(error.message)); } else { - throw new Error(JSON.stringify(error)); + return new Error(JSON.stringify(error)); } } @@ -483,8 +483,7 @@ export class TextStreamParser { break; } case 'error': { - parseUnknownError(chunk.error); - break; + throw toError(chunk.error); } } this.lastType = chunk.type; @@ -550,8 +549,7 @@ export class StreamObjectParser { return chunk; } case 'error': { - parseUnknownError(chunk.error); - return null; + throw toError(chunk.error); } default: { return null; diff --git a/packages/backend/server/src/plugins/copilot/tools/doc-keyword-search.ts b/packages/backend/server/src/plugins/copilot/tools/doc-keyword-search.ts index eed855775a..81167ec7ad 100644 --- a/packages/backend/server/src/plugins/copilot/tools/doc-keyword-search.ts +++ b/packages/backend/server/src/plugins/copilot/tools/doc-keyword-search.ts @@ -4,6 +4,7 @@ import { z } from 'zod'; import type { AccessController } from '../../../core/permission'; import type { IndexerService, SearchDoc } from '../../indexer'; import type { CopilotChatOptions } from '../providers'; +import { toolError } from './error'; export const buildDocKeywordSearchGetter = ( ac: AccessController, @@ -56,8 +57,8 @@ export const createDocKeywordSearchTool = ( createdByUser: doc.createdByUser, updatedByUser: doc.updatedByUser, })); - } catch { - return 'Failed to search documents.'; + } catch (e: any) { + return toolError('Doc Keyword Search Failed', e.message); } }, }); diff --git a/packages/backend/server/src/plugins/copilot/tools/doc-semantic-search.ts b/packages/backend/server/src/plugins/copilot/tools/doc-semantic-search.ts index f14aa7fc81..82fd6e9c64 100644 --- a/packages/backend/server/src/plugins/copilot/tools/doc-semantic-search.ts +++ b/packages/backend/server/src/plugins/copilot/tools/doc-semantic-search.ts @@ -5,6 +5,7 @@ import type { AccessController } from '../../../core/permission'; import type { ChunkSimilarity } from '../../../models'; import type { CopilotContextService } from '../context'; import type { CopilotChatOptions } from '../providers'; +import { toolError } from './error'; export const buildDocSearchGetter = ( ac: AccessController, @@ -46,8 +47,8 @@ export const createDocSemanticSearchTool = ( execute: async ({ query }) => { try { return await searchDocs(query); - } catch { - return 'Failed to search documents.'; + } catch (e: any) { + return toolError('Doc Semantic Search Failed', e.message); } }, }); diff --git a/packages/backend/server/src/plugins/copilot/tools/error.ts b/packages/backend/server/src/plugins/copilot/tools/error.ts new file mode 100644 index 0000000000..ac795ba19f --- /dev/null +++ b/packages/backend/server/src/plugins/copilot/tools/error.ts @@ -0,0 +1,11 @@ +export interface ToolError { + type: 'error'; + name: string; + message: string; +} + +export const toolError = (name: string, message: string): ToolError => ({ + type: 'error', + name, + message, +}); diff --git a/packages/backend/server/src/plugins/copilot/tools/exa-crawl.ts b/packages/backend/server/src/plugins/copilot/tools/exa-crawl.ts new file mode 100644 index 0000000000..4c6af9debf --- /dev/null +++ b/packages/backend/server/src/plugins/copilot/tools/exa-crawl.ts @@ -0,0 +1,39 @@ +import { tool } from 'ai'; +import Exa from 'exa-js'; +import { z } from 'zod'; + +import { Config } from '../../../base'; +import { toolError } from './error'; + +export const createExaCrawlTool = (config: Config) => { + return tool({ + description: 'Crawl the web url for information', + parameters: z.object({ + url: z + .string() + .describe('The URL to crawl (including http:// or https://)'), + }), + execute: async ({ url }) => { + try { + const { key } = config.copilot.exa; + const exa = new Exa(key); + const result = await exa.getContents([url], { + livecrawl: 'always', + text: { + maxCharacters: 100000, + }, + }); + return result.results.map(data => ({ + title: data.title, + url: data.url, + content: data.text, + favicon: data.favicon, + publishedDate: data.publishedDate, + author: data.author, + })); + } catch (e: any) { + return toolError('Exa Crawl Failed', e.message); + } + }, + }); +}; diff --git a/packages/backend/server/src/plugins/copilot/tools/web-search.ts b/packages/backend/server/src/plugins/copilot/tools/exa-search.ts similarity index 52% rename from packages/backend/server/src/plugins/copilot/tools/web-search.ts rename to packages/backend/server/src/plugins/copilot/tools/exa-search.ts index fc2d9abea1..366c99798c 100644 --- a/packages/backend/server/src/plugins/copilot/tools/web-search.ts +++ b/packages/backend/server/src/plugins/copilot/tools/exa-search.ts @@ -3,6 +3,7 @@ import Exa from 'exa-js'; import { z } from 'zod'; import { Config } from '../../../base'; +import { toolError } from './error'; export const createExaSearchTool = (config: Config) => { return tool({ @@ -30,41 +31,8 @@ export const createExaSearchTool = (config: Config) => { publishedDate: data.publishedDate, author: data.author, })); - } catch { - return 'Failed to search the web'; - } - }, - }); -}; - -export const createExaCrawlTool = (config: Config) => { - return tool({ - description: 'Crawl the web url for information', - parameters: z.object({ - url: z - .string() - .describe('The URL to crawl (including http:// or https://)'), - }), - execute: async ({ url }) => { - try { - const { key } = config.copilot.exa; - const exa = new Exa(key); - const result = await exa.getContents([url], { - livecrawl: 'always', - text: { - maxCharacters: 100000, - }, - }); - return result.results.map(data => ({ - title: data.title, - url: data.url, - content: data.text, - favicon: data.favicon, - publishedDate: data.publishedDate, - author: data.author, - })); - } catch { - return 'Failed to crawl the web url'; + } catch (e: any) { + return toolError('Exa Search Failed', e.message); } }, }); diff --git a/packages/backend/server/src/plugins/copilot/tools/index.ts b/packages/backend/server/src/plugins/copilot/tools/index.ts index d58e342022..76f851f84c 100644 --- a/packages/backend/server/src/plugins/copilot/tools/index.ts +++ b/packages/backend/server/src/plugins/copilot/tools/index.ts @@ -1,3 +1,5 @@ export * from './doc-keyword-search'; export * from './doc-semantic-search'; -export * from './web-search'; +export * from './error'; +export * from './exa-crawl'; +export * from './exa-search'; diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/stream-objects.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/stream-objects.ts index d72576c88c..0e9a277897 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/stream-objects.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/stream-objects.ts @@ -1,6 +1,5 @@ import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { WithDisposable } from '@blocksuite/affine/global/lit'; -import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; import type { EditorHost } from '@blocksuite/affine/std'; import { ShadowlessElement } from '@blocksuite/affine/std'; import type { ExtensionType } from '@blocksuite/affine/store'; @@ -21,13 +20,6 @@ export class ChatContentStreamObjects extends WithDisposable( border-radius: 8px; background-color: rgba(0, 0, 0, 0.05); } - - .tool-wrapper { - padding: 12px; - margin: 8px 0; - border-radius: 8px; - border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')}; - } `; @property({ attribute: false }) @@ -70,12 +62,16 @@ export class ChatContentStreamObjects extends WithDisposable( .width=${this.width} > `; - default: + default: { + const name = streamObject.toolName + ' tool calling'; return html` -
- ${streamObject.toolName} tool calling... -
+ `; + } } } @@ -101,12 +97,16 @@ export class ChatContentStreamObjects extends WithDisposable( .width=${this.width} > `; - default: + default: { + const name = streamObject.toolName + ' tool result'; return html` -
- ${streamObject.toolName} tool result... -
+ `; + } } } diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/tool-call-card.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/tool-call-card.ts index 8d31cc0120..aeeedbf53a 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/tool-call-card.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/tool-call-card.ts @@ -1,6 +1,7 @@ import { WithDisposable } from '@blocksuite/affine/global/lit'; import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; import { ShadowlessElement } from '@blocksuite/affine/std'; +import { ToolIcon } from '@blocksuite/icons/lit'; import { css, html, type TemplateResult } from 'lit'; import { property, state } from 'lit/decorators.js'; @@ -51,10 +52,10 @@ export class ToolCallCard extends WithDisposable(ShadowlessElement) { `; @property({ attribute: false }) - accessor name!: string; + accessor name: string = 'Tool calling'; @property({ attribute: false }) - accessor icon!: TemplateResult<1>; + accessor icon: TemplateResult<1> = ToolIcon(); @state() private accessor dotsText = '.'; diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/tool-failed-card.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/tool-failed-card.ts new file mode 100644 index 0000000000..c451fbe223 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/tool-failed-card.ts @@ -0,0 +1,64 @@ +import { WithDisposable } from '@blocksuite/affine/global/lit'; +import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; +import { ShadowlessElement } from '@blocksuite/affine/std'; +import { ToolIcon } from '@blocksuite/icons/lit'; +import { css, html, type TemplateResult } from 'lit'; +import { property } from 'lit/decorators.js'; + +export class ToolFailedCard extends WithDisposable(ShadowlessElement) { + static override styles = css` + .ai-tool-failed-wrapper { + padding: 12px; + margin: 8px 0; + border-radius: 8px; + border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')}; + + .ai-tool-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + } + + .ai-icon { + width: 24px; + height: 24px; + + svg { + width: 24px; + height: 24px; + color: ${unsafeCSSVarV2('button/error')}; + } + } + + .ai-error-name { + font-size: 14px; + font-weight: 500; + line-height: 24px; + margin-left: 0px; + margin-right: auto; + color: ${unsafeCSSVarV2('button/error')}; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + `; + + @property({ attribute: false }) + accessor name: string = 'Tool calling failed'; + + @property({ attribute: false }) + accessor icon: TemplateResult<1> = ToolIcon(); + + protected override render() { + return html` +
+
+
${this.icon}
+
${this.name}
+
+
+ `; + } +} diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/tool-result-card.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/tool-result-card.ts index 7ef3f49e58..76abaa745a 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/tool-result-card.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/tool-result-card.ts @@ -2,7 +2,7 @@ import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; import { ImageProxyService } from '@blocksuite/affine/shared/adapters'; import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; import { type EditorHost, ShadowlessElement } from '@blocksuite/affine/std'; -import { ToggleDownIcon } from '@blocksuite/icons/lit'; +import { ToggleDownIcon, ToolIcon } from '@blocksuite/icons/lit'; import { type Signal } from '@preact/signals-core'; import { css, html, nothing, type TemplateResult } from 'lit'; import { property, state } from 'lit/decorators.js'; @@ -17,7 +17,7 @@ export class ToolResultCard extends SignalWatcher( WithDisposable(ShadowlessElement) ) { static override styles = css` - .ai-tool-wrapper { + .ai-tool-result-wrapper { padding: 12px; margin: 8px 0; border-radius: 8px; @@ -158,16 +158,16 @@ export class ToolResultCard extends SignalWatcher( accessor host!: EditorHost; @property({ attribute: false }) - accessor name!: string; + accessor name: string = 'Tool result'; @property({ attribute: false }) - accessor icon!: TemplateResult<1> | string; + accessor icon: TemplateResult<1> | string = ToolIcon(); @property({ attribute: false }) accessor footerIcons: TemplateResult<1>[] | string[] = []; @property({ attribute: false }) - accessor results!: ToolResult[]; + accessor results: ToolResult[] = []; @property({ attribute: false }) accessor width: Signal | undefined; @@ -177,7 +177,7 @@ export class ToolResultCard extends SignalWatcher( protected override render() { return html` -
+
${this.renderIcon(this.icon)}
${this.name}
diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/type.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/type.ts new file mode 100644 index 0000000000..f8fb01fada --- /dev/null +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/type.ts @@ -0,0 +1,5 @@ +export interface ToolError { + type: 'error'; + name: string; + message: string; +} diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/web-crawl.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/web-crawl.ts index 7ba9d4a55f..5bff3c96f6 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/web-crawl.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/web-crawl.ts @@ -5,6 +5,8 @@ import type { Signal } from '@preact/signals-core'; import { html, nothing } from 'lit'; import { property } from 'lit/decorators.js'; +import type { ToolError } from './type'; + interface WebCrawlToolCall { type: 'tool-call'; toolCallId: string; @@ -17,14 +19,17 @@ interface WebCrawlToolResult { toolCallId: string; toolName: string; args: { url: string }; - result: Array<{ - title: string; - url: string; - content: string; - favicon: string; - publishedDate: string; - author: string; - }>; + result: + | Array<{ + title: string; + url: string; + content: string; + favicon: string; + publishedDate: string; + author: string; + }> + | ToolError + | null; } export class WebCrawlTool extends WithDisposable(ShadowlessElement) { @@ -51,23 +56,32 @@ export class WebCrawlTool extends WithDisposable(ShadowlessElement) { return nothing; } - const { favicon, title, content } = this.data.result[0]; + const result = this.data.result; + if (result && Array.isArray(result)) { + const { favicon, title, content } = result[0]; + return html` + + `; + } return html` - + > `; } diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/web-search.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/web-search.ts index 754c05faff..0e4318af28 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/web-search.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/web-search.ts @@ -5,6 +5,8 @@ import type { Signal } from '@preact/signals-core'; import { html, nothing } from 'lit'; import { property } from 'lit/decorators.js'; +import type { ToolError } from './type'; + interface WebSearchToolCall { type: 'tool-call'; toolCallId: string; @@ -17,14 +19,17 @@ interface WebSearchToolResult { toolCallId: string; toolName: string; args: { url: string }; - result: Array<{ - title: string; - url: string; - content: string; - favicon: string; - publishedDate: string; - author: string; - }>; + result: + | Array<{ + title: string; + url: string; + content: string; + favicon: string; + publishedDate: string; + author: string; + }> + | ToolError + | null; } export class WebSearchTool extends WithDisposable(ShadowlessElement) { @@ -50,27 +55,35 @@ export class WebSearchTool extends WithDisposable(ShadowlessElement) { return nothing; } - const results = this.data.result.map(item => { - const { favicon, title, content } = item; - return { - title: title, - icon: favicon || WebIcon(), - content: content, - }; - }); - const footerIcons = this.data.result - .map(item => item.favicon) - .filter(Boolean); + const result = this.data.result; + if (result && Array.isArray(result)) { + const results = result.map(item => { + const { favicon, title, content } = item; + return { + title: title, + icon: favicon || WebIcon(), + content: content, + }; + }); + const footerIcons = result.map(item => item.favicon).filter(Boolean); + + return html` + + `; + } return html` - + > `; } diff --git a/packages/frontend/core/src/blocksuite/ai/effects.ts b/packages/frontend/core/src/blocksuite/ai/effects.ts index 058004bbab..633b6fa553 100644 --- a/packages/frontend/core/src/blocksuite/ai/effects.ts +++ b/packages/frontend/core/src/blocksuite/ai/effects.ts @@ -45,6 +45,7 @@ import { ChatContentRichText } from './components/ai-message-content/rich-text'; import { ChatContentStreamObjects } from './components/ai-message-content/stream-objects'; import { AIScrollableTextRenderer } from './components/ai-scrollable-text-renderer'; import { ToolCallCard } from './components/ai-tools/tool-call-card'; +import { ToolFailedCard } from './components/ai-tools/tool-failed-card'; import { ToolResultCard } from './components/ai-tools/tool-result-card'; import { WebCrawlTool } from './components/ai-tools/web-crawl'; import { WebSearchTool } from './components/ai-tools/web-search'; @@ -167,6 +168,7 @@ export function registerAIEffects() { customElements.define('tool-call-card', ToolCallCard); customElements.define('tool-result-card', ToolResultCard); + customElements.define('tool-call-failed', ToolFailedCard); customElements.define('web-crawl-tool', WebCrawlTool); customElements.define('web-search-tool', WebSearchTool);