mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
✨ feat(agent-documents): add listDocuments, readDocumentByFilename, upsertDocumentByFilename APIs
This commit is contained in:
@@ -4,10 +4,13 @@ import type {
|
||||
CopyDocumentArgs,
|
||||
CreateDocumentArgs,
|
||||
EditDocumentArgs,
|
||||
ListDocumentsArgs,
|
||||
ReadDocumentArgs,
|
||||
ReadDocumentByFilenameArgs,
|
||||
RemoveDocumentArgs,
|
||||
RenameDocumentArgs,
|
||||
UpdateLoadRuleArgs,
|
||||
UpsertDocumentByFilenameArgs,
|
||||
} from '../types';
|
||||
|
||||
interface AgentDocumentRecord {
|
||||
@@ -36,11 +39,21 @@ export interface AgentDocumentsRuntimeService {
|
||||
agentId: string;
|
||||
},
|
||||
) => Promise<AgentDocumentRecord | undefined>;
|
||||
listDocuments: (
|
||||
params: ListDocumentsArgs & {
|
||||
agentId: string;
|
||||
},
|
||||
) => Promise<AgentDocumentRecord[]>;
|
||||
readDocument: (
|
||||
params: ReadDocumentArgs & {
|
||||
agentId: string;
|
||||
},
|
||||
) => Promise<AgentDocumentRecord | undefined>;
|
||||
readDocumentByFilename: (
|
||||
params: ReadDocumentByFilenameArgs & {
|
||||
agentId: string;
|
||||
},
|
||||
) => Promise<AgentDocumentRecord | undefined>;
|
||||
removeDocument: (
|
||||
params: RemoveDocumentArgs & {
|
||||
agentId: string;
|
||||
@@ -56,6 +69,11 @@ export interface AgentDocumentsRuntimeService {
|
||||
agentId: string;
|
||||
},
|
||||
) => Promise<AgentDocumentRecord | undefined>;
|
||||
upsertDocumentByFilename: (
|
||||
params: UpsertDocumentByFilenameArgs & {
|
||||
agentId: string;
|
||||
},
|
||||
) => Promise<AgentDocumentRecord | undefined>;
|
||||
}
|
||||
|
||||
export class AgentDocumentsExecutionRuntime {
|
||||
@@ -66,6 +84,72 @@ export class AgentDocumentsExecutionRuntime {
|
||||
return context.agentId;
|
||||
}
|
||||
|
||||
async listDocuments(
|
||||
_args: ListDocumentsArgs,
|
||||
context?: AgentDocumentOperationContext,
|
||||
): Promise<BuiltinServerRuntimeOutput> {
|
||||
const agentId = this.resolveAgentId(context);
|
||||
if (!agentId) {
|
||||
return {
|
||||
content: 'Cannot list agent documents without agentId context.',
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
const docs = await this.service.listDocuments({ agentId });
|
||||
const list = docs.map((d) => ({ filename: d.title, id: d.id, title: d.title }));
|
||||
|
||||
return {
|
||||
content: JSON.stringify(list),
|
||||
state: { documents: list },
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
async readDocumentByFilename(
|
||||
args: ReadDocumentByFilenameArgs,
|
||||
context?: AgentDocumentOperationContext,
|
||||
): Promise<BuiltinServerRuntimeOutput> {
|
||||
const agentId = this.resolveAgentId(context);
|
||||
if (!agentId) {
|
||||
return {
|
||||
content: 'Cannot read agent document without agentId context.',
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
const doc = await this.service.readDocumentByFilename({ ...args, agentId });
|
||||
if (!doc) return { content: `Document not found: ${args.filename}`, success: false };
|
||||
|
||||
return {
|
||||
content: doc.content || '',
|
||||
state: { content: doc.content, filename: args.filename, id: doc.id, title: doc.title },
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
async upsertDocumentByFilename(
|
||||
args: UpsertDocumentByFilenameArgs,
|
||||
context?: AgentDocumentOperationContext,
|
||||
): Promise<BuiltinServerRuntimeOutput> {
|
||||
const agentId = this.resolveAgentId(context);
|
||||
if (!agentId) {
|
||||
return {
|
||||
content: 'Cannot upsert agent document without agentId context.',
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
const doc = await this.service.upsertDocumentByFilename({ ...args, agentId });
|
||||
if (!doc) return { content: `Failed to upsert document: ${args.filename}`, success: false };
|
||||
|
||||
return {
|
||||
content: `Upserted document "${args.filename}" (${doc.id}).`,
|
||||
state: { filename: args.filename, id: doc.id },
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
async createDocument(
|
||||
args: CreateDocumentArgs,
|
||||
context?: AgentDocumentOperationContext,
|
||||
|
||||
@@ -7,10 +7,13 @@ import {
|
||||
type CopyDocumentArgs,
|
||||
type CreateDocumentArgs,
|
||||
type EditDocumentArgs,
|
||||
type ListDocumentsArgs,
|
||||
type ReadDocumentArgs,
|
||||
type ReadDocumentByFilenameArgs,
|
||||
type RemoveDocumentArgs,
|
||||
type RenameDocumentArgs,
|
||||
type UpdateLoadRuleArgs,
|
||||
type UpsertDocumentByFilenameArgs,
|
||||
} from '../types';
|
||||
|
||||
export class AgentDocumentsExecutor extends BaseExecutor<typeof AgentDocumentsApiName> {
|
||||
@@ -24,6 +27,27 @@ export class AgentDocumentsExecutor extends BaseExecutor<typeof AgentDocumentsAp
|
||||
this.runtime = runtime;
|
||||
}
|
||||
|
||||
listDocuments = async (
|
||||
params: ListDocumentsArgs,
|
||||
ctx: BuiltinToolContext,
|
||||
): Promise<BuiltinToolResult> => {
|
||||
return this.runtime.listDocuments(params, { agentId: ctx.agentId });
|
||||
};
|
||||
|
||||
readDocumentByFilename = async (
|
||||
params: ReadDocumentByFilenameArgs,
|
||||
ctx: BuiltinToolContext,
|
||||
): Promise<BuiltinToolResult> => {
|
||||
return this.runtime.readDocumentByFilename(params, { agentId: ctx.agentId });
|
||||
};
|
||||
|
||||
upsertDocumentByFilename = async (
|
||||
params: UpsertDocumentByFilenameArgs,
|
||||
ctx: BuiltinToolContext,
|
||||
): Promise<BuiltinToolResult> => {
|
||||
return this.runtime.upsertDocumentByFilename(params, { agentId: ctx.agentId });
|
||||
};
|
||||
|
||||
createDocument = async (
|
||||
params: CreateDocumentArgs,
|
||||
ctx: BuiltinToolContext,
|
||||
@@ -78,10 +102,13 @@ const fallbackRuntime = new AgentDocumentsExecutionRuntime({
|
||||
copyDocument: async ({ agentId: _agentId }) => undefined,
|
||||
createDocument: async () => undefined,
|
||||
editDocument: async ({ agentId: _agentId }) => undefined,
|
||||
listDocuments: async () => [],
|
||||
readDocument: async ({ agentId: _agentId }) => undefined,
|
||||
readDocumentByFilename: async () => undefined,
|
||||
removeDocument: async ({ agentId: _agentId }) => false,
|
||||
renameDocument: async ({ agentId: _agentId }) => undefined,
|
||||
updateLoadRule: async ({ agentId: _agentId }) => undefined,
|
||||
upsertDocumentByFilename: async () => undefined,
|
||||
});
|
||||
|
||||
export const agentDocumentsExecutor = new AgentDocumentsExecutor(fallbackRuntime);
|
||||
|
||||
@@ -10,7 +10,11 @@ export {
|
||||
type CreateDocumentState,
|
||||
type EditDocumentArgs,
|
||||
type EditDocumentState,
|
||||
type ListDocumentsArgs,
|
||||
type ListDocumentsState,
|
||||
type ReadDocumentArgs,
|
||||
type ReadDocumentByFilenameArgs,
|
||||
type ReadDocumentByFilenameState,
|
||||
type ReadDocumentState,
|
||||
type RemoveDocumentArgs,
|
||||
type RemoveDocumentState,
|
||||
@@ -18,4 +22,6 @@ export {
|
||||
type RenameDocumentState,
|
||||
type UpdateLoadRuleArgs,
|
||||
type UpdateLoadRuleState,
|
||||
type UpsertDocumentByFilenameArgs,
|
||||
type UpsertDocumentByFilenameState,
|
||||
} from './types';
|
||||
|
||||
@@ -109,6 +109,50 @@ export const AgentDocumentsManifest: BuiltinToolManifest = {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
{
|
||||
description:
|
||||
'List all agent documents. Returns document id, filename, and title for each document.',
|
||||
name: AgentDocumentsApiName.listDocuments,
|
||||
parameters: {
|
||||
properties: {},
|
||||
required: [],
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Read an existing agent document by its filename (similar intent to cat by filename). Use when you know the filename but not the id.',
|
||||
name: AgentDocumentsApiName.readDocumentByFilename,
|
||||
parameters: {
|
||||
properties: {
|
||||
filename: {
|
||||
description: 'Target document filename.',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['filename'],
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Create or update an agent document by filename. If a document with the given filename exists, its content is updated; otherwise a new document is created.',
|
||||
name: AgentDocumentsApiName.upsertDocumentByFilename,
|
||||
parameters: {
|
||||
properties: {
|
||||
content: {
|
||||
description: 'Document content in markdown or plain text.',
|
||||
type: 'string',
|
||||
},
|
||||
filename: {
|
||||
description: 'Target document filename.',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['filename', 'content'],
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Update agent-document load rules. Use this to control how documents are loaded into runtime context.',
|
||||
@@ -145,7 +189,7 @@ export const AgentDocumentsManifest: BuiltinToolManifest = {
|
||||
meta: {
|
||||
avatar: '🗂️',
|
||||
description:
|
||||
'Manage agent-scoped documents (create/read/edit/remove/rename/copy) and load rules',
|
||||
'Manage agent-scoped documents (list/create/read/edit/remove/rename/copy/upsert) and load rules',
|
||||
title: 'Documents',
|
||||
},
|
||||
systemRole: systemPrompt,
|
||||
|
||||
@@ -4,10 +4,13 @@ export const AgentDocumentsApiName = {
|
||||
createDocument: 'createDocument',
|
||||
copyDocument: 'copyDocument',
|
||||
editDocument: 'editDocument',
|
||||
listDocuments: 'listDocuments',
|
||||
readDocument: 'readDocument',
|
||||
readDocumentByFilename: 'readDocumentByFilename',
|
||||
removeDocument: 'removeDocument',
|
||||
renameDocument: 'renameDocument',
|
||||
updateLoadRule: 'updateLoadRule',
|
||||
upsertDocumentByFilename: 'upsertDocumentByFilename',
|
||||
} as const;
|
||||
|
||||
export interface CreateDocumentArgs {
|
||||
@@ -103,3 +106,31 @@ export interface AgentDocumentReference {
|
||||
id: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface ListDocumentsArgs {}
|
||||
|
||||
export interface ListDocumentsState {
|
||||
documents: { filename: string; id: string; title?: string }[];
|
||||
}
|
||||
|
||||
export interface ReadDocumentByFilenameArgs {
|
||||
filename: string;
|
||||
}
|
||||
|
||||
export interface ReadDocumentByFilenameState {
|
||||
content?: string;
|
||||
filename: string;
|
||||
id: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface UpsertDocumentByFilenameArgs {
|
||||
content: string;
|
||||
filename: string;
|
||||
}
|
||||
|
||||
export interface UpsertDocumentByFilenameState {
|
||||
created: boolean;
|
||||
filename: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
@@ -133,10 +133,13 @@ export default {
|
||||
'builtins.lobe-agent-documents.apiName.copyDocument': 'Copy document',
|
||||
'builtins.lobe-agent-documents.apiName.createDocument': 'Create document',
|
||||
'builtins.lobe-agent-documents.apiName.editDocument': 'Edit document',
|
||||
'builtins.lobe-agent-documents.apiName.listDocuments': 'List documents',
|
||||
'builtins.lobe-agent-documents.apiName.readDocument': 'Read document',
|
||||
'builtins.lobe-agent-documents.apiName.readDocumentByFilename': 'Read document by filename',
|
||||
'builtins.lobe-agent-documents.apiName.removeDocument': 'Remove document',
|
||||
'builtins.lobe-agent-documents.apiName.renameDocument': 'Rename document',
|
||||
'builtins.lobe-agent-documents.apiName.updateLoadRule': 'Update load rule',
|
||||
'builtins.lobe-agent-documents.apiName.upsertDocumentByFilename': 'Upsert document by filename',
|
||||
'builtins.lobe-agent-documents.title': 'Agent Documents',
|
||||
'builtins.lobe-notebook.actions.collapse': 'Collapse',
|
||||
'builtins.lobe-notebook.actions.copy': 'Copy',
|
||||
|
||||
@@ -184,6 +184,48 @@ export const agentDocumentRouter = router({
|
||||
return ctx.agentDocumentService.hasDocuments(input.agentId);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Tool-oriented: list documents for an agent
|
||||
*/
|
||||
listDocuments: agentDocumentProcedure
|
||||
.input(z.object({ agentId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
return ctx.agentDocumentService.listDocuments(input.agentId);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Tool-oriented: read document by filename
|
||||
*/
|
||||
readDocumentByFilename: agentDocumentProcedure
|
||||
.input(
|
||||
z.object({
|
||||
agentId: z.string(),
|
||||
filename: z.string(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
return ctx.agentDocumentService.getDocumentByFilename(input.agentId, input.filename);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Tool-oriented: upsert document by filename
|
||||
*/
|
||||
upsertDocumentByFilename: agentDocumentProcedure
|
||||
.input(
|
||||
z.object({
|
||||
agentId: z.string(),
|
||||
content: z.string(),
|
||||
filename: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return ctx.agentDocumentService.upsertDocumentByFilename({
|
||||
agentId: input.agentId,
|
||||
content: input.content,
|
||||
filename: input.filename,
|
||||
});
|
||||
}),
|
||||
|
||||
/**
|
||||
* Tool-oriented: create document
|
||||
*/
|
||||
|
||||
@@ -20,8 +20,10 @@ describe('AgentDocumentsService', () => {
|
||||
|
||||
const mockModel = {
|
||||
create: vi.fn(),
|
||||
findByAgent: vi.fn(),
|
||||
findByFilename: vi.fn(),
|
||||
hasByAgent: vi.fn(),
|
||||
upsert: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -67,6 +69,71 @@ describe('AgentDocumentsService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('listDocuments', () => {
|
||||
it('should return a list of documents with filename, id, and title', async () => {
|
||||
mockModel.findByAgent.mockResolvedValue([
|
||||
{ content: 'c1', filename: 'a.md', id: 'doc-1', policy: null, title: 'A' },
|
||||
{ content: 'c2', filename: 'b.md', id: 'doc-2', policy: null, title: 'B' },
|
||||
]);
|
||||
|
||||
const service = new AgentDocumentsService(db, userId);
|
||||
const result = await service.listDocuments('agent-1');
|
||||
|
||||
expect(mockModel.findByAgent).toHaveBeenCalledWith('agent-1');
|
||||
expect(result).toEqual([
|
||||
{ filename: 'a.md', id: 'doc-1', loadPosition: undefined, title: 'A' },
|
||||
{ filename: 'b.md', id: 'doc-2', loadPosition: undefined, title: 'B' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDocumentByFilename', () => {
|
||||
it('should read a document by filename', async () => {
|
||||
mockModel.findByFilename.mockResolvedValue({
|
||||
content: 'hello',
|
||||
filename: 'note.md',
|
||||
id: 'doc-1',
|
||||
title: 'note',
|
||||
});
|
||||
|
||||
const service = new AgentDocumentsService(db, userId);
|
||||
const result = await service.getDocumentByFilename('agent-1', 'note.md');
|
||||
|
||||
expect(mockModel.findByFilename).toHaveBeenCalledWith('agent-1', 'note.md');
|
||||
expect(result).toEqual({
|
||||
content: 'hello',
|
||||
filename: 'note.md',
|
||||
id: 'doc-1',
|
||||
title: 'note',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined when filename does not exist', async () => {
|
||||
mockModel.findByFilename.mockResolvedValue(undefined);
|
||||
|
||||
const service = new AgentDocumentsService(db, userId);
|
||||
const result = await service.getDocumentByFilename('agent-1', 'missing.md');
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('upsertDocumentByFilename', () => {
|
||||
it('should create or update a document by filename', async () => {
|
||||
mockModel.upsert.mockResolvedValue({ content: 'new', filename: 'f.md', id: 'doc-1' });
|
||||
|
||||
const service = new AgentDocumentsService(db, userId);
|
||||
const result = await service.upsertDocumentByFilename({
|
||||
agentId: 'agent-1',
|
||||
content: 'new',
|
||||
filename: 'f.md',
|
||||
});
|
||||
|
||||
expect(mockModel.upsert).toHaveBeenCalledWith('agent-1', 'f.md', 'new');
|
||||
expect(result).toEqual({ content: 'new', filename: 'f.md', id: 'doc-1' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasDocuments', () => {
|
||||
it('should use the model existence check', async () => {
|
||||
mockModel.hasByAgent.mockResolvedValue(true);
|
||||
|
||||
@@ -321,6 +321,32 @@ export class AgentDocumentsService {
|
||||
}
|
||||
}
|
||||
|
||||
async listDocuments(agentId: string) {
|
||||
const docs = await this.agentDocumentModel.findByAgent(agentId);
|
||||
return docs.map((d) => ({
|
||||
filename: d.filename,
|
||||
id: d.id,
|
||||
loadPosition: d.policy?.context?.position,
|
||||
title: d.title,
|
||||
}));
|
||||
}
|
||||
|
||||
async getDocumentByFilename(agentId: string, filename: string) {
|
||||
return this.agentDocumentModel.findByFilename(agentId, filename);
|
||||
}
|
||||
|
||||
async upsertDocumentByFilename({
|
||||
agentId,
|
||||
filename,
|
||||
content,
|
||||
}: {
|
||||
agentId: string;
|
||||
content: string;
|
||||
filename: string;
|
||||
}) {
|
||||
return this.agentDocumentModel.upsert(agentId, filename, content);
|
||||
}
|
||||
|
||||
async editDocumentById(documentId: string, content: string, expectedAgentId?: string) {
|
||||
const doc = await this.getDocumentByIdInAgent(documentId, expectedAgentId);
|
||||
if (!doc) return undefined;
|
||||
|
||||
@@ -19,7 +19,13 @@ export const agentDocumentsRuntime: ServerRuntimeRegistration = {
|
||||
createDocument: ({ agentId, content, title }) =>
|
||||
service.createDocument(agentId, title, content),
|
||||
editDocument: ({ agentId, content, id }) => service.editDocumentById(id, content, agentId),
|
||||
listDocuments: async ({ agentId }) => {
|
||||
const docs = await service.listDocuments(agentId);
|
||||
return docs.map((d) => ({ id: d.id, title: d.title }));
|
||||
},
|
||||
readDocument: ({ agentId, id }) => service.getDocumentById(id, agentId),
|
||||
readDocumentByFilename: ({ agentId, filename }) =>
|
||||
service.getDocumentByFilename(agentId, filename),
|
||||
removeDocument: ({ agentId, id }) => service.removeDocumentById(id, agentId),
|
||||
renameDocument: ({ agentId, id, newTitle }) =>
|
||||
service.renameDocumentById(id, newTitle, agentId),
|
||||
@@ -29,6 +35,8 @@ export const agentDocumentsRuntime: ServerRuntimeRegistration = {
|
||||
{ ...rule, rule: rule.rule as DocumentLoadRule | undefined },
|
||||
agentId,
|
||||
),
|
||||
upsertDocumentByFilename: ({ agentId, content, filename }) =>
|
||||
service.upsertDocumentByFilename({ agentId, content, filename }),
|
||||
});
|
||||
},
|
||||
identifier: AgentDocumentsIdentifier,
|
||||
|
||||
@@ -14,6 +14,22 @@ class AgentDocumentService {
|
||||
return lambdaClient.agentDocument.initializeFromTemplate.mutate(params);
|
||||
};
|
||||
|
||||
listDocuments = async (params: { agentId: string }) => {
|
||||
return lambdaClient.agentDocument.listDocuments.query(params);
|
||||
};
|
||||
|
||||
readDocumentByFilename = async (params: { agentId: string; filename: string }) => {
|
||||
return lambdaClient.agentDocument.readDocumentByFilename.query(params);
|
||||
};
|
||||
|
||||
upsertDocumentByFilename = async (params: {
|
||||
agentId: string;
|
||||
content: string;
|
||||
filename: string;
|
||||
}) => {
|
||||
return lambdaClient.agentDocument.upsertDocumentByFilename.mutate(params);
|
||||
};
|
||||
|
||||
createDocument = async (params: { agentId: string; content: string; title: string }) => {
|
||||
return lambdaClient.agentDocument.createDocument.mutate(params);
|
||||
};
|
||||
|
||||
@@ -11,7 +11,13 @@ const runtime = new AgentDocumentsExecutionRuntime({
|
||||
agentDocumentService.createDocument({ agentId, content, title }),
|
||||
editDocument: ({ agentId, content, id }) =>
|
||||
agentDocumentService.editDocument({ agentId, content, id }),
|
||||
listDocuments: async ({ agentId }) => {
|
||||
const docs = await agentDocumentService.listDocuments({ agentId });
|
||||
return docs.map((d) => ({ id: d.id, title: d.title }));
|
||||
},
|
||||
readDocument: ({ agentId, id }) => agentDocumentService.readDocument({ agentId, id }),
|
||||
readDocumentByFilename: ({ agentId, filename }) =>
|
||||
agentDocumentService.readDocumentByFilename({ agentId, filename }),
|
||||
removeDocument: async ({ agentId, id }) =>
|
||||
(await agentDocumentService.removeDocument({ agentId, id })).deleted,
|
||||
renameDocument: ({ agentId, id, newTitle }) =>
|
||||
@@ -26,6 +32,8 @@ const runtime = new AgentDocumentsExecutionRuntime({
|
||||
rule: rule.rule as DocumentLoadRule | undefined,
|
||||
},
|
||||
}),
|
||||
upsertDocumentByFilename: ({ agentId, content, filename }) =>
|
||||
agentDocumentService.upsertDocumentByFilename({ agentId, content, filename }),
|
||||
});
|
||||
|
||||
export const agentDocumentsExecutor = new AgentDocumentsExecutor(runtime);
|
||||
|
||||
Reference in New Issue
Block a user