mirror of
https://github.com/nocobase/nocobase.git
synced 2026-08-30 16:56:07 +08:00
feat: improvements for ai employee (#8191)
* feat: add margin for ai hint message box * feat(ai-employee): add submit validation for AI employee form * fix: delete unnecessary build-in AI employee Avery * feat: improve and unify copywriting style * feat(ai-employee): define default role permissions for AI employees * fix: ai employee add error handler * feat: ai support external data source * feat: add ai work context variables * fix: ai work context read from ctx * feat: ai tool i18n text * feat: ai chat messages add token limit * fix: ai builder employee add dara * fix: resolve data modeling tool issues * fix: ai tool getDataSources type and displayName
This commit is contained in:
+2
@@ -7,6 +7,8 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
// Notice: This component is not used in the current version.
|
||||
|
||||
import React from 'react';
|
||||
import { SchemaComponent } from '@nocobase/client';
|
||||
import { Alert } from 'antd';
|
||||
|
||||
@@ -19,7 +19,6 @@ import { ProfileSettings } from './ProfileSettings';
|
||||
import { SystemPrompt } from './SystemPrompt';
|
||||
import aiEmployees from '../../../collections/ai-employees';
|
||||
import { SkillSettings } from './SkillSettings';
|
||||
import { DataSourceSettings } from './DataSourceSettings';
|
||||
import { Templates } from './Templates';
|
||||
import {
|
||||
useCreateFormProps,
|
||||
@@ -59,8 +58,8 @@ const AIEmployeeForm: React.FC<{
|
||||
forceRender: true,
|
||||
},
|
||||
{
|
||||
key: 'persona',
|
||||
label: t('Characterization'),
|
||||
key: 'roleSetting',
|
||||
label: t('Role setting'),
|
||||
children: <SystemPrompt />,
|
||||
forceRender: true,
|
||||
},
|
||||
@@ -80,11 +79,6 @@ const AIEmployeeForm: React.FC<{
|
||||
label: t('Skills'),
|
||||
children: <SkillSettings />,
|
||||
},
|
||||
// {
|
||||
// key: 'dataSources',
|
||||
// label: t('Data sources'),
|
||||
// children: <DataSourceSettings />,
|
||||
// },
|
||||
...(knowledgeBaseEnabled
|
||||
? [
|
||||
{
|
||||
|
||||
@@ -78,7 +78,7 @@ const Description = () => {
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
}}
|
||||
message={t('Characterization description')}
|
||||
message={t('Role setting description')}
|
||||
type="info"
|
||||
/>
|
||||
);
|
||||
@@ -110,13 +110,13 @@ export const SystemPrompt: React.FC = () => {
|
||||
properties: {
|
||||
about: {
|
||||
type: 'string',
|
||||
title: '{{t("Characterization")}}',
|
||||
title: '{{t("Role setting")}}',
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Variable.RawTextArea',
|
||||
'x-component-props': {
|
||||
scope: options,
|
||||
placeholder: t('Characterization placeholder'),
|
||||
placeholder: t('Role setting placeholder'),
|
||||
autoSize: {
|
||||
minRows: 15,
|
||||
},
|
||||
|
||||
@@ -85,8 +85,17 @@ export const useCreateActionProps = () => {
|
||||
return {
|
||||
type: 'primary',
|
||||
async onClick() {
|
||||
await form.submit();
|
||||
const values = form.values;
|
||||
if (!values?.about) {
|
||||
message.warning(t('Please complete role setting before submitting'));
|
||||
return;
|
||||
}
|
||||
const modelSettings = values?.modelSettings;
|
||||
if (!modelSettings?.llmService || !modelSettings?.model) {
|
||||
message.warning(t('Please complete model setting before submitting'));
|
||||
return;
|
||||
}
|
||||
await form.submit();
|
||||
await api.resource('aiEmployees').create({
|
||||
values,
|
||||
});
|
||||
@@ -113,8 +122,17 @@ export const useEditActionProps = () => {
|
||||
return {
|
||||
type: 'primary',
|
||||
async onClick() {
|
||||
await form.submit();
|
||||
const values = form.values;
|
||||
if (!values?.about) {
|
||||
message.warning(t('Please complete persona before submitting'));
|
||||
return;
|
||||
}
|
||||
const modelSettings = values?.modelSettings;
|
||||
if (!modelSettings?.llmService || !modelSettings?.model) {
|
||||
message.warning(t('Please complete model settings before submitting'));
|
||||
return;
|
||||
}
|
||||
await form.submit();
|
||||
await resource.update({
|
||||
values,
|
||||
filterByTk: values[filterTk],
|
||||
@@ -134,9 +152,12 @@ export const useDeleteActionProps = () => {
|
||||
const { onClick } = useDestroyActionProps();
|
||||
const isBuiltIn = record?.builtIn;
|
||||
const { message } = App.useApp();
|
||||
const api = useAPIClient();
|
||||
const isSuperUser = api.auth.role === 'root';
|
||||
|
||||
return {
|
||||
async onClick(e?, callBack?) {
|
||||
if (isBuiltIn) {
|
||||
if (isBuiltIn && !isSuperUser) {
|
||||
message.warning(t('Cannot delete built-in ai employees'));
|
||||
return;
|
||||
}
|
||||
|
||||
+43
-2
@@ -15,7 +15,8 @@ import { Avatar, Popover, Tooltip } from 'antd';
|
||||
import { useChatMessagesStore } from '../chatbox/stores/chat-messages';
|
||||
import { ProfileCard } from '../ProfileCard';
|
||||
import { avatars } from '../avatars';
|
||||
import { EditorRef } from '@nocobase/client';
|
||||
import { EditorRef, useCompile } from '@nocobase/client';
|
||||
import { useFlowContext } from '@nocobase/flow-engine';
|
||||
import { isEngineer } from '../built-in/utils';
|
||||
import { Task } from '../types';
|
||||
import { useT } from '../../locale';
|
||||
@@ -31,6 +32,7 @@ export interface AICodingButtonProps {
|
||||
|
||||
export const AICodingButton: React.FC<AICodingButtonProps> = ({ uid, scene, language, editorRef, setActive }) => {
|
||||
const t = useT();
|
||||
const compile = useCompile();
|
||||
const { aiEmployees } = useAIEmployeesData();
|
||||
const open = useChatBoxStore.use.open();
|
||||
const currentEmployee = useChatBoxStore.use.currentEmployee();
|
||||
@@ -38,6 +40,24 @@ export const AICodingButton: React.FC<AICodingButtonProps> = ({ uid, scene, lang
|
||||
const addContextItems = useChatMessagesStore.use.addContextItems();
|
||||
const setEditorRef = useChatMessagesStore.use.setEditorRef();
|
||||
const setCurrentEditorRefUid = useChatMessagesStore.use.setCurrentEditorRefUid();
|
||||
const ctx = useFlowContext();
|
||||
|
||||
const buildCtxVariablesDesc = () => {
|
||||
const metaTree = ctx?.getPropertyMetaTree?.() || [];
|
||||
return metaTree
|
||||
.filter((node) => {
|
||||
const disabled = typeof node.disabled === 'function' ? node.disabled() : node.disabled;
|
||||
return !disabled;
|
||||
})
|
||||
.map((node) => {
|
||||
const paths = node.paths?.length ? node.paths : [node.name].filter(Boolean);
|
||||
const fullPath = ['ctx', ...paths].join('.');
|
||||
const nodeTitle = compile(node.title, { t });
|
||||
const title = nodeTitle && nodeTitle !== node.name ? ` (${nodeTitle})` : '';
|
||||
return `- {{${fullPath}}}${title}`;
|
||||
})
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
const aiEmployee = aiEmployees.filter((e) => isEngineer(e))[0];
|
||||
|
||||
@@ -72,6 +92,7 @@ export const AICodingButton: React.FC<AICodingButtonProps> = ({ uid, scene, lang
|
||||
|
||||
const TaskTemplate = (prototype: Partial<Task>) => {
|
||||
const { message, ...rest } = prototype;
|
||||
const variablesDesc = buildCtxVariablesDesc();
|
||||
return {
|
||||
message: {
|
||||
workContext: [
|
||||
@@ -85,7 +106,15 @@ export const AICodingButton: React.FC<AICodingButtonProps> = ({ uid, scene, lang
|
||||
code: editorRef?.read(),
|
||||
},
|
||||
},
|
||||
],
|
||||
variablesDesc
|
||||
? {
|
||||
type: 'text',
|
||||
uid: 'available-variables',
|
||||
title: 'Available variables',
|
||||
content: `You can access the following variables via context:\n${variablesDesc}`,
|
||||
}
|
||||
: null,
|
||||
].filter(Boolean),
|
||||
...(message ?? {}),
|
||||
},
|
||||
autoSend: false,
|
||||
@@ -139,6 +168,9 @@ export const AICodingButton: React.FC<AICodingButtonProps> = ({ uid, scene, lang
|
||||
}
|
||||
|
||||
setCurrentEditorRefUid(uid);
|
||||
|
||||
const variablesDesc = buildCtxVariablesDesc();
|
||||
|
||||
addContextItems({
|
||||
type: 'code-editor',
|
||||
uid,
|
||||
@@ -149,6 +181,15 @@ export const AICodingButton: React.FC<AICodingButtonProps> = ({ uid, scene, lang
|
||||
code: editorRef?.read(),
|
||||
},
|
||||
});
|
||||
|
||||
if (variablesDesc) {
|
||||
addContextItems({
|
||||
type: 'text',
|
||||
uid: 'available-variables',
|
||||
title: 'Available variables',
|
||||
content: `You can access the following variables via context:\n${variablesDesc}`,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Popover>
|
||||
|
||||
+1
-1
@@ -333,7 +333,7 @@ export const ErrorMessage: React.FC<{
|
||||
});
|
||||
|
||||
export const HintMessage: React.FC<{ msg: any }> = memo(({ msg }) => {
|
||||
return <Alert message={<>{msg.content} </>} type="info" showIcon closable />;
|
||||
return <Alert style={{ marginBottom: 8 }} message={<>{msg.content} </>} type="info" showIcon closable />;
|
||||
});
|
||||
|
||||
export const TaskMessage: React.FC<{
|
||||
|
||||
@@ -93,6 +93,8 @@
|
||||
"Get code snippet list": "Get code snippet list",
|
||||
"Get collection metadata": "Get collection metadata",
|
||||
"Get collection names": "Get collection names",
|
||||
"Get data sources": "Get data sources",
|
||||
"Retrieve list of all available data sources": "Retrieve list of all available data sources",
|
||||
"Get models list failed, you can enter a model name manually.": "Get models list failed, you can enter a model name manually.",
|
||||
"Greeting message": "Greeting message",
|
||||
"Greeting message placeholder": "Opening message sent to the user when starting a new conversation.",
|
||||
@@ -129,9 +131,9 @@
|
||||
"Parameter name": "Parameter name",
|
||||
"Parameter type": "Parameter type",
|
||||
"Parameters": "Parameters",
|
||||
"Characterization": "Characterization",
|
||||
"Characterization description": "The system prompt for the AI model, defines who \"I\" am, as well as the rules and requirements I follow to perform tasks.",
|
||||
"Characterization placeholder": "The system prompt for the AI model, defines who \"I\" am, as well as the rules and requirements I follow to perform tasks.",
|
||||
"Role setting": "Role setting",
|
||||
"Role setting description": "The system prompt for the AI model, defines who \"I\" am, as well as the rules and requirements I follow to perform tasks.",
|
||||
"Role setting placeholder": "The system prompt for the AI model, defines who \"I\" am, as well as the rules and requirements I follow to perform tasks.",
|
||||
"Personalized prompt": "Personalized prompt",
|
||||
"Personalized prompt description": "You can set personalized prompt for the current AI employee which will be sent to the LLM at the start of each new conversation.",
|
||||
"Pick block": "Pick block",
|
||||
@@ -204,5 +206,7 @@
|
||||
"knowledge Base Prompt default": "From knowledge base:\n{knowledgeBaseData}\nAnswer user's question using this information.",
|
||||
"please fix the error": "please fix the error",
|
||||
"please review the code": "please review the code",
|
||||
"references {{index}}": "references {{index}}"
|
||||
"references {{index}}": "references {{index}}",
|
||||
"Please complete role setting before submitting": "Please complete role setting before submitting",
|
||||
"Please complete model setting before submitting": "Please complete model setting before submitting"
|
||||
}
|
||||
@@ -93,6 +93,8 @@
|
||||
"Get code snippet list": "获取代码片段列表",
|
||||
"Get collection metadata": "获取数据表元数据",
|
||||
"Get collection names": "获取数据表名称",
|
||||
"Get data sources": "获取数据源",
|
||||
"Retrieve list of all available data sources": "获取所有可用数据源列表",
|
||||
"Get models list failed, you can enter a model name manually.": "获取模型列表失败,你可以手动输入模型名称。",
|
||||
"Greeting message": "问候语",
|
||||
"Greeting message placeholder": "开启新对话时发送给用户的开场白",
|
||||
@@ -129,9 +131,9 @@
|
||||
"Parameter name": "参数名",
|
||||
"Parameter type": "参数类型",
|
||||
"Parameters": "参数",
|
||||
"Characterization": "人物设定",
|
||||
"Characterization description": "AI模型的系统提示词,决定了“我”是谁,遵循哪些要求来工作和完成任务。",
|
||||
"Characterization placeholder": "AI模型的系统提示词,决定了“我”是谁,遵循哪些要求来工作和完成任务。",
|
||||
"Role setting": "人物设定",
|
||||
"Role setting description": "AI 模型的系统提示词,决定了“我”是谁,遵循哪些要求来工作和完成任务。",
|
||||
"Role setting placeholder": "AI 模型的系统提示词,决定了“我”是谁,遵循哪些要求来工作和完成任务。",
|
||||
"Personalized prompt": "个性化提示词",
|
||||
"Personalized prompt description": "你可以针对当前 AI 员工设置个性化的提示词,将在每轮新对话时发送给大模型",
|
||||
"Pick block": "选择区块",
|
||||
@@ -204,5 +206,7 @@
|
||||
"knowledge Base Prompt default": "从知识库检索到的已知信息如下:\n{knowledgeBaseData}\n请参考上述已知信回答用户提问。",
|
||||
"please fix the error": "请修复这个错误",
|
||||
"please review the code": "请审查这份代码",
|
||||
"references {{index}}": "参考资料 {{index}}"
|
||||
"references {{index}}": "参考资料 {{index}}",
|
||||
"Please complete role setting before submitting": "请先完成人物设定",
|
||||
"Please complete model setting before submitting": "请先完成模型设置"
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import { LLMProvider } from '../llm-providers/provider';
|
||||
import { Database } from '@nocobase/database';
|
||||
import { concat } from '@langchain/core/utils/stream';
|
||||
import PluginAIServer from '../plugin';
|
||||
import { parseVariables } from '../utils';
|
||||
import { sendSSEError, parseVariables } from '../utils';
|
||||
import { getSystemPrompt } from './prompts';
|
||||
import _ from 'lodash';
|
||||
import { AIChatContext, AIChatConversation, AIMessage, AIMessageInput } from '../types';
|
||||
@@ -173,12 +173,17 @@ export class AIEmployee {
|
||||
|
||||
this.plugin.aiEmployeesManager.conversationController.delete(this.sessionId);
|
||||
|
||||
// 如果流式过程中发生错误,必须发送错误事件,无论是否有部分内容
|
||||
if (errMsg) {
|
||||
this.sendErrorResponse(errMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
const message = gathered?.content;
|
||||
const toolCalls = gathered?.tool_calls;
|
||||
const skills = this.employee.skillSettings?.skills;
|
||||
if (!message && !toolCalls?.length && !signal.aborted && !allowEmpty) {
|
||||
this.ctx.res.write(`data: ${JSON.stringify({ type: 'error', body: errMsg })}\n\n`);
|
||||
this.ctx.res.end();
|
||||
this.sendErrorResponse(errMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -271,7 +276,8 @@ export class AIEmployee {
|
||||
return result;
|
||||
}
|
||||
|
||||
getDataSources() {
|
||||
// Notice: employee.dataSourceSettings is not used in the current version.
|
||||
getEmployeeDataSourceContext() {
|
||||
const dataSourceSettings: {
|
||||
collections?: {
|
||||
collection: string;
|
||||
@@ -330,7 +336,7 @@ export class AIEmployee {
|
||||
});
|
||||
|
||||
let systemMessage = await parseVariables(this.ctx, this.employee.about);
|
||||
const dataSourceMessage = this.getDataSources();
|
||||
const dataSourceMessage = this.getEmployeeDataSourceContext();
|
||||
if (dataSourceMessage) {
|
||||
systemMessage = `${systemMessage}\n${dataSourceMessage}`;
|
||||
}
|
||||
@@ -583,7 +589,7 @@ export class AIEmployee {
|
||||
}
|
||||
} catch (err) {
|
||||
this.ctx.log.error(err);
|
||||
this.sendErrorResponse('Tool call error');
|
||||
this.sendErrorResponse(err.message || 'Tool call error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -753,8 +759,7 @@ export class AIEmployee {
|
||||
}
|
||||
|
||||
sendErrorResponse(errorMessage: string) {
|
||||
this.ctx.res.write(`data: ${JSON.stringify({ type: 'error', body: errorMessage })} \n\n`);
|
||||
this.ctx.res.end();
|
||||
sendSSEError(this.ctx, errorMessage);
|
||||
}
|
||||
|
||||
async processMessages(userMessages: AIMessageInput[], messageId?: string) {
|
||||
|
||||
+4
@@ -15,6 +15,10 @@ export default {
|
||||
profile,
|
||||
skillSettings: {
|
||||
skills: [
|
||||
{
|
||||
name: 'dataModeling-getDataSources',
|
||||
autoCall: true,
|
||||
},
|
||||
{
|
||||
name: 'dataModeling-getCollectionNames',
|
||||
autoCall: true,
|
||||
|
||||
+7
-10
@@ -119,9 +119,13 @@ ctx.render(
|
||||
```
|
||||
|
||||
### 4. Popup Environment
|
||||
- **Accessing Records**: When running in a popup (e.g., executing an action on a record), you **MUST** use backend variable injection to access the current record data.
|
||||
- **Correct**: `const popupRecord = {{ ctx.popup.record }};`
|
||||
- **Incorrect**: `const popupRecord = ctx.popup.record;` (This will be undefined at runtime).
|
||||
- **Popup Record Access**: When running inside a popup (e.g., detail/edit popup or record action popup), access the current record via backend variable injection.
|
||||
- **SQL / Expression usage (no quotes)**: Use `{{ ctx.popup.record }}` directly (it will be injected by the backend as a real value/object at runtime).
|
||||
- **Example (expression)**: `{{ ctx.popup.record }}`
|
||||
- **Example (SQL placeholder)**: `{{ ctx.popup.record.id }}` (or other fields) where your SQL/template system supports variable injection.
|
||||
- **JavaScript / JSX usage (runtime code)**: In JS/JSX, retrieve the popup record via `ctx.resolveJsonTemplate()`.
|
||||
- **Example**: `const popupRecord = await ctx.resolveJsonTemplate('{{ ctx.popup.record }}');`
|
||||
- **Note**: This is the reliable way to read injected popup variables in runtime code.
|
||||
|
||||
### 5. Implementation Constraints
|
||||
1. **Single-file constraint**
|
||||
@@ -172,13 +176,6 @@ Don't add headings like "Summary:" or "Update:".
|
||||
- The conversation history may refer to tools that are no longer available. NEVER call tools that are not explicitly provided.
|
||||
- After you decide to call a tool, include the tool call information and parameters in your response, and I will run the tool for you and provide you with tool call results.
|
||||
|
||||
### Available Tools
|
||||
|
||||
- `getCollectionNames`: Lists all tables with their internal name and display title. Use this to disambiguate user references.
|
||||
- `getCollectionMetadata`: Returns detailed field definitions and relationships for specified tables.
|
||||
- `listCodeSnippet`: list builtin code snippets which can be used to implement the task.
|
||||
- `getCodeSnippet`: use ref from "listCodeSnippet" to get code snippets content.
|
||||
|
||||
## Code Style
|
||||
IMPORTANT: The code you write will be reviewed by humans; optimize for clarity and readability. Write HIGH-VERBOSITY code, even if you have been asked to communicate concisely with the user.
|
||||
|
||||
|
||||
+4
@@ -19,6 +19,10 @@ export default {
|
||||
name: 'dataModeling-intentRouter',
|
||||
autoCall: true,
|
||||
},
|
||||
{
|
||||
name: 'dataModeling-getDataSources',
|
||||
autoCall: true,
|
||||
},
|
||||
{
|
||||
name: 'dataModeling-getCollectionNames',
|
||||
autoCall: true,
|
||||
|
||||
+4
@@ -19,6 +19,10 @@ export default {
|
||||
name: 'frontend-formFiller',
|
||||
autoCall: true,
|
||||
},
|
||||
{
|
||||
name: 'dataModeling-getDataSources',
|
||||
autoCall: true,
|
||||
},
|
||||
{
|
||||
name: 'dataModeling-getCollectionNames',
|
||||
autoCall: true,
|
||||
|
||||
+1
@@ -22,6 +22,7 @@ export default {
|
||||
{ name: 'dataSource-dataSourceCounting', autoCall: true },
|
||||
{ name: 'dataSource-dataSourceQuery', autoCall: true },
|
||||
// 了解数据结构
|
||||
{ name: 'dataModeling-getDataSources', autoCall: true },
|
||||
{ name: 'dataModeling-getCollectionNames', autoCall: true },
|
||||
{ name: 'dataModeling-getCollectionMetadata', autoCall: true },
|
||||
],
|
||||
|
||||
+3
-1
@@ -15,7 +15,7 @@ Answer questions using data by fetching required information, analyzing results,
|
||||
|
||||
**YOUR PROCESS:**
|
||||
1. Understand the user’s intent and the required data.
|
||||
2. Produce a single sql code block using safe, read-only SELECT to fetch the data. Use tools like getCollectionNames/getCollectionMetadata only to inspect schema (collections and fields). Do not use dataSourceQuery for aggregate or computed expressions; it is for plain column selection only. Always wait for data before continuing.
|
||||
2. Produce a single sql code block using safe, read-only SELECT to fetch the data. Use tools like getCollectionNames/getCollectionMetadata only to inspect schema (collections and fields).
|
||||
3. Analyze the data to answer the question without fabricating any content.
|
||||
4. Visualize the answer:
|
||||
- Trends/Comparisons/Distributions: use charts (bar/line/pie/etc.)
|
||||
@@ -24,6 +24,8 @@ Answer questions using data by fetching required information, analyzing results,
|
||||
|
||||
**CRITICAL RULES:**
|
||||
- Language: Respond in the user’s language: {{$nLang}}.
|
||||
- SQL Dialect Awareness: Adjust SQL syntax based on the target data source type (e.g., use backticks \` for MySQL/MariaDB, double quotes " for PostgreSQL/SQLite). Check the "type" field in data source information (from context or tool results) before writing SQL.
|
||||
- DataSource Specification: When writing SQL, ALWAYS add a comment on the first line specifying the data source key, e.g., \`-- dataSource: ExternalMySQL\`. If it's the main database, use \`-- dataSource: main\`.
|
||||
- Visual-first: Prefer charts or KPI cards whenever possible.
|
||||
- Data integrity: NEVER fabricate data; if missing, ask one focused question.
|
||||
- SQL safety: ONLY use SELECT; never INSERT/UPDATE/DELETE.
|
||||
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import profile from './profile';
|
||||
|
||||
export default {
|
||||
username: 'avery',
|
||||
description: 'AI employee for form filling',
|
||||
profile,
|
||||
skillSettings: {
|
||||
skills: [
|
||||
{ name: 'frontend-formFiller', autoCall: true },
|
||||
{
|
||||
name: 'dataSource-dataSourceCounting',
|
||||
autoCall: true,
|
||||
},
|
||||
{
|
||||
name: 'dataSource-dataSourceQuery',
|
||||
autoCall: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import prompt from './prompt';
|
||||
|
||||
export default {
|
||||
'en-US': {
|
||||
avatar: 'nocobase-045-female',
|
||||
nickname: 'Avery',
|
||||
position: 'Form filler',
|
||||
bio: 'I specialize in extracting structured fields from unstructured input and completing forms quickly and accurately. Your reliable partner in form handling.',
|
||||
greeting: 'Hi, I’m Avery. Send me the form and the content you’d like filled in—I’ll take care of the rest.',
|
||||
about: prompt['en-US'],
|
||||
},
|
||||
'zh-CN': {
|
||||
avatar: 'nocobase-045-female',
|
||||
nickname: 'Avery',
|
||||
position: '表单助理',
|
||||
bio: '我擅长从非结构化输入中提取结构化字段,并快速准确地完成表单填写。我是您处理表单时的可靠伙伴。',
|
||||
greeting: '嗨,我是 Avery。请给我表单和您想填写的内容,我负责处理。',
|
||||
about: prompt['en-US'],
|
||||
},
|
||||
};
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
export default {
|
||||
'en-US': `You are Avery, a professional and reliable form assistant. The user will provide a form definition (with field definitions) and unstructured content to be filled. Your tasks:
|
||||
1. Parse the form definition to identify the fields;
|
||||
2. Extract corresponding values from the content;
|
||||
3. Build a structured data object in JSON;
|
||||
4. Call formFiller tool to fill in the form;
|
||||
5. If user requests for generating data for multiple forms, the response should be separated.Unless an error occurs or the user asks for explanation, keep your response natural, focused, and execution-oriented.`,
|
||||
};
|
||||
+4
@@ -15,6 +15,10 @@ export default {
|
||||
profile,
|
||||
skillSettings: {
|
||||
skills: [
|
||||
{
|
||||
name: 'dataModeling-getDataSources',
|
||||
autoCall: true,
|
||||
},
|
||||
{
|
||||
name: 'dataModeling-getCollectionNames',
|
||||
autoCall: true,
|
||||
|
||||
+4
@@ -15,6 +15,10 @@ export default {
|
||||
profile,
|
||||
skillSettings: {
|
||||
skills: [
|
||||
{
|
||||
name: 'dataModeling-getDataSources',
|
||||
autoCall: true,
|
||||
},
|
||||
{
|
||||
name: 'dataModeling-getCollectionNames',
|
||||
autoCall: true,
|
||||
|
||||
+4
@@ -19,6 +19,10 @@ export default {
|
||||
name: 'frontend-formFiller',
|
||||
autoCall: true,
|
||||
},
|
||||
{
|
||||
name: 'dataModeling-getDataSources',
|
||||
autoCall: true,
|
||||
},
|
||||
{
|
||||
name: 'dataModeling-getCollectionNames',
|
||||
autoCall: true,
|
||||
|
||||
@@ -96,10 +96,12 @@ class AIChatConversationImpl implements AIChatConversation {
|
||||
$lt: query.messageId,
|
||||
};
|
||||
}
|
||||
return await this.aiConversationMessagesRepo.find({
|
||||
sort: ['messageId'],
|
||||
const messages = await this.aiConversationMessagesRepo.find({
|
||||
sort: ['-messageId'], // 改为倒序,取最新的
|
||||
limit: 50, // 新增:最多 50 条消息
|
||||
filter,
|
||||
});
|
||||
return messages.reverse(); // 反转回正序
|
||||
}
|
||||
|
||||
async lastUserMessage(): Promise<AIMessage> {
|
||||
@@ -133,12 +135,20 @@ class AIChatConversationImpl implements AIChatConversation {
|
||||
const formattedMessages = [];
|
||||
const { provider, workContextHandler } = options;
|
||||
|
||||
// 新增:截断过长的内容
|
||||
const truncate = (text: string, maxLen = 50000) => {
|
||||
if (!text || text.length <= maxLen) return text;
|
||||
return text.slice(0, maxLen) + '\n...[truncated]';
|
||||
};
|
||||
|
||||
for (const msg of messages) {
|
||||
const attachments = msg.attachments;
|
||||
const workContext = msg.workContext;
|
||||
let content = msg.content?.content;
|
||||
if (!content && !attachments && !msg.toolCalls?.length) {
|
||||
continue;
|
||||
|
||||
// 新增:截断消息内容
|
||||
if (typeof content === 'string') {
|
||||
content = truncate(content);
|
||||
}
|
||||
if (msg.role === 'user') {
|
||||
if (typeof content === 'string') {
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
import PluginAIServer from '../plugin';
|
||||
import dataModeling from '../ai-employees/built-in/data-modeling';
|
||||
import formFiller from '../ai-employees/built-in/form-filler';
|
||||
import aiCoding from '../ai-employees/built-in/ai-coding';
|
||||
import dataOrganizer from '../ai-employees/built-in/data-organizer';
|
||||
import insightsAnalyst from '../ai-employees/built-in/insights-analyst';
|
||||
@@ -33,7 +32,6 @@ const DEFAULT_KNOWLEDGE_BASE_PROMPT =
|
||||
export class BuiltInManager {
|
||||
private builtInEmployees = [
|
||||
dataModeling,
|
||||
formFiller,
|
||||
aiCoding,
|
||||
dataOrganizer,
|
||||
insightsAnalyst,
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
formFiller,
|
||||
getCollectionMetadata,
|
||||
getCollectionNames,
|
||||
getDataSources,
|
||||
getWorkflowCallers,
|
||||
chartGenerator,
|
||||
} from './tools';
|
||||
@@ -56,6 +57,15 @@ export class PluginAIServer extends Plugin {
|
||||
workContextHandler = createWorkContextHandler(this);
|
||||
snowflake: Snowflake;
|
||||
|
||||
/**
|
||||
* Check if the AI employee is a builder/admin-only type (e.g., Nathan, Orin).
|
||||
* These employees have powerful capabilities (coding, schema modification) and should be restricted to admins.
|
||||
*/
|
||||
isBuilderAI(username: string) {
|
||||
const BUILDER_AI_USERNAMES = ['nathan', 'orin', 'dara'];
|
||||
return BUILDER_AI_USERNAMES.includes(username);
|
||||
}
|
||||
|
||||
async afterAdd() {}
|
||||
|
||||
async beforeLoad() {
|
||||
@@ -133,6 +143,10 @@ export class PluginAIServer extends Plugin {
|
||||
groupName: dataModelingGroupName,
|
||||
tool: dataModelingIntentRouter,
|
||||
},
|
||||
{
|
||||
groupName: dataModelingGroupName,
|
||||
tool: getDataSources,
|
||||
},
|
||||
{
|
||||
groupName: dataModelingGroupName,
|
||||
tool: getCollectionNames,
|
||||
@@ -228,8 +242,21 @@ export class PluginAIServer extends Plugin {
|
||||
}
|
||||
|
||||
this.app.db.on('roles.beforeCreate', async (instance: Model) => {
|
||||
instance.set('allowNewAiEmployee', ['admin', 'member'].includes(instance.name));
|
||||
instance.set('allowNewAiEmployee', true);
|
||||
});
|
||||
// 为新角色默认开启 AI 员工
|
||||
this.app.db.on('roles.afterCreate', async (instance: Model, { transaction }) => {
|
||||
const allAiEmployees = await this.app.db.getRepository('aiEmployees').find({
|
||||
transaction,
|
||||
});
|
||||
|
||||
const generalAiEmployees = allAiEmployees.filter((ai: { username: string }) => !this.isBuilderAI(ai.username));
|
||||
|
||||
if (generalAiEmployees.length > 0) {
|
||||
await instance.addAiEmployees(generalAiEmployees, { transaction });
|
||||
}
|
||||
});
|
||||
|
||||
this.app.db.on('aiEmployees.afterCreate', async (instance: Model, { transaction }) => {
|
||||
const roles = await this.app.db.getRepository('roles').find({
|
||||
filter: {
|
||||
@@ -238,9 +265,15 @@ export class PluginAIServer extends Plugin {
|
||||
transaction,
|
||||
});
|
||||
|
||||
// 为初始化/新创建的 AI 员工分配角色,builder 类的AI员工默认只对 Admin 角色开启
|
||||
let targetRoles = roles;
|
||||
if (this.isBuilderAI(instance.username)) {
|
||||
targetRoles = roles.filter((role: { name: string }) => role.name === 'admin');
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
await this.app.db.getRepository('aiEmployees.roles', instance.username).add({
|
||||
tk: roles.map((role: { name: string }) => role.name),
|
||||
tk: targetRoles.map((role: { name: string }) => role.name),
|
||||
transaction,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import actions, { Context, Next } from '@nocobase/actions';
|
||||
import PluginAIServer from '../plugin';
|
||||
import { Model } from '@nocobase/database';
|
||||
import { parseResponseMessage } from '../utils';
|
||||
import { parseResponseMessage, sendSSEError } from '../utils';
|
||||
import { AIEmployee } from '../ai-employees/ai-employee';
|
||||
|
||||
async function getAIEmployee(ctx: Context, username: string) {
|
||||
@@ -36,8 +36,7 @@ function setupSSEHeaders(ctx: Context) {
|
||||
}
|
||||
|
||||
function sendErrorResponse(ctx: Context, errorMessage: string) {
|
||||
ctx.res.write(`data: ${JSON.stringify({ type: 'error', body: errorMessage })} \n\n`);
|
||||
ctx.res.end();
|
||||
sendSSEError(ctx, errorMessage);
|
||||
}
|
||||
|
||||
export default {
|
||||
@@ -357,7 +356,7 @@ export default {
|
||||
await aiEmployee.processMessages(messages, editingMessageId);
|
||||
} catch (err) {
|
||||
ctx.log.error(err);
|
||||
sendErrorResponse(ctx, err.message || 'Chat error warning');
|
||||
sendErrorResponse(ctx, err.message || 'Tool call error');
|
||||
}
|
||||
|
||||
await next();
|
||||
@@ -421,7 +420,7 @@ export default {
|
||||
await aiEmployee.resendMessages(messageId);
|
||||
} catch (err) {
|
||||
ctx.log.error(err);
|
||||
sendErrorResponse(ctx, 'Chat error warning');
|
||||
sendErrorResponse(ctx, err.message || 'Chat error warning');
|
||||
}
|
||||
|
||||
await next();
|
||||
@@ -525,7 +524,7 @@ export default {
|
||||
await aiEmployee.callTool(message.messageId, false);
|
||||
} catch (err) {
|
||||
ctx.log.error(err);
|
||||
sendErrorResponse(ctx, 'Tool call error');
|
||||
sendErrorResponse(ctx, err.message || 'Tool call error');
|
||||
}
|
||||
await next();
|
||||
},
|
||||
@@ -584,7 +583,7 @@ export default {
|
||||
await aiEmployee.confirmToolCall(message.messageId, toolCallIds);
|
||||
} catch (err) {
|
||||
ctx.log.error(err);
|
||||
sendErrorResponse(ctx, 'Tool call confirm error');
|
||||
sendErrorResponse(ctx, err.message || 'Tool call confirm error');
|
||||
}
|
||||
await next();
|
||||
},
|
||||
|
||||
@@ -129,14 +129,25 @@ export const getCollectionNames: ToolOptions = {
|
||||
description: '{{t("Retrieve names and titles map of all collections")}}',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
properties: {
|
||||
dataSource: {
|
||||
type: 'string',
|
||||
description: 'The data source name to retrieve collections from. Defaults to "main".',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
invoke: async (ctx: Context) => {
|
||||
invoke: async (ctx: Context, args: { dataSource?: string }) => {
|
||||
const { dataSource = 'main' } = args || {};
|
||||
let names: { name: string; title: string }[] = [];
|
||||
try {
|
||||
const collections = await ctx.db.getRepository('collections').find();
|
||||
names = collections.map((collection: { name: string; title: string }) => ({
|
||||
const ds = ctx.app.dataSourceManager.dataSources.get(dataSource);
|
||||
if (!ds) {
|
||||
throw new Error(`Data source "${dataSource}" not found`);
|
||||
}
|
||||
|
||||
const collections = ds.collectionManager.getCollections();
|
||||
names = collections.map((collection) => ({
|
||||
name: collection.name,
|
||||
title: collection.title,
|
||||
}));
|
||||
@@ -152,7 +163,6 @@ export const getCollectionNames: ToolOptions = {
|
||||
content: `Failed to retrieve collection names: ${err.message}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
content: JSON.stringify(names),
|
||||
@@ -167,6 +177,10 @@ export const getCollectionMetadata: ToolOptions = {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
dataSource: {
|
||||
type: 'string',
|
||||
description: 'The data source name. Defaults to "main".',
|
||||
},
|
||||
collectionNames: {
|
||||
type: 'array',
|
||||
items: {
|
||||
@@ -181,39 +195,92 @@ export const getCollectionMetadata: ToolOptions = {
|
||||
invoke: async (
|
||||
ctx: Context,
|
||||
args: {
|
||||
dataSource?: string;
|
||||
collectionNames: string[];
|
||||
},
|
||||
) => {
|
||||
const { collectionNames } = args || {};
|
||||
const { collectionNames, dataSource = 'main' } = args || {};
|
||||
if (!collectionNames || !Array.isArray(collectionNames) || collectionNames.length === 0) {
|
||||
return {
|
||||
status: 'error',
|
||||
content: 'No collection names provided or invalid format.',
|
||||
};
|
||||
}
|
||||
const collections = await ctx.db.getRepository('collections').find({
|
||||
filter: { name: collectionNames },
|
||||
appends: ['fields'],
|
||||
});
|
||||
const metadata = collections.map((collection: any) => {
|
||||
const fields = collection.fields.map((field: any) => {
|
||||
|
||||
try {
|
||||
const ds = ctx.app.dataSourceManager.dataSources.get(dataSource);
|
||||
if (!ds) {
|
||||
return {
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
interface: field.interface,
|
||||
options: field.options || {},
|
||||
status: 'error',
|
||||
content: `Data source "${dataSource}" not found`,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const metadata = [];
|
||||
for (const name of collectionNames) {
|
||||
const collection = ds.collectionManager.getCollection(name);
|
||||
if (!collection) continue;
|
||||
|
||||
const fields = collection.getFields().map((field) => {
|
||||
return {
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
interface: field.options.interface,
|
||||
options: field.options || {},
|
||||
};
|
||||
});
|
||||
metadata.push({
|
||||
name: collection.name,
|
||||
title: collection.title,
|
||||
fields,
|
||||
});
|
||||
}
|
||||
return {
|
||||
name: collection.name,
|
||||
title: collection.title,
|
||||
fields,
|
||||
status: 'success',
|
||||
content: JSON.stringify(metadata),
|
||||
};
|
||||
});
|
||||
return {
|
||||
status: 'success',
|
||||
content: JSON.stringify(metadata),
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
status: 'error',
|
||||
content: `Failed to retrieve metadata: ${err.message}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const getDataSources: ToolOptions = {
|
||||
name: 'getDataSources',
|
||||
title: '{{t("Get data sources")}}',
|
||||
description: '{{t("Retrieve list of all available data sources")}}',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
additionalProperties: false,
|
||||
},
|
||||
invoke: async (ctx: Context) => {
|
||||
try {
|
||||
const records = await ctx.db.getRepository('dataSources').find();
|
||||
const displayNameMap = new Map(records.map((r) => [r.get('key'), r.get('displayName')]));
|
||||
const dataSources = [];
|
||||
// Add data sources
|
||||
for (const [key, ds] of ctx.app.dataSourceManager.dataSources) {
|
||||
dataSources.push({
|
||||
key: key,
|
||||
displayName: displayNameMap.get(key) || key,
|
||||
type: ds.collectionManager?.db?.sequelize?.getDialect() || 'unknown',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
content: JSON.stringify(dataSources),
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
status: 'error',
|
||||
content: `Failed to retrieve data sources: ${err.message}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,20 @@ import axios from 'axios';
|
||||
import { getDateVars, parse, parseFilter } from '@nocobase/utils';
|
||||
import { Context } from '@nocobase/actions';
|
||||
|
||||
export function sendSSEError(ctx: Context, error: Error | string) {
|
||||
const body = typeof error === 'string' ? error : error.message || 'Unknown error';
|
||||
if (!ctx.res.headersSent) {
|
||||
ctx.set({
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
});
|
||||
ctx.status = 200;
|
||||
}
|
||||
ctx.res.write(`data: ${JSON.stringify({ type: 'error', body })}\n\n`);
|
||||
ctx.res.end();
|
||||
}
|
||||
|
||||
export function stripToolCallTags(content: string): string | null {
|
||||
if (typeof content !== 'string') {
|
||||
return content;
|
||||
|
||||
+6
-1
@@ -68,7 +68,12 @@ export const DaraButton: React.FC<{ ctx: FlowSettingsContext<any> }> = ({ ctx })
|
||||
) && !/return\s*\{/.test(content);
|
||||
const isEvents = /chart\.(on|off)\s*\(|ctx\.\w+\s*\(/.test(content) && !/\breturn\s*\{/.test(content);
|
||||
|
||||
if (isSql) return ctx.writeSql(content);
|
||||
if (isSql) {
|
||||
// 从注释中提取数据源
|
||||
const dsMatch = content.match(/^--\s*dataSource:\s*(\S+)/i);
|
||||
const dataSource = dsMatch ? dsMatch[1] : undefined;
|
||||
return ctx.writeSql(content, dataSource);
|
||||
}
|
||||
if (isEvents) return ctx.writeChartEvents(content);
|
||||
return ctx.writeChartConfig(content);
|
||||
} catch (e) {
|
||||
|
||||
+2
-2
@@ -38,8 +38,8 @@ export const ConfigPanel: React.FC = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
ctx?.defineMethod?.('writeSql', async (sql: string) => {
|
||||
const dsKey = form?.values?.query?.sqlDatasource ?? DEFAULT_DATA_SOURCE_KEY;
|
||||
ctx?.defineMethod?.('writeSql', async (sql: string, dataSource?: string) => {
|
||||
const dsKey = dataSource || form?.values?.query?.sqlDatasource || DEFAULT_DATA_SOURCE_KEY;
|
||||
form?.setValuesIn?.('query.mode', 'sql');
|
||||
form?.setValuesIn?.('query.sql', sql);
|
||||
form?.setValuesIn?.('query.sqlDatasource', dsKey);
|
||||
|
||||
@@ -89,11 +89,11 @@
|
||||
"Please click 'Run Query' to fetch data before configuring chart options": "请先点击”运行查询“获取数据,再配置图表选项",
|
||||
"Please configure and run query": "请配置并执行数据查询",
|
||||
"Please configure chart": "请配置图表",
|
||||
"Please run query to retrive data.": "请执行查询来获取数据。",
|
||||
"Please run query to retrive data.": "请运行查询来获取数据。",
|
||||
"Please select a chart type.": "请选择图表类型",
|
||||
"Query": "查询",
|
||||
"Recommend chart type by data structure and explain reasons": "请根据数据结构推荐图表类型并解释原因",
|
||||
"Run query": "执行查询",
|
||||
"Run query": "运行查询",
|
||||
"Same properties set in the form above will be overwritten by this JSON config.": "上面表单中设置的相同属性将被JSON配置覆盖。",
|
||||
"Scatter": "散点图",
|
||||
"Select Field": "选择字段",
|
||||
|
||||
Reference in New Issue
Block a user