@nocobase/plugin-ai 2.0.0-alpha.25 → 2.0.0-alpha.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/ai-employees/flow/index.d.ts +2 -0
- package/dist/client/ai-employees/flow/models/AIEmployeeShortcutModel.d.ts +6 -0
- package/dist/client/ai-employees/types.d.ts +1 -0
- package/dist/client/index.js +1 -1
- package/dist/externalVersion.js +11 -11
- package/dist/locale/en-US.json +7 -1
- package/dist/locale/zh-CN.json +7 -1
- package/dist/node_modules/@langchain/anthropic/package.json +1 -1
- package/dist/node_modules/@langchain/core/package.json +1 -1
- package/dist/node_modules/@langchain/deepseek/package.json +1 -1
- package/dist/node_modules/@langchain/google-genai/package.json +1 -1
- package/dist/node_modules/@langchain/ollama/package.json +1 -1
- package/dist/node_modules/@langchain/openai/package.json +1 -1
- package/dist/node_modules/nodejs-snowflake/package.json +1 -1
- package/dist/node_modules/zod/package.json +1 -1
- package/dist/node_modules/zod-to-json-schema/package.json +1 -1
- package/dist/server/ai-employees/built-in/ai-coding/index.js +8 -0
- package/dist/server/ai-employees/built-in/data-modeling/index.js +8 -0
- package/dist/server/ai-employees/built-in/data-organizer/index.js +8 -0
- package/dist/server/ai-employees/built-in/form-filler/index.js +13 -1
- package/dist/server/ai-employees/built-in/insights-analyst/index.js +8 -0
- package/dist/server/ai-employees/built-in/nocobase-assistant/index.d.ts +4 -1
- package/dist/server/ai-employees/built-in/nocobase-assistant/index.js +10 -1
- package/dist/server/ai-employees/built-in/research-analyst/index.js +8 -0
- package/dist/server/ai-employees/built-in/translator/index.js +8 -0
- package/dist/server/manager/ai-chat-conversation.js +4 -4
- package/dist/server/manager/ai-context-datasource-manager.d.ts +5 -1
- package/dist/server/manager/ai-context-datasource-manager.js +10 -2
- package/dist/server/plugin.js +15 -0
- package/dist/server/resource/aiEmployees.d.ts +1 -0
- package/dist/server/resource/aiEmployees.js +19 -0
- package/dist/server/tools/datasource-query.d.ts +11 -0
- package/dist/server/tools/datasource-query.js +133 -0
- package/dist/server/utils.js +2 -2
- package/package.json +2 -2
- /package/dist/server/migrations/{20250923221104-setup-build-in.d.ts → 20250923221106-setup-build-in.d.ts} +0 -0
- /package/dist/server/migrations/{20250923221104-setup-build-in.js → 20250923221106-setup-build-in.js} +0 -0
|
@@ -141,14 +141,14 @@ class AIChatConversationImpl {
|
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
143
|
async formatMessages(messages, options) {
|
|
144
|
-
var _a, _b, _c;
|
|
144
|
+
var _a, _b, _c, _d;
|
|
145
145
|
const formattedMessages = [];
|
|
146
146
|
const { provider, workContextHandler } = options;
|
|
147
147
|
for (const msg of messages) {
|
|
148
148
|
const attachments = msg.attachments;
|
|
149
149
|
const workContext = msg.workContext;
|
|
150
|
-
let content = msg.content.content;
|
|
151
|
-
if (!content && !attachments && !((
|
|
150
|
+
let content = (_a = msg.content) == null ? void 0 : _a.content;
|
|
151
|
+
if (!content && !attachments && !((_b = msg.toolCalls) == null ? void 0 : _b.length)) {
|
|
152
152
|
continue;
|
|
153
153
|
}
|
|
154
154
|
if (msg.role === "user") {
|
|
@@ -182,7 +182,7 @@ class AIChatConversationImpl {
|
|
|
182
182
|
formattedMessages.push({
|
|
183
183
|
role: "tool",
|
|
184
184
|
content,
|
|
185
|
-
tool_call_id: (
|
|
185
|
+
tool_call_id: (_d = (_c = msg.metadata) == null ? void 0 : _c.toolCall) == null ? void 0 : _d.id
|
|
186
186
|
});
|
|
187
187
|
continue;
|
|
188
188
|
}
|
|
@@ -15,15 +15,19 @@ export declare class AIContextDatasourceManager {
|
|
|
15
15
|
protected plugin: PluginAIServer;
|
|
16
16
|
constructor(plugin: PluginAIServer);
|
|
17
17
|
preview(ctx: Context, options: PreviewOptions): Promise<QueryResult | null>;
|
|
18
|
+
query(ctx: Context, options: InnerQueryOptions): Promise<QueryResult | null>;
|
|
18
19
|
provideWorkContextResolveStrategy(): WorkContextResolveStrategy;
|
|
19
20
|
private innerQuery;
|
|
20
21
|
}
|
|
21
|
-
export type PreviewOptions = Pick<AIContextDatasource, 'datasource' | 'collectionName' | 'fields' | 'appends' | 'filter' | 'sort' | 'limit'
|
|
22
|
+
export type PreviewOptions = Pick<AIContextDatasource, 'datasource' | 'collectionName' | 'fields' | 'appends' | 'filter' | 'sort' | 'limit'> & {
|
|
23
|
+
offset?: number;
|
|
24
|
+
};
|
|
22
25
|
export type QueryOptions = {
|
|
23
26
|
id: string;
|
|
24
27
|
};
|
|
25
28
|
export type QueryResult = {
|
|
26
29
|
options: InnerQueryOptions;
|
|
30
|
+
total: number;
|
|
27
31
|
records: {
|
|
28
32
|
name: string;
|
|
29
33
|
type: string;
|
|
@@ -37,6 +37,9 @@ class AIContextDatasourceManager {
|
|
|
37
37
|
async preview(ctx, options) {
|
|
38
38
|
return await this.innerQuery(ctx, { ...options, filter: options.filter ? (0, import_utils.transformFilter)(options.filter) : null });
|
|
39
39
|
}
|
|
40
|
+
async query(ctx, options) {
|
|
41
|
+
return await this.innerQuery(ctx, { ...options, filter: options.filter });
|
|
42
|
+
}
|
|
40
43
|
provideWorkContextResolveStrategy() {
|
|
41
44
|
return async (ctx, contextItem) => {
|
|
42
45
|
if (!contextItem.content) {
|
|
@@ -57,6 +60,7 @@ class AIContextDatasourceManager {
|
|
|
57
60
|
this.plugin.log.warn(`Datasource ${datasource} not found`);
|
|
58
61
|
return {
|
|
59
62
|
options,
|
|
63
|
+
total: 0,
|
|
60
64
|
records: []
|
|
61
65
|
};
|
|
62
66
|
}
|
|
@@ -65,6 +69,7 @@ class AIContextDatasourceManager {
|
|
|
65
69
|
this.plugin.log.warn(`Collection ${collectionName} not found`);
|
|
66
70
|
return {
|
|
67
71
|
options,
|
|
72
|
+
total: 0,
|
|
68
73
|
records: []
|
|
69
74
|
};
|
|
70
75
|
}
|
|
@@ -78,6 +83,7 @@ class AIContextDatasourceManager {
|
|
|
78
83
|
if (!can || typeof can !== "object") {
|
|
79
84
|
return {
|
|
80
85
|
options,
|
|
86
|
+
total: 0,
|
|
81
87
|
records: []
|
|
82
88
|
};
|
|
83
89
|
}
|
|
@@ -113,8 +119,9 @@ class AIContextDatasourceManager {
|
|
|
113
119
|
}
|
|
114
120
|
});
|
|
115
121
|
}
|
|
116
|
-
const { fields, filter, sort, limit } = options;
|
|
117
|
-
const result = await collection.repository.find({ fields, filter, sort, limit });
|
|
122
|
+
const { fields, filter, sort, offset, limit } = options;
|
|
123
|
+
const result = await collection.repository.find({ fields, filter, sort, offset: offset ?? 0, limit });
|
|
124
|
+
const total = await collection.repository.count({ fields, filter });
|
|
118
125
|
const records = result.map(
|
|
119
126
|
(x) => fields.map((field) => {
|
|
120
127
|
var _a2;
|
|
@@ -129,6 +136,7 @@ class AIContextDatasourceManager {
|
|
|
129
136
|
);
|
|
130
137
|
return {
|
|
131
138
|
options,
|
|
139
|
+
total,
|
|
132
140
|
records
|
|
133
141
|
};
|
|
134
142
|
}
|
package/dist/server/plugin.js
CHANGED
|
@@ -66,6 +66,7 @@ var import_aiContextDatasources = require("./resource/aiContextDatasources");
|
|
|
66
66
|
var import_work_context_handler = require("./manager/work-context-handler");
|
|
67
67
|
var import_ai_coding_manager = require("./manager/ai-coding-manager");
|
|
68
68
|
var import_code_editor = require("./tools/code-editor");
|
|
69
|
+
var import_datasource_query = require("./tools/datasource-query");
|
|
69
70
|
class PluginAIServer extends import_server.Plugin {
|
|
70
71
|
features = new import_ai_feature_manager.AIPluginFeatureManagerImpl();
|
|
71
72
|
aiManager = new import_ai_manager.AIManager(this);
|
|
@@ -111,6 +112,7 @@ class PluginAIServer extends import_server.Plugin {
|
|
|
111
112
|
const dataModelingGroupName = "dataModeling";
|
|
112
113
|
const workflowGroupName = "workflowCaller";
|
|
113
114
|
const codeEditorGroupName = "codeEditor";
|
|
115
|
+
const dataSourceGroupName = "dataSource";
|
|
114
116
|
toolManager.registerToolGroup({
|
|
115
117
|
groupName: frontendGroupName,
|
|
116
118
|
title: '{{t("Frontend")}}',
|
|
@@ -131,6 +133,11 @@ class PluginAIServer extends import_server.Plugin {
|
|
|
131
133
|
title: '{{t("CodeEditor")}}',
|
|
132
134
|
description: '{{t("CodeEditor actions")}}'
|
|
133
135
|
});
|
|
136
|
+
toolManager.registerToolGroup({
|
|
137
|
+
groupName: dataSourceGroupName,
|
|
138
|
+
title: '{{t("DataSource")}}',
|
|
139
|
+
description: '{{t("Data source query")}}'
|
|
140
|
+
});
|
|
134
141
|
this.aiManager.toolManager.registerTools([
|
|
135
142
|
{
|
|
136
143
|
groupName: frontendGroupName,
|
|
@@ -162,6 +169,14 @@ class PluginAIServer extends import_server.Plugin {
|
|
|
162
169
|
{
|
|
163
170
|
groupName: codeEditorGroupName,
|
|
164
171
|
tool: import_code_editor.getCodeSnippet
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
groupName: dataSourceGroupName,
|
|
175
|
+
tool: import_datasource_query.dataSourceCounting
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
groupName: dataSourceGroupName,
|
|
179
|
+
tool: import_datasource_query.dataSourceQuery
|
|
165
180
|
}
|
|
166
181
|
]);
|
|
167
182
|
toolManager.registerDynamicTool({
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { Context, Next } from '@nocobase/actions';
|
|
10
10
|
export declare const list: (ctx: Context, next: Next) => Promise<void>;
|
|
11
|
+
export declare const create: (ctx: Context, next: Next) => Promise<void>;
|
|
11
12
|
export declare const listByUser: (ctx: Context, next: Next) => Promise<any>;
|
|
12
13
|
export declare const updateUserPrompt: (ctx: Context, next: Next) => Promise<any>;
|
|
13
14
|
export declare const getTemplates: (ctx: Context, next: Next) => Promise<void>;
|
|
@@ -36,6 +36,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
36
36
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
37
37
|
var aiEmployees_exports = {};
|
|
38
38
|
__export(aiEmployees_exports, {
|
|
39
|
+
create: () => create,
|
|
39
40
|
getTemplates: () => getTemplates,
|
|
40
41
|
list: () => list,
|
|
41
42
|
listByUser: () => listByUser,
|
|
@@ -44,6 +45,7 @@ __export(aiEmployees_exports, {
|
|
|
44
45
|
module.exports = __toCommonJS(aiEmployees_exports);
|
|
45
46
|
var import_actions = __toESM(require("@nocobase/actions"));
|
|
46
47
|
var templates = __toESM(require("../ai-employees/templates"));
|
|
48
|
+
var import_lodash = __toESM(require("lodash"));
|
|
47
49
|
const list = async (ctx, next) => {
|
|
48
50
|
const { paginate } = ctx.action.params || {};
|
|
49
51
|
const plugin = ctx.app.pm.get("ai");
|
|
@@ -62,6 +64,22 @@ const list = async (ctx, next) => {
|
|
|
62
64
|
});
|
|
63
65
|
await next();
|
|
64
66
|
};
|
|
67
|
+
const create = async (ctx, next) => {
|
|
68
|
+
const { skillSettings } = ctx.action.params.values ?? {};
|
|
69
|
+
const skills = skillSettings.skills ?? [];
|
|
70
|
+
skills.push(
|
|
71
|
+
{
|
|
72
|
+
name: "dataSource-dataSourceCounting",
|
|
73
|
+
autoCall: true
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "dataSource-dataSourceQuery",
|
|
77
|
+
autoCall: true
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
skillSettings.skills = import_lodash.default.uniqBy(skills, "name");
|
|
81
|
+
await import_actions.default.create(ctx, next);
|
|
82
|
+
};
|
|
65
83
|
const listByUser = async (ctx, next) => {
|
|
66
84
|
const plugin = ctx.app.pm.get("ai");
|
|
67
85
|
const user = ctx.auth.user;
|
|
@@ -168,6 +186,7 @@ const getTemplates = async (ctx, next) => {
|
|
|
168
186
|
};
|
|
169
187
|
// Annotate the CommonJS export names for ESM import in node:
|
|
170
188
|
0 && (module.exports = {
|
|
189
|
+
create,
|
|
171
190
|
getTemplates,
|
|
172
191
|
list,
|
|
173
192
|
listByUser,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
import { ToolOptions } from '../manager/tool-manager';
|
|
10
|
+
export declare const dataSourceQuery: ToolOptions;
|
|
11
|
+
export declare const dataSourceCounting: ToolOptions;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __export = (target, all) => {
|
|
15
|
+
for (var name in all)
|
|
16
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
|
+
};
|
|
18
|
+
var __copyProps = (to, from, except, desc) => {
|
|
19
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
20
|
+
for (let key of __getOwnPropNames(from))
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
22
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
23
|
+
}
|
|
24
|
+
return to;
|
|
25
|
+
};
|
|
26
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
|
+
var datasource_query_exports = {};
|
|
28
|
+
__export(datasource_query_exports, {
|
|
29
|
+
dataSourceCounting: () => dataSourceCounting,
|
|
30
|
+
dataSourceQuery: () => dataSourceQuery
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(datasource_query_exports);
|
|
33
|
+
var import_zod = require("zod");
|
|
34
|
+
const ArgSchema = import_zod.z.object({
|
|
35
|
+
datasource: import_zod.z.string().describe('{{t("Data source key")}}'),
|
|
36
|
+
collectionName: import_zod.z.string().describe('{{t("Collection name")}}'),
|
|
37
|
+
fields: import_zod.z.array(import_zod.z.string()).describe('{{t("Fields to be queried")}}'),
|
|
38
|
+
appends: import_zod.z.array(import_zod.z.string()).describe('{{t("Related collection to be queried")}}'),
|
|
39
|
+
filter: import_zod.z.object({}).catchall(import_zod.z.any()).describe(`# Parameters definition
|
|
40
|
+
\`\`\`
|
|
41
|
+
export type QueryCondition = {
|
|
42
|
+
[field: string]: { // \`field\` key is Field name
|
|
43
|
+
[operator: string]: any; // \`operator\` key is Query condition operator.
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
export type QueryObject =
|
|
49
|
+
| {
|
|
50
|
+
[logic: string]: (QueryCondition | QueryObject)[]; // \`logic\` key is the relationship between query conditions, should be one of '$and', '$or', the value is recursion object array, item in array can be QueryCondition or QueryObject
|
|
51
|
+
}
|
|
52
|
+
| QueryCondition // QueryCondition definition above;
|
|
53
|
+
\`\`\`
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
// QueryCondition examples
|
|
57
|
+
|
|
58
|
+
\`\`\`
|
|
59
|
+
const example1: QueryCondition = {
|
|
60
|
+
age: { $gt: 18 }, // age great than 18
|
|
61
|
+
name: { $like: '%John%' }, // name include John
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const example2: QueryCondition = {
|
|
65
|
+
status: { $in: ['active', 'pending'] }, // status is active or pending
|
|
66
|
+
};
|
|
67
|
+
\`\`\`
|
|
68
|
+
|
|
69
|
+
// QueryObject example
|
|
70
|
+
\`\`\`
|
|
71
|
+
const example1: QueryObject = {
|
|
72
|
+
$and: [
|
|
73
|
+
{ age: { $gt: 18 } },
|
|
74
|
+
{ status: { $eq: 'active' } }
|
|
75
|
+
]
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const example2: QueryObject = {
|
|
79
|
+
$or: [
|
|
80
|
+
{ name: { $like: '%John%' } },
|
|
81
|
+
{
|
|
82
|
+
$and: [
|
|
83
|
+
{ age: { $gte: 30 } },
|
|
84
|
+
{ status: { $eq: 'pending' } }
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const example3: QueryObject = { age: { $lt: 50 } };
|
|
91
|
+
\`\`\`
|
|
92
|
+
`),
|
|
93
|
+
sort: import_zod.z.array(import_zod.z.string()).describe(
|
|
94
|
+
'{{t("Sort field names. By default, they are in ascending order. A minus sign before the field name indicates descending order")}}'
|
|
95
|
+
),
|
|
96
|
+
offset: import_zod.z.number().describe('{{t("Offset of records to be queried")}}'),
|
|
97
|
+
limit: import_zod.z.number().describe('{{t("Maximum number of records to be queried")}}')
|
|
98
|
+
});
|
|
99
|
+
const dataSourceQuery = {
|
|
100
|
+
name: "dataSourceQuery",
|
|
101
|
+
title: '{{t("Data source query")}}',
|
|
102
|
+
description: '{{t("Use dataSource, collectionName, and collection fields to query data from the database")}}',
|
|
103
|
+
schema: ArgSchema,
|
|
104
|
+
invoke: async (ctx, args) => {
|
|
105
|
+
const plugin = ctx.app.pm.get("ai");
|
|
106
|
+
const content = await plugin.aiContextDatasourceManager.query(ctx, args);
|
|
107
|
+
return {
|
|
108
|
+
status: "success",
|
|
109
|
+
content: JSON.stringify(content)
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const dataSourceCounting = {
|
|
114
|
+
name: "dataSourceCounting",
|
|
115
|
+
title: '{{t("Data source records counting")}}',
|
|
116
|
+
description: '{{t("Use dataSource, collectionName, and collection fields to query data from the database, get total count of records")}}',
|
|
117
|
+
schema: ArgSchema,
|
|
118
|
+
invoke: async (ctx, args) => {
|
|
119
|
+
const plugin = ctx.app.pm.get("ai");
|
|
120
|
+
args.offset = 0;
|
|
121
|
+
args.limit = 1;
|
|
122
|
+
const content = await plugin.aiContextDatasourceManager.query(ctx, args);
|
|
123
|
+
return {
|
|
124
|
+
status: "success",
|
|
125
|
+
content: String((content == null ? void 0 : content.total) ?? 0)
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
130
|
+
0 && (module.exports = {
|
|
131
|
+
dataSourceCounting,
|
|
132
|
+
dataSourceQuery
|
|
133
|
+
});
|
package/dist/server/utils.js
CHANGED
|
@@ -56,8 +56,8 @@ function stripToolCallTags(content) {
|
|
|
56
56
|
function parseResponseMessage(row) {
|
|
57
57
|
const { content: rawContent, messageId, metadata, role, toolCalls, attachments, workContext } = row;
|
|
58
58
|
const content = {
|
|
59
|
-
...rawContent,
|
|
60
|
-
content: stripToolCallTags(rawContent.content),
|
|
59
|
+
...rawContent ?? {},
|
|
60
|
+
content: stripToolCallTags(rawContent == null ? void 0 : rawContent.content),
|
|
61
61
|
messageId,
|
|
62
62
|
metadata,
|
|
63
63
|
attachments,
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"displayName.zh-CN": "AI 员工",
|
|
5
5
|
"description": "Create AI employees with diverse skills to collaborate with humans, build systems, and handle business operations.",
|
|
6
6
|
"description.zh-CN": "创建各种技能的 AI 员工,与人类协同,搭建系统,处理业务。",
|
|
7
|
-
"version": "2.0.0-alpha.
|
|
7
|
+
"version": "2.0.0-alpha.27",
|
|
8
8
|
"main": "dist/server/index.js",
|
|
9
9
|
"peerDependencies": {
|
|
10
10
|
"@nocobase/client": "2.x",
|
|
@@ -42,5 +42,5 @@
|
|
|
42
42
|
"keywords": [
|
|
43
43
|
"AI"
|
|
44
44
|
],
|
|
45
|
-
"gitHead": "
|
|
45
|
+
"gitHead": "bb67b95bc49f8d808209e635296ba15a1bd72f72"
|
|
46
46
|
}
|
|
File without changes
|
|
File without changes
|