mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-25 14:28:51 +08:00
feat(server): improve doc tools error handle (#14662)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Centralized sync/status messages for cloud document sync and explicit user-facing error types. * Frontend helpers to detect and display tool errors with friendly names. * **Bug Fixes** * Consistent, actionable error reporting for document and attachment reads instead of silent failures. * Search and semantic tools now validate workspace sync and permissions and return clear responses. * **Tests** * Added comprehensive tests covering document/blob reads, search tools, and sync/error paths. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
+19
-6
@@ -7,6 +7,8 @@ import { html, nothing } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import type { ToolResult } from './tool-result-card';
|
||||
import { getToolErrorDisplayName, isToolError } from './tool-result-utils';
|
||||
import type { ToolError } from './type';
|
||||
|
||||
interface DocKeywordSearchToolCall {
|
||||
type: 'tool-call';
|
||||
@@ -20,10 +22,7 @@ interface DocKeywordSearchToolResult {
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: { query: string };
|
||||
result: Array<{
|
||||
title: string;
|
||||
docId: string;
|
||||
}>;
|
||||
result: Array<{ title: string; docId: string }> | ToolError | null;
|
||||
}
|
||||
|
||||
export class DocKeywordSearchResult extends WithDisposable(ShadowlessElement) {
|
||||
@@ -51,9 +50,23 @@ export class DocKeywordSearchResult extends WithDisposable(ShadowlessElement) {
|
||||
if (this.data.type !== 'tool-result') {
|
||||
return nothing;
|
||||
}
|
||||
const result = this.data.result;
|
||||
if (!result || isToolError(result)) {
|
||||
return html`<tool-call-failed
|
||||
.name=${getToolErrorDisplayName(
|
||||
isToolError(result) ? result : null,
|
||||
'Document search failed',
|
||||
{
|
||||
'Workspace Sync Required':
|
||||
'Enable workspace sync to search documents',
|
||||
}
|
||||
)}
|
||||
.icon=${SearchIcon()}
|
||||
></tool-call-failed>`;
|
||||
}
|
||||
let results: ToolResult[] = [];
|
||||
try {
|
||||
results = this.data.result.map(item => ({
|
||||
results = result.map(item => ({
|
||||
title: item.title,
|
||||
icon: PageIcon(),
|
||||
onClick: () => {
|
||||
@@ -69,7 +82,7 @@ export class DocKeywordSearchResult extends WithDisposable(ShadowlessElement) {
|
||||
console.error('Failed to parse result', err);
|
||||
}
|
||||
return html`<tool-result-card
|
||||
.name=${`Found ${this.data.result.length} pages for "${this.data.args.query}"`}
|
||||
.name=${`Found ${result.length} pages for "${this.data.args.query}"`}
|
||||
.icon=${SearchIcon()}
|
||||
.width=${this.width}
|
||||
.results=${results}
|
||||
|
||||
@@ -6,6 +6,9 @@ import type { Signal } from '@preact/signals-core';
|
||||
import { html, nothing } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import { getToolErrorDisplayName, isToolError } from './tool-result-utils';
|
||||
import type { ToolError } from './type';
|
||||
|
||||
interface DocReadToolCall {
|
||||
type: 'tool-call';
|
||||
toolCallId: string;
|
||||
@@ -18,14 +21,24 @@ interface DocReadToolResult {
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: { doc_id: string };
|
||||
result: {
|
||||
/** Old result may not have docId */
|
||||
docId?: string;
|
||||
title: string;
|
||||
markdown: string;
|
||||
};
|
||||
result:
|
||||
| {
|
||||
/** Old result may not have docId */
|
||||
docId?: string;
|
||||
title: string;
|
||||
markdown: string;
|
||||
}
|
||||
| ToolError
|
||||
| null;
|
||||
}
|
||||
|
||||
const getFailedName = (result: ToolError | null) => {
|
||||
return getToolErrorDisplayName(result, 'Document read failed', {
|
||||
'Workspace Sync Required': 'Enable workspace sync to read this document',
|
||||
'Document Sync Pending': 'Wait for document sync to finish',
|
||||
});
|
||||
};
|
||||
|
||||
export class DocReadResult extends WithDisposable(ShadowlessElement) {
|
||||
@property({ attribute: false })
|
||||
accessor data!: DocReadToolCall | DocReadToolResult;
|
||||
@@ -49,18 +62,25 @@ export class DocReadResult extends WithDisposable(ShadowlessElement) {
|
||||
if (this.data.type !== 'tool-result') {
|
||||
return nothing;
|
||||
}
|
||||
const result = this.data.result;
|
||||
if (!result || isToolError(result)) {
|
||||
return html`<tool-call-failed
|
||||
.name=${getFailedName(isToolError(result) ? result : null)}
|
||||
.icon=${ViewIcon()}
|
||||
></tool-call-failed>`;
|
||||
}
|
||||
// TODO: better markdown rendering
|
||||
return html`<tool-result-card
|
||||
.name=${`Read "${this.data.result.title}"`}
|
||||
.name=${`Read "${result.title}"`}
|
||||
.icon=${ViewIcon()}
|
||||
.width=${this.width}
|
||||
.results=${[
|
||||
{
|
||||
title: this.data.result.title,
|
||||
title: result.title,
|
||||
icon: PageIcon(),
|
||||
content: this.data.result.markdown,
|
||||
content: result.markdown,
|
||||
onClick: () => {
|
||||
const docId = (this.data as DocReadToolResult).result.docId;
|
||||
const docId = result.docId;
|
||||
if (!docId) {
|
||||
return;
|
||||
}
|
||||
|
||||
+18
-5
@@ -7,6 +7,8 @@ import { html, nothing } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import type { DocDisplayConfig } from '../ai-chat-chips';
|
||||
import { getToolErrorDisplayName, isToolError } from './tool-result-utils';
|
||||
import type { ToolError } from './type';
|
||||
|
||||
interface DocSemanticSearchToolCall {
|
||||
type: 'tool-call';
|
||||
@@ -20,10 +22,7 @@ interface DocSemanticSearchToolResult {
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: { query: string };
|
||||
result: Array<{
|
||||
content: string;
|
||||
docId: string;
|
||||
}>;
|
||||
result: Array<{ content: string; docId: string }> | ToolError | null;
|
||||
}
|
||||
|
||||
function parseResultContent(content: string) {
|
||||
@@ -82,11 +81,25 @@ export class DocSemanticSearchResult extends WithDisposable(ShadowlessElement) {
|
||||
if (this.data.type !== 'tool-result') {
|
||||
return nothing;
|
||||
}
|
||||
const result = this.data.result;
|
||||
if (!result || isToolError(result)) {
|
||||
return html`<tool-call-failed
|
||||
.name=${getToolErrorDisplayName(
|
||||
isToolError(result) ? result : null,
|
||||
'Semantic search failed',
|
||||
{
|
||||
'Workspace Sync Required':
|
||||
'Enable workspace sync to search documents',
|
||||
}
|
||||
)}
|
||||
.icon=${AiEmbeddingIcon()}
|
||||
></tool-call-failed>`;
|
||||
}
|
||||
return html`<tool-result-card
|
||||
.name=${`Found semantically related pages for "${this.data.args.query}"`}
|
||||
.icon=${AiEmbeddingIcon()}
|
||||
.width=${this.width}
|
||||
.results=${this.data.result
|
||||
.results=${result
|
||||
.map(result => ({
|
||||
...parseResultContent(result.content),
|
||||
title: this.docDisplayService.getTitle(result.docId),
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ToolError } from './type';
|
||||
|
||||
export const isToolError = (result: unknown): result is ToolError =>
|
||||
!!result &&
|
||||
typeof result === 'object' &&
|
||||
'type' in result &&
|
||||
(result as ToolError).type === 'error';
|
||||
|
||||
export const getToolErrorDisplayName = (
|
||||
result: ToolError | null,
|
||||
fallback: string,
|
||||
overrides: Record<string, string> = {}
|
||||
) => {
|
||||
if (!result) return fallback;
|
||||
return overrides[result.name] ?? result.name;
|
||||
};
|
||||
Reference in New Issue
Block a user