From 5599c39e97f2a671f9b3401b8acc3f2f885b235c Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Mon, 30 Jun 2025 11:37:34 +0800 Subject: [PATCH] feat(server): add read doc tool (#12811) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit close AI-186 #### PR Dependency Tree * **PR #12811** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) ## Summary by CodeRabbit - **New Features** - Added a new tool enabling users to read document content and metadata within a workspace, with enforced access control. - **Improvements** - Updated tool interfaces and outputs to support the document reading functionality seamlessly. --- .../src/plugins/copilot/providers/provider.ts | 11 +++ .../src/plugins/copilot/providers/utils.ts | 6 ++ .../src/plugins/copilot/tools/doc-read.ts | 88 +++++++++++++++++++ .../server/src/plugins/copilot/tools/index.ts | 1 + 4 files changed, 106 insertions(+) create mode 100644 packages/backend/server/src/plugins/copilot/tools/doc-read.ts diff --git a/packages/backend/server/src/plugins/copilot/providers/provider.ts b/packages/backend/server/src/plugins/copilot/providers/provider.ts index 4f0b77bb6..213fbe317 100644 --- a/packages/backend/server/src/plugins/copilot/providers/provider.ts +++ b/packages/backend/server/src/plugins/copilot/providers/provider.ts @@ -11,14 +11,17 @@ import { } from '../../../base'; import { DocReader } from '../../../core/doc'; import { AccessController } from '../../../core/permission'; +import { Models } from '../../../models'; import { IndexerService } from '../../indexer'; import { CopilotContextService } from '../context'; import { buildContentGetter, + buildDocContentGetter, buildDocKeywordSearchGetter, buildDocSearchGetter, createDocEditTool, createDocKeywordSearchTool, + createDocReadTool, createDocSemanticSearchTool, createExaCrawlTool, createExaSearchTool, @@ -182,6 +185,14 @@ export abstract class CopilotProvider { } break; } + case 'docRead': { + const ac = this.moduleRef.get(AccessController, { strict: false }); + const models = this.moduleRef.get(Models, { strict: false }); + const docReader = this.moduleRef.get(DocReader, { strict: false }); + const getDoc = buildDocContentGetter(ac, docReader, models); + tools.doc_read = createDocReadTool(getDoc.bind(null, options)); + break; + } case 'webSearch': { tools.web_search_exa = createExaSearchTool(this.AFFiNEConfig); tools.web_crawl_exa = createExaCrawlTool(this.AFFiNEConfig); diff --git a/packages/backend/server/src/plugins/copilot/providers/utils.ts b/packages/backend/server/src/plugins/copilot/providers/utils.ts index 36e98635d..602271dbd 100644 --- a/packages/backend/server/src/plugins/copilot/providers/utils.ts +++ b/packages/backend/server/src/plugins/copilot/providers/utils.ts @@ -13,6 +13,7 @@ import { ZodType } from 'zod'; import { createDocEditTool, createDocKeywordSearchTool, + createDocReadTool, createDocSemanticSearchTool, createExaCrawlTool, createExaSearchTool, @@ -386,6 +387,7 @@ export interface CustomAITools extends ToolSet { doc_edit: ReturnType; doc_semantic_search: ReturnType; doc_keyword_search: ReturnType; + doc_read: ReturnType; web_search_exa: ReturnType; web_crawl_exa: ReturnType; } @@ -451,6 +453,10 @@ export class TextStreamParser { result += `\nSearching the keyword "${chunk.args.query}"\n`; break; } + case 'doc_read': { + result += `\nReading the doc "${chunk.args.doc_id}"\n`; + break; + } } result = this.markAsCallout(result); break; diff --git a/packages/backend/server/src/plugins/copilot/tools/doc-read.ts b/packages/backend/server/src/plugins/copilot/tools/doc-read.ts new file mode 100644 index 000000000..aa6411b73 --- /dev/null +++ b/packages/backend/server/src/plugins/copilot/tools/doc-read.ts @@ -0,0 +1,88 @@ +import { Logger } from '@nestjs/common'; +import { tool } from 'ai'; +import { z } from 'zod'; + +import { DocReader } from '../../../core/doc'; +import { AccessController } from '../../../core/permission'; +import { Models, publicUserSelect } from '../../../models'; +import type { CopilotChatOptions } from '../providers'; +import { toolError } from './error'; + +const logger = new Logger('DocReadTool'); + +export const buildDocContentGetter = ( + ac: AccessController, + docReader: DocReader, + models: Models +) => { + const getDoc = async (options: CopilotChatOptions, docId?: string) => { + if (!options?.user || !options?.workspace || !docId) { + return; + } + const canAccess = await ac + .user(options.user) + .workspace(options.workspace) + .doc(docId) + .can('Doc.Read'); + if (!canAccess) { + logger.warn( + `User ${options.user} does not have access to doc ${docId} in workspace ${options.workspace}` + ); + return; + } + + const docMeta = await models.doc.getSnapshot(options.workspace, docId, { + select: { + createdAt: true, + updatedAt: true, + createdByUser: { + select: publicUserSelect, + }, + updatedByUser: { + select: publicUserSelect, + }, + }, + }); + if (!docMeta) { + return; + } + + const content = await docReader.getDocMarkdown(options.workspace, docId); + if (!content) { + return; + } + + return { + title: content.title, + markdown: content.markdown, + createdAt: docMeta.createdAt, + updatedAt: docMeta.updatedAt, + createdByUser: docMeta.createdByUser, + updatedByUser: docMeta.updatedByUser, + }; + }; + return getDoc; +}; + +export const createDocReadTool = ( + getDoc: (targetId?: string) => Promise +) => { + return tool({ + description: 'Read the content of a doc in the current workspace', + parameters: z.object({ + doc_id: z.string().describe('The target doc to read'), + }), + execute: async ({ doc_id }) => { + try { + const doc = await getDoc(doc_id); + if (!doc) { + return; + } + return { ...doc }; + } catch (err: any) { + logger.error(`Failed to read the doc ${doc_id}`, err); + return toolError('Doc Read Failed', err.message); + } + }, + }); +}; diff --git a/packages/backend/server/src/plugins/copilot/tools/index.ts b/packages/backend/server/src/plugins/copilot/tools/index.ts index 8ac1c6536..d4f8f339a 100644 --- a/packages/backend/server/src/plugins/copilot/tools/index.ts +++ b/packages/backend/server/src/plugins/copilot/tools/index.ts @@ -1,5 +1,6 @@ export * from './doc-edit'; export * from './doc-keyword-search'; +export * from './doc-read'; export * from './doc-semantic-search'; export * from './error'; export * from './exa-crawl';