refactor(core): call copilot in tools (#13024)

fix AI-298

#### PR Dependency Tree


* **PR #13024** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Document and code artifact generation tools now use a single prompt
field for user input, enabling more flexible content creation powered by
AI.

* **Bug Fixes**
* Improved error handling for missing prompt templates or providers
during document and code artifact generation.

* **Refactor**
* Simplified input schemas for document and code artifact tools,
consolidating multiple input fields into a single prompt.
* Output schemas updated to remove metadata and other unused fields for
a cleaner result.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Peng Xiao
2025-07-04 15:23:09 +08:00
committed by GitHub
parent 24f1181069
commit d0beab9638
4 changed files with 81 additions and 86 deletions

View File

@@ -14,6 +14,7 @@ import { AccessController } from '../../../core/permission';
import { Models } from '../../../models';
import { IndexerService } from '../../indexer';
import { CopilotContextService } from '../context';
import { PromptService } from '../prompt';
import {
buildContentGetter,
buildDocContentGetter,
@@ -201,11 +202,23 @@ export abstract class CopilotProvider<C = any> {
break;
}
case 'docCompose': {
tools.doc_compose = createDocComposeTool();
const promptService = this.moduleRef.get(PromptService, {
strict: false,
});
tools.doc_compose = createDocComposeTool(
promptService,
this.factory
);
break;
}
case 'codeArtifact': {
tools.code_artifact = createCodeArtifactTool();
const promptService = this.moduleRef.get(PromptService, {
strict: false,
});
tools.code_artifact = createCodeArtifactTool(
promptService,
this.factory
);
break;
}
}

View File

@@ -2,6 +2,8 @@ import { Logger } from '@nestjs/common';
import { tool } from 'ai';
import { z } from 'zod';
import type { PromptService } from '../prompt';
import type { CopilotProviderFactory } from '../providers';
import { toolError } from './error';
const logger = new Logger('CodeArtifactTool');
@@ -12,7 +14,10 @@ const logger = new Logger('CodeArtifactTool');
* it can be saved as a single .html file and opened in any browser with no
* external dependencies.
*/
export const createCodeArtifactTool = () => {
export const createCodeArtifactTool = (
promptService: PromptService,
factory: CopilotProviderFactory
) => {
return tool({
description:
'Generate a single-file HTML snippet (with inline <style> and <script>) that accomplishes the requested functionality. The final HTML should be runnable when saved as an .html file and opened in a browser. Do NOT reference external resources (CSS, JS, images) except through data URIs.',
@@ -22,58 +27,49 @@ export const createCodeArtifactTool = () => {
*/
title: z.string().describe('The title of the HTML page'),
/**
* The raw HTML that should be placed inside <body>. *Do not* include
* <body> tags here the tool will wrap it for you.
* The optimized user prompt
*/
body: z
userPrompt: z
.string()
.describe('HTML markup that goes inside the <body> element'),
/**
* Optional CSS rules to be wrapped in a single <style> tag inside <head>.
*/
css: z
.string()
.optional()
.describe('CSS to inline in a <style> tag (omit if none).'),
/**
* Optional JavaScript code to be wrapped in a single <script> tag before
* </body>.
*/
js: z
.string()
.optional()
.describe('JavaScript to inline in a <script> tag (omit if none).'),
.describe(
'The user description of the code artifact, will be used to generate the code artifact'
),
}),
execute: async ({ title, body, css = '', js = '' }) => {
execute: async ({ title, userPrompt }) => {
try {
const parts: string[] = [];
parts.push('<!DOCTYPE html>');
parts.push('<html lang="en">');
parts.push('<head>');
parts.push('<meta charset="UTF-8" />');
parts.push(`<title>${title}</title>`);
if (css.trim().length) {
parts.push('<style>');
parts.push(css);
parts.push('</style>');
const prompt = await promptService.get('Make it real with text');
if (!prompt) {
throw new Error('Prompt not found');
}
parts.push('</head>');
parts.push('<body>');
parts.push(body);
if (js.trim().length) {
parts.push('<script>');
parts.push(js);
parts.push('</script>');
}
parts.push('</body>');
parts.push('</html>');
const html = parts.join('\n');
const provider = await factory.getProviderByModel(prompt.model);
if (!provider) {
throw new Error('Provider not found');
}
const content = await provider.text(
{
modelId: prompt.model,
},
[...prompt.finish({}), { role: 'user', content: userPrompt }]
);
// Remove surrounding ``` or ```html fences if present
let stripped = content.trim();
if (stripped.startsWith('```')) {
const firstNewline = stripped.indexOf('\n');
if (firstNewline !== -1) {
stripped = stripped.slice(firstNewline + 1);
}
if (stripped.endsWith('```')) {
stripped = stripped.slice(0, -3);
}
}
return {
title,
html,
size: html.length,
html: stripped,
size: stripped.length,
};
} catch (err: any) {
logger.error(`Failed to compose code artifact (${title})`, err);

View File

@@ -2,64 +2,51 @@ import { Logger } from '@nestjs/common';
import { tool } from 'ai';
import { z } from 'zod';
import type { PromptService } from '../prompt';
import type { CopilotProviderFactory } from '../providers';
import { toolError } from './error';
const logger = new Logger('DocComposeTool');
export const createDocComposeTool = () => {
export const createDocComposeTool = (
promptService: PromptService,
factory: CopilotProviderFactory
) => {
return tool({
description:
'Write a new document with markdown content. This tool creates structured markdown content for documents including titles, sections, and formatting.',
parameters: z.object({
title: z.string().describe('The title of the document'),
content: z
userPrompt: z
.string()
.describe(
'The main content to write in markdown format. Include proper markdown formatting like headers (# ## ###), lists (- * 1.), links [text](url), code blocks ```code```, and other markdown elements as needed.'
'The user description of the document, will be used to generate the document'
),
sections: z
.array(
z.object({
heading: z.string().describe('Section heading'),
content: z.string().describe('Section content in markdown'),
})
)
.optional()
.describe('Optional structured sections for the document'),
metadata: z
.object({
tags: z
.array(z.string())
.optional()
.describe('Optional tags for the document'),
description: z
.string()
.optional()
.describe('Optional brief description of the document'),
})
.optional()
.describe('Optional metadata for the document'),
}),
execute: async ({ title, content, sections, metadata }) => {
execute: async ({ title, userPrompt }) => {
try {
let markdownContent = '';
markdownContent += `${content}\n\n`;
if (sections && sections.length > 0) {
for (const section of sections) {
markdownContent += `## ${section.heading}\n\n`;
markdownContent += `${section.content}\n\n`;
}
const prompt = await promptService.get('Write an article about this');
if (!prompt) {
throw new Error('Prompt not found');
}
const provider = await factory.getProviderByModel(prompt.model);
if (!provider) {
throw new Error('Provider not found');
}
const content = await provider.text(
{
modelId: prompt.model,
},
[...prompt.finish({}), { role: 'user', content: userPrompt }]
);
return {
title,
markdown: markdownContent.trim(),
markdown: content,
wordCount: content.split(/\s+/).length,
metadata: metadata || {},
tags: metadata?.tags || [],
description: metadata?.description || '',
};
} catch (err: any) {
logger.error(`Failed to write document: ${title}`, err);