plugin-agent-orchestrator 1.0.27 → 1.0.32
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/README.md +9 -7
- package/dist/client/index.js +1 -1
- package/dist/client-v2/{214.723affb37c13bf7a.js → 214.79650a549273f163.js} +1 -1
- package/dist/client-v2/264.718a107e43fc163c.js +10 -0
- package/dist/client-v2/373.f5d5292e53c4e832.js +10 -0
- package/dist/client-v2/{41.1805b2edfaa4afe2.js → 41.ba6e080cc0488143.js} +1 -1
- package/dist/client-v2/418.29e713f79131eece.js +10 -0
- package/dist/client-v2/619.bd3c5698b40705c3.js +10 -0
- package/dist/client-v2/677.a991ce0250ff5c77.js +10 -0
- package/dist/client-v2/{70.a15d7fcec7c41768.js → 70.bda9518881c05360.js} +1 -1
- package/dist/client-v2/925.f5370de8f6632d65.js +10 -0
- package/dist/client-v2/index.js +1 -1
- package/dist/externalVersion.js +7 -10
- package/dist/locale/en-US.json +94 -25
- package/dist/locale/vi-VN.json +94 -25
- package/dist/locale/zh-CN.json +94 -25
- package/dist/server/collections/agent-execution-spans.js +37 -0
- package/dist/server/collections/agent-harness-profiles.js +2 -2
- package/dist/server/collections/agent-memory-contexts.js +125 -0
- package/dist/server/collections/orchestrator-logs.js +2 -2
- package/dist/server/migrations/20260425000000-add-interaction-schema.js +3 -1
- package/dist/server/migrations/20260427000000-change-packages-to-text.js +3 -1
- package/dist/server/migrations/20260427000001-change-other-json-to-text.js +6 -2
- package/dist/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.js +21 -19
- package/dist/server/migrations/20260621000000-native-policy-profile-defaults.js +193 -0
- package/dist/server/plugin.js +128 -74
- package/dist/server/resources/agent-monitor.js +454 -0
- package/dist/server/services/AgentHarness.js +24 -499
- package/dist/server/services/AgentMemoryContextService.js +216 -0
- package/dist/server/services/ExecutionSpanService.js +2 -2
- package/dist/server/services/NativeSubAgentObserver.js +413 -0
- package/dist/server/skill-hub/mcp/McpController.js +16 -5
- package/dist/server/skill-hub/plugin.js +81 -5
- package/dist/server/skill-hub/tasks/SkillExecutionTask.js +9 -3
- package/dist/server/tools/delegate-task.js +11 -589
- package/dist/server/utils/skill-settings.js +18 -1
- package/package.json +47 -49
- package/src/client/AIEmployeesContext.tsx +5 -18
- package/src/client/AgentRunsTab.tsx +2 -771
- package/src/client/HarnessProfilesTab.tsx +2 -257
- package/src/client/OrchestratorSettings.tsx +97 -106
- package/src/client/RulesTab.tsx +2 -788
- package/src/client/plugin.tsx +0 -2
- package/src/client/skill-hub/components/ExecutionHistory.tsx +200 -202
- package/src/client/skill-hub/components/ExecutionProgress.tsx +51 -55
- package/src/client/skill-hub/components/LoopSettings.tsx +331 -331
- package/src/client/skill-hub/components/SkillEditor.tsx +43 -39
- package/src/client/skill-hub/components/SkillManager.tsx +194 -181
- package/src/client/skill-hub/components/SkillTestPanel.tsx +141 -145
- package/src/client/skill-hub/locale.ts +16 -16
- package/src/client/skill-hub/tools/SkillHubCard.tsx +104 -109
- package/src/client/skill-hub/tools/loopTemplates.ts +52 -52
- package/src/client/skill-hub/utils/jsonFields.ts +7 -3
- package/src/client-v2/components/AIEmployeesContext.tsx +3 -16
- package/src/client-v2/components/AgentRunsTab.tsx +182 -455
- package/src/client-v2/components/HarnessProfilesTab.tsx +34 -31
- package/src/client-v2/components/RulesTab.tsx +2 -782
- package/src/client-v2/components/TracingTab.tsx +1 -1
- package/src/client-v2/hooks/useApiRequest.ts +8 -1
- package/src/client-v2/pages/RulesPage.tsx +2 -2
- package/src/client-v2/plugin.tsx +3 -3
- package/src/locale/en-US.json +94 -25
- package/src/locale/vi-VN.json +94 -25
- package/src/locale/zh-CN.json +94 -25
- package/src/server/__tests__/native-sub-agent-observer.test.ts +246 -0
- package/src/server/__tests__/skill-settings.test.ts +6 -6
- package/src/server/__tests__/smoke.test.ts +1 -0
- package/src/server/collections/agent-execution-spans.ts +37 -0
- package/src/server/collections/agent-harness-profiles.ts +59 -59
- package/src/server/collections/agent-loop-events.ts +71 -71
- package/src/server/collections/agent-loop-steps.ts +144 -144
- package/src/server/collections/agent-memory-contexts.ts +95 -0
- package/src/server/collections/orchestrator-logs.ts +4 -4
- package/src/server/collections/skill-definitions.ts +111 -111
- package/src/server/collections/skill-executions.ts +106 -106
- package/src/server/collections/skill-loop-configs.ts +65 -65
- package/src/server/migrations/20260423000000-add-progress-fields.ts +14 -14
- package/src/server/migrations/20260425000000-add-interaction-schema.ts +3 -1
- package/src/server/migrations/20260427000000-change-packages-to-text.ts +4 -2
- package/src/server/migrations/20260427000001-change-other-json-to-text.ts +9 -5
- package/src/server/migrations/20260524000000-add-agent-loop-fields-to-skill-executions.ts +30 -30
- package/src/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.ts +145 -142
- package/src/server/migrations/20260615000000-normalize-ai-employee-tool-bindings.ts +2 -2
- package/src/server/migrations/20260621000000-native-policy-profile-defaults.ts +193 -0
- package/src/server/plugin.ts +151 -94
- package/src/server/resources/agent-monitor.ts +482 -0
- package/src/server/services/AgentHarness.ts +38 -623
- package/src/server/services/AgentMemoryContextService.ts +256 -0
- package/src/server/services/AgentPlanValidator.ts +73 -73
- package/src/server/services/ExecutionSpanService.ts +6 -2
- package/src/server/services/FileManager.ts +144 -144
- package/src/server/services/NativeSubAgentObserver.ts +507 -0
- package/src/server/services/SkillManager.ts +583 -583
- package/src/server/services/SkillRepositoryService.ts +5 -7
- package/src/server/services/TokenTracker.ts +3 -3
- package/src/server/services/WorkerEnvManager.ts +1 -2
- package/src/server/skill-hub/actions/git-import.ts +5 -7
- package/src/server/skill-hub/mcp/McpController.ts +41 -14
- package/src/server/skill-hub/plugin.ts +89 -6
- package/src/server/skill-hub/tasks/SkillExecutionTask.ts +470 -460
- package/src/server/skill-hub/utils/json-fields.ts +1 -1
- package/src/server/tools/delegate-task.ts +13 -847
- package/src/server/utils/skill-settings.ts +24 -6
- package/dist/client-v2/264.0533912e6c5ea2d7.js +0 -10
- package/dist/client-v2/418.5ae055abf141820e.js +0 -10
- package/dist/client-v2/619.d99d3c9e61c99064.js +0 -10
- package/dist/client-v2/892.72db4161511c8a16.js +0 -10
- package/dist/client-v2/926.87f660b670d85bcc.js +0 -10
- package/src/client/tools/PlanApprovalCard.tsx +0 -176
- package/src/client/tools/registerOrchestratorCards.ts +0 -17
|
@@ -1,583 +1,583 @@
|
|
|
1
|
-
import { Database } from '@nocobase/database';
|
|
2
|
-
import { stringifyJsonText } from '../skill-hub/utils/json-fields';
|
|
3
|
-
|
|
4
|
-
export class SkillManager {
|
|
5
|
-
constructor(private db: Database) {}
|
|
6
|
-
|
|
7
|
-
async seedDefaults() {
|
|
8
|
-
const repo = this.db.getRepository('skillDefinitions');
|
|
9
|
-
|
|
10
|
-
const seeds = [
|
|
11
|
-
{
|
|
12
|
-
name: 'generate-word-report',
|
|
13
|
-
title: 'Generate Word Report',
|
|
14
|
-
description: 'Generate a Word document (.docx) with title, content, and optional table data.',
|
|
15
|
-
language: 'python',
|
|
16
|
-
codeTemplate: SEED_WORD_REPORT,
|
|
17
|
-
inputSchema: {
|
|
18
|
-
type: 'object',
|
|
19
|
-
properties: {
|
|
20
|
-
title: { type: 'string', description: 'Report title' },
|
|
21
|
-
content: { type: 'string', description: 'Report body text' },
|
|
22
|
-
tableData: {
|
|
23
|
-
type: 'array',
|
|
24
|
-
items: { type: 'object' },
|
|
25
|
-
description: 'Optional array of objects for table rows',
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
required: ['title', 'content'],
|
|
29
|
-
},
|
|
30
|
-
packages: ['python-docx'],
|
|
31
|
-
timeoutSeconds: 30,
|
|
32
|
-
enabled: true,
|
|
33
|
-
toolScope: 'CUSTOM',
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
name: 'generate-excel',
|
|
37
|
-
title: 'Generate Excel Spreadsheet',
|
|
38
|
-
description: 'Generate an Excel file (.xlsx) with headers and row data.',
|
|
39
|
-
language: 'python',
|
|
40
|
-
codeTemplate: SEED_EXCEL,
|
|
41
|
-
inputSchema: {
|
|
42
|
-
type: 'object',
|
|
43
|
-
properties: {
|
|
44
|
-
sheetName: { type: 'string', description: 'Sheet name', default: 'Sheet1' },
|
|
45
|
-
headers: { type: 'array', items: { type: 'string' }, description: 'Column headers' },
|
|
46
|
-
rows: { type: 'array', items: { type: 'array' }, description: 'Row data (array of arrays)' },
|
|
47
|
-
},
|
|
48
|
-
required: ['headers', 'rows'],
|
|
49
|
-
},
|
|
50
|
-
packages: ['openpyxl'],
|
|
51
|
-
timeoutSeconds: 30,
|
|
52
|
-
enabled: true,
|
|
53
|
-
toolScope: 'CUSTOM',
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
name: 'generate-pdf-report',
|
|
57
|
-
title: 'Generate PDF Report',
|
|
58
|
-
description:
|
|
59
|
-
'Generate a professional PDF report with title, sections, and optional table. Suitable for CRM reports, customer summaries, and sales reports.',
|
|
60
|
-
language: 'python',
|
|
61
|
-
codeTemplate: SEED_PDF_REPORT,
|
|
62
|
-
inputSchema: {
|
|
63
|
-
type: 'object',
|
|
64
|
-
properties: {
|
|
65
|
-
title: { type: 'string', description: 'Report title' },
|
|
66
|
-
sections: {
|
|
67
|
-
type: 'array',
|
|
68
|
-
items: {
|
|
69
|
-
type: 'object',
|
|
70
|
-
properties: {
|
|
71
|
-
heading: { type: 'string' },
|
|
72
|
-
body: { type: 'string' },
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
description: 'Array of {heading, body} sections',
|
|
76
|
-
},
|
|
77
|
-
tableData: {
|
|
78
|
-
type: 'array',
|
|
79
|
-
items: { type: 'object' },
|
|
80
|
-
description: 'Optional array of objects for a summary table',
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
required: ['title', 'sections'],
|
|
84
|
-
},
|
|
85
|
-
packages: ['reportlab'],
|
|
86
|
-
timeoutSeconds: 30,
|
|
87
|
-
enabled: true,
|
|
88
|
-
toolScope: 'CUSTOM',
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
name: 'generate-chart-image',
|
|
92
|
-
title: 'Generate Chart Image',
|
|
93
|
-
description:
|
|
94
|
-
'Generate a chart image (PNG) from data. Supports bar, line, pie charts. Useful for visualizing CRM metrics like sales, leads, revenue.',
|
|
95
|
-
language: 'python',
|
|
96
|
-
codeTemplate: SEED_CHART,
|
|
97
|
-
inputSchema: {
|
|
98
|
-
type: 'object',
|
|
99
|
-
properties: {
|
|
100
|
-
chartType: {
|
|
101
|
-
type: 'string',
|
|
102
|
-
enum: ['bar', 'line', 'pie'],
|
|
103
|
-
description: 'Chart type',
|
|
104
|
-
},
|
|
105
|
-
title: { type: 'string', description: 'Chart title' },
|
|
106
|
-
labels: { type: 'array', items: { type: 'string' }, description: 'X-axis labels or pie labels' },
|
|
107
|
-
values: { type: 'array', items: { type: 'number' }, description: 'Data values' },
|
|
108
|
-
xlabel: { type: 'string', description: 'X-axis label (bar/line only)' },
|
|
109
|
-
ylabel: { type: 'string', description: 'Y-axis label (bar/line only)' },
|
|
110
|
-
},
|
|
111
|
-
required: ['chartType', 'title', 'labels', 'values'],
|
|
112
|
-
},
|
|
113
|
-
packages: ['matplotlib'],
|
|
114
|
-
timeoutSeconds: 30,
|
|
115
|
-
enabled: true,
|
|
116
|
-
toolScope: 'CUSTOM',
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
name: 'generate-invoice-pdf',
|
|
120
|
-
title: 'Generate Invoice PDF',
|
|
121
|
-
description:
|
|
122
|
-
'Generate a professional invoice PDF with company info, line items, and totals. For CRM billing and quotation workflows.',
|
|
123
|
-
language: 'python',
|
|
124
|
-
codeTemplate: SEED_INVOICE,
|
|
125
|
-
inputSchema: {
|
|
126
|
-
type: 'object',
|
|
127
|
-
properties: {
|
|
128
|
-
invoiceNumber: { type: 'string', description: 'Invoice number' },
|
|
129
|
-
date: { type: 'string', description: 'Invoice date (YYYY-MM-DD)' },
|
|
130
|
-
companyName: { type: 'string', description: 'Your company name' },
|
|
131
|
-
customerName: { type: 'string', description: 'Customer name' },
|
|
132
|
-
customerAddress: { type: 'string', description: 'Customer address' },
|
|
133
|
-
items: {
|
|
134
|
-
type: 'array',
|
|
135
|
-
items: {
|
|
136
|
-
type: 'object',
|
|
137
|
-
properties: {
|
|
138
|
-
description: { type: 'string' },
|
|
139
|
-
quantity: { type: 'number' },
|
|
140
|
-
unitPrice: { type: 'number' },
|
|
141
|
-
},
|
|
142
|
-
},
|
|
143
|
-
description: 'Line items with description, quantity, unitPrice',
|
|
144
|
-
},
|
|
145
|
-
currency: { type: 'string', description: 'Currency symbol', default: '$' },
|
|
146
|
-
notes: { type: 'string', description: 'Optional notes or payment terms' },
|
|
147
|
-
},
|
|
148
|
-
required: ['invoiceNumber', 'date', 'companyName', 'customerName', 'items'],
|
|
149
|
-
},
|
|
150
|
-
packages: ['reportlab'],
|
|
151
|
-
timeoutSeconds: 30,
|
|
152
|
-
enabled: true,
|
|
153
|
-
toolScope: 'CUSTOM',
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
name: 'data-summary-report',
|
|
157
|
-
title: 'Data Summary & Analysis Report',
|
|
158
|
-
description:
|
|
159
|
-
'Analyze tabular data and generate a Word report with summary statistics, top records, and insights. Ideal for CRM data analysis — leads, deals, customers.',
|
|
160
|
-
language: 'python',
|
|
161
|
-
codeTemplate: SEED_DATA_SUMMARY,
|
|
162
|
-
inputSchema: {
|
|
163
|
-
type: 'object',
|
|
164
|
-
properties: {
|
|
165
|
-
title: { type: 'string', description: 'Report title' },
|
|
166
|
-
data: {
|
|
167
|
-
type: 'array',
|
|
168
|
-
items: { type: 'object' },
|
|
169
|
-
description: 'Array of data objects (e.g. deals, leads, customers)',
|
|
170
|
-
},
|
|
171
|
-
groupByField: { type: 'string', description: 'Optional field to group and summarize by' },
|
|
172
|
-
sortByField: { type: 'string', description: 'Optional field to sort by (descending)' },
|
|
173
|
-
topN: { type: 'number', description: 'Number of top records to include', default: 10 },
|
|
174
|
-
},
|
|
175
|
-
required: ['title', 'data'],
|
|
176
|
-
},
|
|
177
|
-
packages: ['pandas', 'python-docx'],
|
|
178
|
-
timeoutSeconds: 60,
|
|
179
|
-
enabled: true,
|
|
180
|
-
toolScope: 'CUSTOM',
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
name: 'data-transform',
|
|
184
|
-
title: 'Data Transform (CSV/JSON)',
|
|
185
|
-
description: 'Transform data array to CSV or JSON file for download.',
|
|
186
|
-
language: 'node',
|
|
187
|
-
codeTemplate: SEED_DATA_TRANSFORM,
|
|
188
|
-
inputSchema: {
|
|
189
|
-
type: 'object',
|
|
190
|
-
properties: {
|
|
191
|
-
data: { type: 'array', items: { type: 'object' }, description: 'Array of data objects' },
|
|
192
|
-
format: { type: 'string', enum: ['csv', 'json'], description: 'Output format' },
|
|
193
|
-
filename: { type: 'string', description: 'Output filename (without extension)' },
|
|
194
|
-
},
|
|
195
|
-
required: ['data', 'format'],
|
|
196
|
-
},
|
|
197
|
-
packages: [],
|
|
198
|
-
timeoutSeconds: 30,
|
|
199
|
-
enabled: true,
|
|
200
|
-
toolScope: 'CUSTOM',
|
|
201
|
-
},
|
|
202
|
-
];
|
|
203
|
-
|
|
204
|
-
for (const seed of seeds) {
|
|
205
|
-
try {
|
|
206
|
-
const count = await repo.count({ filter: { name: seed.name } });
|
|
207
|
-
if (count === 0) {
|
|
208
|
-
await repo.create({
|
|
209
|
-
values: {
|
|
210
|
-
...seed,
|
|
211
|
-
inputSchema: stringifyJsonText(seed.inputSchema),
|
|
212
|
-
packages: stringifyJsonText(seed.packages, []),
|
|
213
|
-
},
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
} catch (err) {
|
|
217
|
-
console.error(`[import-skill] Failed to insert ${seed.name}:`, err);
|
|
218
|
-
// Skip if already exists (unique constraint on name)
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// ─── Seed Code Templates ───
|
|
225
|
-
|
|
226
|
-
const SEED_WORD_REPORT = `import os, json
|
|
227
|
-
from docx import Document
|
|
228
|
-
|
|
229
|
-
title = json.loads('''{{title}}''') if '{{title}}'.startswith('"') else '{{title}}'
|
|
230
|
-
content = json.loads('''{{content}}''') if '{{content}}'.startswith('"') else '{{content}}'
|
|
231
|
-
|
|
232
|
-
doc = Document()
|
|
233
|
-
doc.add_heading(title, 0)
|
|
234
|
-
doc.add_paragraph(content)
|
|
235
|
-
|
|
236
|
-
table_data_raw = '''{{tableData}}'''
|
|
237
|
-
if table_data_raw and table_data_raw != '{{' + 'tableData}}':
|
|
238
|
-
table_data = json.loads(table_data_raw)
|
|
239
|
-
if table_data and len(table_data) > 0:
|
|
240
|
-
headers = list(table_data[0].keys())
|
|
241
|
-
table = doc.add_table(rows=1, cols=len(headers))
|
|
242
|
-
table.style = 'Light Grid Accent 1'
|
|
243
|
-
for i, header in enumerate(headers):
|
|
244
|
-
table.rows[0].cells[i].text = str(header)
|
|
245
|
-
for row_data in table_data:
|
|
246
|
-
row = table.add_row()
|
|
247
|
-
for i, header in enumerate(headers):
|
|
248
|
-
row.cells[i].text = str(row_data.get(header, ''))
|
|
249
|
-
|
|
250
|
-
output_dir = os.environ.get('OUTPUT_DIR', '/output')
|
|
251
|
-
filepath = os.path.join(output_dir, 'report.docx')
|
|
252
|
-
doc.save(filepath)
|
|
253
|
-
print(f'Generated: report.docx')
|
|
254
|
-
`;
|
|
255
|
-
|
|
256
|
-
const SEED_EXCEL = `import os, json
|
|
257
|
-
import openpyxl
|
|
258
|
-
|
|
259
|
-
sheet_name_raw = '{{sheetName}}'
|
|
260
|
-
sheet_name = sheet_name_raw if sheet_name_raw != '{{' + 'sheetName}}' else 'Sheet1'
|
|
261
|
-
headers = json.loads('''{{headers}}''')
|
|
262
|
-
rows = json.loads('''{{rows}}''')
|
|
263
|
-
|
|
264
|
-
wb = openpyxl.Workbook()
|
|
265
|
-
ws = wb.active
|
|
266
|
-
ws.title = sheet_name
|
|
267
|
-
|
|
268
|
-
for col, header in enumerate(headers, 1):
|
|
269
|
-
ws.cell(row=1, column=col, value=header)
|
|
270
|
-
|
|
271
|
-
for row_idx, row_data in enumerate(rows, 2):
|
|
272
|
-
for col_idx, value in enumerate(row_data, 1):
|
|
273
|
-
ws.cell(row=row_idx, column=col_idx, value=value)
|
|
274
|
-
|
|
275
|
-
output_dir = os.environ.get('OUTPUT_DIR', '/output')
|
|
276
|
-
filepath = os.path.join(output_dir, 'data.xlsx')
|
|
277
|
-
wb.save(filepath)
|
|
278
|
-
print(f'Generated: data.xlsx')
|
|
279
|
-
`;
|
|
280
|
-
|
|
281
|
-
const SEED_PDF_REPORT = `import os, json
|
|
282
|
-
from reportlab.lib.pagesizes import A4
|
|
283
|
-
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
|
284
|
-
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
|
|
285
|
-
from reportlab.lib import colors
|
|
286
|
-
from reportlab.lib.units import cm
|
|
287
|
-
|
|
288
|
-
title = json.loads('''{{title}}''') if '{{title}}'.startswith('"') else '{{title}}'
|
|
289
|
-
sections = json.loads('''{{sections}}''')
|
|
290
|
-
|
|
291
|
-
output_dir = os.environ.get('OUTPUT_DIR', '/output')
|
|
292
|
-
filepath = os.path.join(output_dir, 'report.pdf')
|
|
293
|
-
|
|
294
|
-
doc = SimpleDocTemplate(filepath, pagesize=A4,
|
|
295
|
-
topMargin=2*cm, bottomMargin=2*cm, leftMargin=2*cm, rightMargin=2*cm)
|
|
296
|
-
styles = getSampleStyleSheet()
|
|
297
|
-
title_style = ParagraphStyle('CustomTitle', parent=styles['Title'], fontSize=22, spaceAfter=20)
|
|
298
|
-
heading_style = ParagraphStyle('CustomHeading', parent=styles['Heading2'], fontSize=14, spaceBefore=16, spaceAfter=8)
|
|
299
|
-
body_style = styles['BodyText']
|
|
300
|
-
|
|
301
|
-
story = [Paragraph(title, title_style), Spacer(1, 12)]
|
|
302
|
-
|
|
303
|
-
for section in sections:
|
|
304
|
-
if section.get('heading'):
|
|
305
|
-
story.append(Paragraph(section['heading'], heading_style))
|
|
306
|
-
if section.get('body'):
|
|
307
|
-
for line in section['body'].split('\\n'):
|
|
308
|
-
story.append(Paragraph(line, body_style))
|
|
309
|
-
story.append(Spacer(1, 8))
|
|
310
|
-
|
|
311
|
-
table_data_raw = '''{{tableData}}'''
|
|
312
|
-
if table_data_raw and table_data_raw != '{{' + 'tableData}}':
|
|
313
|
-
td = json.loads(table_data_raw)
|
|
314
|
-
if td and len(td) > 0:
|
|
315
|
-
headers = list(td[0].keys())
|
|
316
|
-
data = [headers] + [[str(r.get(h, '')) for h in headers] for r in td]
|
|
317
|
-
t = Table(data, repeatRows=1)
|
|
318
|
-
t.setStyle(TableStyle([
|
|
319
|
-
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#4472C4')),
|
|
320
|
-
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
|
321
|
-
('FONTSIZE', (0, 0), (-1, 0), 10),
|
|
322
|
-
('FONTSIZE', (0, 1), (-1, -1), 9),
|
|
323
|
-
('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
|
|
324
|
-
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#D9E2F3')]),
|
|
325
|
-
]))
|
|
326
|
-
story.append(Spacer(1, 12))
|
|
327
|
-
story.append(t)
|
|
328
|
-
|
|
329
|
-
doc.build(story)
|
|
330
|
-
print('Generated: report.pdf')
|
|
331
|
-
`;
|
|
332
|
-
|
|
333
|
-
const SEED_CHART = `import os, json
|
|
334
|
-
import matplotlib
|
|
335
|
-
matplotlib.use('Agg')
|
|
336
|
-
import matplotlib.pyplot as plt
|
|
337
|
-
|
|
338
|
-
chart_type = '{{chartType}}'
|
|
339
|
-
title = json.loads('''{{title}}''') if '{{title}}'.startswith('"') else '{{title}}'
|
|
340
|
-
labels = json.loads('''{{labels}}''')
|
|
341
|
-
values = json.loads('''{{values}}''')
|
|
342
|
-
xlabel_raw = '''{{xlabel}}'''
|
|
343
|
-
ylabel_raw = '''{{ylabel}}'''
|
|
344
|
-
xlabel = xlabel_raw if xlabel_raw != '{{' + 'xlabel}}' else ''
|
|
345
|
-
ylabel = ylabel_raw if ylabel_raw != '{{' + 'ylabel}}' else ''
|
|
346
|
-
|
|
347
|
-
fig, ax = plt.subplots(figsize=(10, 6))
|
|
348
|
-
colors = ['#4472C4', '#ED7D31', '#A5A5A5', '#FFC000', '#5B9BD5', '#70AD47', '#264478', '#9B59B6']
|
|
349
|
-
|
|
350
|
-
if chart_type == 'bar':
|
|
351
|
-
bars = ax.bar(labels, values, color=colors[:len(labels)])
|
|
352
|
-
for bar, val in zip(bars, values):
|
|
353
|
-
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + max(values)*0.01,
|
|
354
|
-
f'{val:,.0f}' if isinstance(val, (int, float)) else str(val),
|
|
355
|
-
ha='center', va='bottom', fontsize=9)
|
|
356
|
-
if xlabel: ax.set_xlabel(xlabel)
|
|
357
|
-
if ylabel: ax.set_ylabel(ylabel)
|
|
358
|
-
elif chart_type == 'line':
|
|
359
|
-
ax.plot(labels, values, marker='o', linewidth=2, color='#4472C4', markersize=6)
|
|
360
|
-
for i, val in enumerate(values):
|
|
361
|
-
ax.annotate(f'{val:,.0f}', (labels[i], val), textcoords="offset points",
|
|
362
|
-
xytext=(0, 10), ha='center', fontsize=9)
|
|
363
|
-
if xlabel: ax.set_xlabel(xlabel)
|
|
364
|
-
if ylabel: ax.set_ylabel(ylabel)
|
|
365
|
-
elif chart_type == 'pie':
|
|
366
|
-
ax.pie(values, labels=labels, colors=colors[:len(labels)], autopct='%1.1f%%', startangle=90)
|
|
367
|
-
|
|
368
|
-
ax.set_title(title, fontsize=14, fontweight='bold', pad=15)
|
|
369
|
-
plt.tight_layout()
|
|
370
|
-
|
|
371
|
-
output_dir = os.environ.get('OUTPUT_DIR', '/output')
|
|
372
|
-
filepath = os.path.join(output_dir, 'chart.png')
|
|
373
|
-
fig.savefig(filepath, dpi=150, bbox_inches='tight')
|
|
374
|
-
plt.close()
|
|
375
|
-
print('Generated: chart.png')
|
|
376
|
-
`;
|
|
377
|
-
|
|
378
|
-
const SEED_INVOICE = `import os, json
|
|
379
|
-
from reportlab.lib.pagesizes import A4
|
|
380
|
-
from reportlab.pdfgen import canvas
|
|
381
|
-
from reportlab.lib.units import cm, mm
|
|
382
|
-
from reportlab.lib import colors
|
|
383
|
-
|
|
384
|
-
inv_number = json.loads('''{{invoiceNumber}}''') if '{{invoiceNumber}}'.startswith('"') else '{{invoiceNumber}}'
|
|
385
|
-
date = json.loads('''{{date}}''') if '{{date}}'.startswith('"') else '{{date}}'
|
|
386
|
-
company = json.loads('''{{companyName}}''') if '{{companyName}}'.startswith('"') else '{{companyName}}'
|
|
387
|
-
customer = json.loads('''{{customerName}}''') if '{{customerName}}'.startswith('"') else '{{customerName}}'
|
|
388
|
-
address_raw = '''{{customerAddress}}'''
|
|
389
|
-
address = json.loads(address_raw) if address_raw.startswith('"') else address_raw if address_raw != '{{' + 'customerAddress}}' else ''
|
|
390
|
-
items = json.loads('''{{items}}''')
|
|
391
|
-
currency_raw = '''{{currency}}'''
|
|
392
|
-
currency = currency_raw if currency_raw != '{{' + 'currency}}' else '$'
|
|
393
|
-
notes_raw = '''{{notes}}'''
|
|
394
|
-
notes = json.loads(notes_raw) if notes_raw.startswith('"') else notes_raw if notes_raw != '{{' + 'notes}}' else ''
|
|
395
|
-
|
|
396
|
-
output_dir = os.environ.get('OUTPUT_DIR', '/output')
|
|
397
|
-
filepath = os.path.join(output_dir, f'invoice_{inv_number}.pdf')
|
|
398
|
-
w, h = A4
|
|
399
|
-
c = canvas.Canvas(filepath, pagesize=A4)
|
|
400
|
-
|
|
401
|
-
# Header
|
|
402
|
-
c.setFont('Helvetica-Bold', 24)
|
|
403
|
-
c.setFillColor(colors.HexColor('#2C3E50'))
|
|
404
|
-
c.drawString(2*cm, h - 2.5*cm, 'INVOICE')
|
|
405
|
-
c.setFont('Helvetica', 10)
|
|
406
|
-
c.setFillColor(colors.HexColor('#7F8C8D'))
|
|
407
|
-
c.drawRightString(w - 2*cm, h - 2.5*cm, f'#{inv_number}')
|
|
408
|
-
c.drawRightString(w - 2*cm, h - 3*cm, f'Date: {date}')
|
|
409
|
-
|
|
410
|
-
# Company & Customer
|
|
411
|
-
y = h - 4.5*cm
|
|
412
|
-
c.setFillColor(colors.HexColor('#2C3E50'))
|
|
413
|
-
c.setFont('Helvetica-Bold', 11)
|
|
414
|
-
c.drawString(2*cm, y, 'From:')
|
|
415
|
-
c.drawString(10*cm, y, 'To:')
|
|
416
|
-
c.setFont('Helvetica', 10)
|
|
417
|
-
c.setFillColor(colors.black)
|
|
418
|
-
c.drawString(2*cm, y - 0.5*cm, company)
|
|
419
|
-
c.drawString(10*cm, y - 0.5*cm, customer)
|
|
420
|
-
if address:
|
|
421
|
-
for i, line in enumerate(address.split('\\n')):
|
|
422
|
-
c.drawString(10*cm, y - (1 + i*0.5)*cm, line)
|
|
423
|
-
|
|
424
|
-
# Table header
|
|
425
|
-
y = h - 8*cm
|
|
426
|
-
c.setFillColor(colors.HexColor('#4472C4'))
|
|
427
|
-
c.rect(2*cm, y - 0.1*cm, w - 4*cm, 0.7*cm, fill=1, stroke=0)
|
|
428
|
-
c.setFillColor(colors.white)
|
|
429
|
-
c.setFont('Helvetica-Bold', 9)
|
|
430
|
-
c.drawString(2.2*cm, y + 0.1*cm, 'Description')
|
|
431
|
-
c.drawRightString(12*cm, y + 0.1*cm, 'Qty')
|
|
432
|
-
c.drawRightString(15*cm, y + 0.1*cm, 'Unit Price')
|
|
433
|
-
c.drawRightString(w - 2.2*cm, y + 0.1*cm, 'Amount')
|
|
434
|
-
|
|
435
|
-
# Items
|
|
436
|
-
c.setFillColor(colors.black)
|
|
437
|
-
c.setFont('Helvetica', 9)
|
|
438
|
-
total = 0
|
|
439
|
-
for i, item in enumerate(items):
|
|
440
|
-
row_y = y - (i + 1) * 0.6*cm
|
|
441
|
-
qty = item.get('quantity', 0)
|
|
442
|
-
price = item.get('unitPrice', 0)
|
|
443
|
-
amount = qty * price
|
|
444
|
-
total += amount
|
|
445
|
-
if i % 2 == 1:
|
|
446
|
-
c.setFillColor(colors.HexColor('#F0F4F8'))
|
|
447
|
-
c.rect(2*cm, row_y - 0.1*cm, w - 4*cm, 0.6*cm, fill=1, stroke=0)
|
|
448
|
-
c.setFillColor(colors.black)
|
|
449
|
-
c.drawString(2.2*cm, row_y + 0.1*cm, str(item.get('description', '')))
|
|
450
|
-
c.drawRightString(12*cm, row_y + 0.1*cm, str(qty))
|
|
451
|
-
c.drawRightString(15*cm, row_y + 0.1*cm, f'{currency}{price:,.2f}')
|
|
452
|
-
c.drawRightString(w - 2.2*cm, row_y + 0.1*cm, f'{currency}{amount:,.2f}')
|
|
453
|
-
|
|
454
|
-
# Total
|
|
455
|
-
total_y = y - (len(items) + 1.5) * 0.6*cm
|
|
456
|
-
c.setStrokeColor(colors.HexColor('#4472C4'))
|
|
457
|
-
c.line(12*cm, total_y + 0.4*cm, w - 2*cm, total_y + 0.4*cm)
|
|
458
|
-
c.setFont('Helvetica-Bold', 11)
|
|
459
|
-
c.drawRightString(15*cm, total_y, 'Total:')
|
|
460
|
-
c.setFillColor(colors.HexColor('#2C3E50'))
|
|
461
|
-
c.drawRightString(w - 2.2*cm, total_y, f'{currency}{total:,.2f}')
|
|
462
|
-
|
|
463
|
-
# Notes
|
|
464
|
-
if notes:
|
|
465
|
-
notes_y = total_y - 2*cm
|
|
466
|
-
c.setFillColor(colors.HexColor('#7F8C8D'))
|
|
467
|
-
c.setFont('Helvetica-Bold', 9)
|
|
468
|
-
c.drawString(2*cm, notes_y, 'Notes:')
|
|
469
|
-
c.setFont('Helvetica', 9)
|
|
470
|
-
c.setFillColor(colors.black)
|
|
471
|
-
for i, line in enumerate(notes.split('\\n')):
|
|
472
|
-
c.drawString(2*cm, notes_y - (i + 1) * 0.4*cm, line)
|
|
473
|
-
|
|
474
|
-
c.save()
|
|
475
|
-
print(f'Generated: invoice_{inv_number}.pdf')
|
|
476
|
-
`;
|
|
477
|
-
|
|
478
|
-
const SEED_DATA_SUMMARY = `import os, json
|
|
479
|
-
import pandas as pd
|
|
480
|
-
from docx import Document
|
|
481
|
-
from docx.shared import Inches, Pt
|
|
482
|
-
from docx.enum.table import WD_TABLE_ALIGNMENT
|
|
483
|
-
|
|
484
|
-
title = json.loads('''{{title}}''') if '{{title}}'.startswith('"') else '{{title}}'
|
|
485
|
-
raw_data = json.loads('''{{data}}''')
|
|
486
|
-
group_by_raw = '''{{groupByField}}'''
|
|
487
|
-
group_by = group_by_raw if group_by_raw != '{{' + 'groupByField}}' else None
|
|
488
|
-
sort_by_raw = '''{{sortByField}}'''
|
|
489
|
-
sort_by = sort_by_raw if sort_by_raw != '{{' + 'sortByField}}' else None
|
|
490
|
-
top_n_raw = '''{{topN}}'''
|
|
491
|
-
top_n = int(top_n_raw) if top_n_raw != '{{' + 'topN}}' else 10
|
|
492
|
-
|
|
493
|
-
df = pd.DataFrame(raw_data)
|
|
494
|
-
doc = Document()
|
|
495
|
-
doc.add_heading(title, 0)
|
|
496
|
-
|
|
497
|
-
# Overview
|
|
498
|
-
doc.add_heading('Overview', level=1)
|
|
499
|
-
doc.add_paragraph(f'Total records: {len(df)}')
|
|
500
|
-
doc.add_paragraph(f'Fields: {", ".join(df.columns.tolist())}')
|
|
501
|
-
|
|
502
|
-
# Numeric summary
|
|
503
|
-
num_cols = df.select_dtypes(include='number').columns.tolist()
|
|
504
|
-
if num_cols:
|
|
505
|
-
doc.add_heading('Numeric Summary', level=1)
|
|
506
|
-
stats = df[num_cols].describe().round(2)
|
|
507
|
-
table = doc.add_table(rows=len(stats) + 1, cols=len(num_cols) + 1)
|
|
508
|
-
table.style = 'Light Grid Accent 1'
|
|
509
|
-
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
|
510
|
-
table.rows[0].cells[0].text = 'Metric'
|
|
511
|
-
for j, col in enumerate(num_cols):
|
|
512
|
-
table.rows[0].cells[j + 1].text = col
|
|
513
|
-
for i, (idx, row) in enumerate(stats.iterrows()):
|
|
514
|
-
table.rows[i + 1].cells[0].text = str(idx)
|
|
515
|
-
for j, col in enumerate(num_cols):
|
|
516
|
-
table.rows[i + 1].cells[j + 1].text = str(row[col])
|
|
517
|
-
|
|
518
|
-
# Group-by analysis
|
|
519
|
-
if group_by and group_by in df.columns:
|
|
520
|
-
doc.add_heading(f'Grouped by: {group_by}', level=1)
|
|
521
|
-
grouped = df.groupby(group_by)
|
|
522
|
-
summary_rows = []
|
|
523
|
-
for name, group in grouped:
|
|
524
|
-
row_info = {'Group': str(name), 'Count': len(group)}
|
|
525
|
-
for nc in num_cols:
|
|
526
|
-
row_info[f'{nc} (sum)'] = round(group[nc].sum(), 2)
|
|
527
|
-
row_info[f'{nc} (avg)'] = round(group[nc].mean(), 2)
|
|
528
|
-
summary_rows.append(row_info)
|
|
529
|
-
if summary_rows:
|
|
530
|
-
headers = list(summary_rows[0].keys())
|
|
531
|
-
table = doc.add_table(rows=len(summary_rows) + 1, cols=len(headers))
|
|
532
|
-
table.style = 'Light Grid Accent 1'
|
|
533
|
-
for j, h in enumerate(headers):
|
|
534
|
-
table.rows[0].cells[j].text = h
|
|
535
|
-
for i, sr in enumerate(summary_rows):
|
|
536
|
-
for j, h in enumerate(headers):
|
|
537
|
-
table.rows[i + 1].cells[j].text = str(sr[h])
|
|
538
|
-
|
|
539
|
-
# Top records
|
|
540
|
-
if sort_by and sort_by in df.columns:
|
|
541
|
-
df_sorted = df.sort_values(sort_by, ascending=False)
|
|
542
|
-
else:
|
|
543
|
-
df_sorted = df
|
|
544
|
-
top = df_sorted.head(top_n)
|
|
545
|
-
doc.add_heading(f'Top {top_n} Records', level=1)
|
|
546
|
-
cols = top.columns.tolist()
|
|
547
|
-
table = doc.add_table(rows=len(top) + 1, cols=len(cols))
|
|
548
|
-
table.style = 'Light Grid Accent 1'
|
|
549
|
-
for j, col in enumerate(cols):
|
|
550
|
-
table.rows[0].cells[j].text = str(col)
|
|
551
|
-
for i, (_, row) in enumerate(top.iterrows()):
|
|
552
|
-
for j, col in enumerate(cols):
|
|
553
|
-
table.rows[i + 1].cells[j].text = str(row[col])
|
|
554
|
-
|
|
555
|
-
output_dir = os.environ.get('OUTPUT_DIR', '/output')
|
|
556
|
-
filepath = os.path.join(output_dir, 'summary_report.docx')
|
|
557
|
-
doc.save(filepath)
|
|
558
|
-
print('Generated: summary_report.docx')
|
|
559
|
-
`;
|
|
560
|
-
|
|
561
|
-
const SEED_DATA_TRANSFORM = `const fs = require('fs');
|
|
562
|
-
const path = require('path');
|
|
563
|
-
|
|
564
|
-
const data = {{data}};
|
|
565
|
-
const format = '{{format}}';
|
|
566
|
-
const filename = '{{filename}}' !== '{{' + 'filename}}' ? '{{filename}}' : 'result';
|
|
567
|
-
const outputDir = process.env.OUTPUT_DIR || '/output';
|
|
568
|
-
|
|
569
|
-
if (format === 'csv') {
|
|
570
|
-
const headers = Object.keys(data[0] || {});
|
|
571
|
-
const csv = [
|
|
572
|
-
headers.join(','),
|
|
573
|
-
...data.map(row => headers.map(h => JSON.stringify(row[h] ?? '')).join(','))
|
|
574
|
-
].join('\\n');
|
|
575
|
-
const outPath = path.join(outputDir, filename + '.csv');
|
|
576
|
-
fs.writeFileSync(outPath, csv, 'utf-8');
|
|
577
|
-
console.log('Generated: ' + filename + '.csv');
|
|
578
|
-
} else {
|
|
579
|
-
const outPath = path.join(outputDir, filename + '.json');
|
|
580
|
-
fs.writeFileSync(outPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
581
|
-
console.log('Generated: ' + filename + '.json');
|
|
582
|
-
}
|
|
583
|
-
`;
|
|
1
|
+
import { Database } from '@nocobase/database';
|
|
2
|
+
import { stringifyJsonText } from '../skill-hub/utils/json-fields';
|
|
3
|
+
|
|
4
|
+
export class SkillManager {
|
|
5
|
+
constructor(private db: Database) {}
|
|
6
|
+
|
|
7
|
+
async seedDefaults() {
|
|
8
|
+
const repo = this.db.getRepository('skillDefinitions');
|
|
9
|
+
|
|
10
|
+
const seeds = [
|
|
11
|
+
{
|
|
12
|
+
name: 'generate-word-report',
|
|
13
|
+
title: 'Generate Word Report',
|
|
14
|
+
description: 'Generate a Word document (.docx) with title, content, and optional table data.',
|
|
15
|
+
language: 'python',
|
|
16
|
+
codeTemplate: SEED_WORD_REPORT,
|
|
17
|
+
inputSchema: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
title: { type: 'string', description: 'Report title' },
|
|
21
|
+
content: { type: 'string', description: 'Report body text' },
|
|
22
|
+
tableData: {
|
|
23
|
+
type: 'array',
|
|
24
|
+
items: { type: 'object' },
|
|
25
|
+
description: 'Optional array of objects for table rows',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
required: ['title', 'content'],
|
|
29
|
+
},
|
|
30
|
+
packages: ['python-docx'],
|
|
31
|
+
timeoutSeconds: 30,
|
|
32
|
+
enabled: true,
|
|
33
|
+
toolScope: 'CUSTOM',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'generate-excel',
|
|
37
|
+
title: 'Generate Excel Spreadsheet',
|
|
38
|
+
description: 'Generate an Excel file (.xlsx) with headers and row data.',
|
|
39
|
+
language: 'python',
|
|
40
|
+
codeTemplate: SEED_EXCEL,
|
|
41
|
+
inputSchema: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: {
|
|
44
|
+
sheetName: { type: 'string', description: 'Sheet name', default: 'Sheet1' },
|
|
45
|
+
headers: { type: 'array', items: { type: 'string' }, description: 'Column headers' },
|
|
46
|
+
rows: { type: 'array', items: { type: 'array' }, description: 'Row data (array of arrays)' },
|
|
47
|
+
},
|
|
48
|
+
required: ['headers', 'rows'],
|
|
49
|
+
},
|
|
50
|
+
packages: ['openpyxl'],
|
|
51
|
+
timeoutSeconds: 30,
|
|
52
|
+
enabled: true,
|
|
53
|
+
toolScope: 'CUSTOM',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'generate-pdf-report',
|
|
57
|
+
title: 'Generate PDF Report',
|
|
58
|
+
description:
|
|
59
|
+
'Generate a professional PDF report with title, sections, and optional table. Suitable for CRM reports, customer summaries, and sales reports.',
|
|
60
|
+
language: 'python',
|
|
61
|
+
codeTemplate: SEED_PDF_REPORT,
|
|
62
|
+
inputSchema: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
title: { type: 'string', description: 'Report title' },
|
|
66
|
+
sections: {
|
|
67
|
+
type: 'array',
|
|
68
|
+
items: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {
|
|
71
|
+
heading: { type: 'string' },
|
|
72
|
+
body: { type: 'string' },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
description: 'Array of {heading, body} sections',
|
|
76
|
+
},
|
|
77
|
+
tableData: {
|
|
78
|
+
type: 'array',
|
|
79
|
+
items: { type: 'object' },
|
|
80
|
+
description: 'Optional array of objects for a summary table',
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
required: ['title', 'sections'],
|
|
84
|
+
},
|
|
85
|
+
packages: ['reportlab'],
|
|
86
|
+
timeoutSeconds: 30,
|
|
87
|
+
enabled: true,
|
|
88
|
+
toolScope: 'CUSTOM',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'generate-chart-image',
|
|
92
|
+
title: 'Generate Chart Image',
|
|
93
|
+
description:
|
|
94
|
+
'Generate a chart image (PNG) from data. Supports bar, line, pie charts. Useful for visualizing CRM metrics like sales, leads, revenue.',
|
|
95
|
+
language: 'python',
|
|
96
|
+
codeTemplate: SEED_CHART,
|
|
97
|
+
inputSchema: {
|
|
98
|
+
type: 'object',
|
|
99
|
+
properties: {
|
|
100
|
+
chartType: {
|
|
101
|
+
type: 'string',
|
|
102
|
+
enum: ['bar', 'line', 'pie'],
|
|
103
|
+
description: 'Chart type',
|
|
104
|
+
},
|
|
105
|
+
title: { type: 'string', description: 'Chart title' },
|
|
106
|
+
labels: { type: 'array', items: { type: 'string' }, description: 'X-axis labels or pie labels' },
|
|
107
|
+
values: { type: 'array', items: { type: 'number' }, description: 'Data values' },
|
|
108
|
+
xlabel: { type: 'string', description: 'X-axis label (bar/line only)' },
|
|
109
|
+
ylabel: { type: 'string', description: 'Y-axis label (bar/line only)' },
|
|
110
|
+
},
|
|
111
|
+
required: ['chartType', 'title', 'labels', 'values'],
|
|
112
|
+
},
|
|
113
|
+
packages: ['matplotlib'],
|
|
114
|
+
timeoutSeconds: 30,
|
|
115
|
+
enabled: true,
|
|
116
|
+
toolScope: 'CUSTOM',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'generate-invoice-pdf',
|
|
120
|
+
title: 'Generate Invoice PDF',
|
|
121
|
+
description:
|
|
122
|
+
'Generate a professional invoice PDF with company info, line items, and totals. For CRM billing and quotation workflows.',
|
|
123
|
+
language: 'python',
|
|
124
|
+
codeTemplate: SEED_INVOICE,
|
|
125
|
+
inputSchema: {
|
|
126
|
+
type: 'object',
|
|
127
|
+
properties: {
|
|
128
|
+
invoiceNumber: { type: 'string', description: 'Invoice number' },
|
|
129
|
+
date: { type: 'string', description: 'Invoice date (YYYY-MM-DD)' },
|
|
130
|
+
companyName: { type: 'string', description: 'Your company name' },
|
|
131
|
+
customerName: { type: 'string', description: 'Customer name' },
|
|
132
|
+
customerAddress: { type: 'string', description: 'Customer address' },
|
|
133
|
+
items: {
|
|
134
|
+
type: 'array',
|
|
135
|
+
items: {
|
|
136
|
+
type: 'object',
|
|
137
|
+
properties: {
|
|
138
|
+
description: { type: 'string' },
|
|
139
|
+
quantity: { type: 'number' },
|
|
140
|
+
unitPrice: { type: 'number' },
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
description: 'Line items with description, quantity, unitPrice',
|
|
144
|
+
},
|
|
145
|
+
currency: { type: 'string', description: 'Currency symbol', default: '$' },
|
|
146
|
+
notes: { type: 'string', description: 'Optional notes or payment terms' },
|
|
147
|
+
},
|
|
148
|
+
required: ['invoiceNumber', 'date', 'companyName', 'customerName', 'items'],
|
|
149
|
+
},
|
|
150
|
+
packages: ['reportlab'],
|
|
151
|
+
timeoutSeconds: 30,
|
|
152
|
+
enabled: true,
|
|
153
|
+
toolScope: 'CUSTOM',
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'data-summary-report',
|
|
157
|
+
title: 'Data Summary & Analysis Report',
|
|
158
|
+
description:
|
|
159
|
+
'Analyze tabular data and generate a Word report with summary statistics, top records, and insights. Ideal for CRM data analysis — leads, deals, customers.',
|
|
160
|
+
language: 'python',
|
|
161
|
+
codeTemplate: SEED_DATA_SUMMARY,
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
properties: {
|
|
165
|
+
title: { type: 'string', description: 'Report title' },
|
|
166
|
+
data: {
|
|
167
|
+
type: 'array',
|
|
168
|
+
items: { type: 'object' },
|
|
169
|
+
description: 'Array of data objects (e.g. deals, leads, customers)',
|
|
170
|
+
},
|
|
171
|
+
groupByField: { type: 'string', description: 'Optional field to group and summarize by' },
|
|
172
|
+
sortByField: { type: 'string', description: 'Optional field to sort by (descending)' },
|
|
173
|
+
topN: { type: 'number', description: 'Number of top records to include', default: 10 },
|
|
174
|
+
},
|
|
175
|
+
required: ['title', 'data'],
|
|
176
|
+
},
|
|
177
|
+
packages: ['pandas', 'python-docx'],
|
|
178
|
+
timeoutSeconds: 60,
|
|
179
|
+
enabled: true,
|
|
180
|
+
toolScope: 'CUSTOM',
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: 'data-transform',
|
|
184
|
+
title: 'Data Transform (CSV/JSON)',
|
|
185
|
+
description: 'Transform data array to CSV or JSON file for download.',
|
|
186
|
+
language: 'node',
|
|
187
|
+
codeTemplate: SEED_DATA_TRANSFORM,
|
|
188
|
+
inputSchema: {
|
|
189
|
+
type: 'object',
|
|
190
|
+
properties: {
|
|
191
|
+
data: { type: 'array', items: { type: 'object' }, description: 'Array of data objects' },
|
|
192
|
+
format: { type: 'string', enum: ['csv', 'json'], description: 'Output format' },
|
|
193
|
+
filename: { type: 'string', description: 'Output filename (without extension)' },
|
|
194
|
+
},
|
|
195
|
+
required: ['data', 'format'],
|
|
196
|
+
},
|
|
197
|
+
packages: [],
|
|
198
|
+
timeoutSeconds: 30,
|
|
199
|
+
enabled: true,
|
|
200
|
+
toolScope: 'CUSTOM',
|
|
201
|
+
},
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
for (const seed of seeds) {
|
|
205
|
+
try {
|
|
206
|
+
const count = await repo.count({ filter: { name: seed.name } });
|
|
207
|
+
if (count === 0) {
|
|
208
|
+
await repo.create({
|
|
209
|
+
values: {
|
|
210
|
+
...seed,
|
|
211
|
+
inputSchema: stringifyJsonText(seed.inputSchema),
|
|
212
|
+
packages: stringifyJsonText(seed.packages, []),
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
} catch (err) {
|
|
217
|
+
console.error(`[import-skill] Failed to insert ${seed.name}:`, err);
|
|
218
|
+
// Skip if already exists (unique constraint on name)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ─── Seed Code Templates ───
|
|
225
|
+
|
|
226
|
+
const SEED_WORD_REPORT = `import os, json
|
|
227
|
+
from docx import Document
|
|
228
|
+
|
|
229
|
+
title = json.loads('''{{title}}''') if '{{title}}'.startswith('"') else '{{title}}'
|
|
230
|
+
content = json.loads('''{{content}}''') if '{{content}}'.startswith('"') else '{{content}}'
|
|
231
|
+
|
|
232
|
+
doc = Document()
|
|
233
|
+
doc.add_heading(title, 0)
|
|
234
|
+
doc.add_paragraph(content)
|
|
235
|
+
|
|
236
|
+
table_data_raw = '''{{tableData}}'''
|
|
237
|
+
if table_data_raw and table_data_raw != '{{' + 'tableData}}':
|
|
238
|
+
table_data = json.loads(table_data_raw)
|
|
239
|
+
if table_data and len(table_data) > 0:
|
|
240
|
+
headers = list(table_data[0].keys())
|
|
241
|
+
table = doc.add_table(rows=1, cols=len(headers))
|
|
242
|
+
table.style = 'Light Grid Accent 1'
|
|
243
|
+
for i, header in enumerate(headers):
|
|
244
|
+
table.rows[0].cells[i].text = str(header)
|
|
245
|
+
for row_data in table_data:
|
|
246
|
+
row = table.add_row()
|
|
247
|
+
for i, header in enumerate(headers):
|
|
248
|
+
row.cells[i].text = str(row_data.get(header, ''))
|
|
249
|
+
|
|
250
|
+
output_dir = os.environ.get('OUTPUT_DIR', '/output')
|
|
251
|
+
filepath = os.path.join(output_dir, 'report.docx')
|
|
252
|
+
doc.save(filepath)
|
|
253
|
+
print(f'Generated: report.docx')
|
|
254
|
+
`;
|
|
255
|
+
|
|
256
|
+
const SEED_EXCEL = `import os, json
|
|
257
|
+
import openpyxl
|
|
258
|
+
|
|
259
|
+
sheet_name_raw = '{{sheetName}}'
|
|
260
|
+
sheet_name = sheet_name_raw if sheet_name_raw != '{{' + 'sheetName}}' else 'Sheet1'
|
|
261
|
+
headers = json.loads('''{{headers}}''')
|
|
262
|
+
rows = json.loads('''{{rows}}''')
|
|
263
|
+
|
|
264
|
+
wb = openpyxl.Workbook()
|
|
265
|
+
ws = wb.active
|
|
266
|
+
ws.title = sheet_name
|
|
267
|
+
|
|
268
|
+
for col, header in enumerate(headers, 1):
|
|
269
|
+
ws.cell(row=1, column=col, value=header)
|
|
270
|
+
|
|
271
|
+
for row_idx, row_data in enumerate(rows, 2):
|
|
272
|
+
for col_idx, value in enumerate(row_data, 1):
|
|
273
|
+
ws.cell(row=row_idx, column=col_idx, value=value)
|
|
274
|
+
|
|
275
|
+
output_dir = os.environ.get('OUTPUT_DIR', '/output')
|
|
276
|
+
filepath = os.path.join(output_dir, 'data.xlsx')
|
|
277
|
+
wb.save(filepath)
|
|
278
|
+
print(f'Generated: data.xlsx')
|
|
279
|
+
`;
|
|
280
|
+
|
|
281
|
+
const SEED_PDF_REPORT = `import os, json
|
|
282
|
+
from reportlab.lib.pagesizes import A4
|
|
283
|
+
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
|
284
|
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
|
|
285
|
+
from reportlab.lib import colors
|
|
286
|
+
from reportlab.lib.units import cm
|
|
287
|
+
|
|
288
|
+
title = json.loads('''{{title}}''') if '{{title}}'.startswith('"') else '{{title}}'
|
|
289
|
+
sections = json.loads('''{{sections}}''')
|
|
290
|
+
|
|
291
|
+
output_dir = os.environ.get('OUTPUT_DIR', '/output')
|
|
292
|
+
filepath = os.path.join(output_dir, 'report.pdf')
|
|
293
|
+
|
|
294
|
+
doc = SimpleDocTemplate(filepath, pagesize=A4,
|
|
295
|
+
topMargin=2*cm, bottomMargin=2*cm, leftMargin=2*cm, rightMargin=2*cm)
|
|
296
|
+
styles = getSampleStyleSheet()
|
|
297
|
+
title_style = ParagraphStyle('CustomTitle', parent=styles['Title'], fontSize=22, spaceAfter=20)
|
|
298
|
+
heading_style = ParagraphStyle('CustomHeading', parent=styles['Heading2'], fontSize=14, spaceBefore=16, spaceAfter=8)
|
|
299
|
+
body_style = styles['BodyText']
|
|
300
|
+
|
|
301
|
+
story = [Paragraph(title, title_style), Spacer(1, 12)]
|
|
302
|
+
|
|
303
|
+
for section in sections:
|
|
304
|
+
if section.get('heading'):
|
|
305
|
+
story.append(Paragraph(section['heading'], heading_style))
|
|
306
|
+
if section.get('body'):
|
|
307
|
+
for line in section['body'].split('\\n'):
|
|
308
|
+
story.append(Paragraph(line, body_style))
|
|
309
|
+
story.append(Spacer(1, 8))
|
|
310
|
+
|
|
311
|
+
table_data_raw = '''{{tableData}}'''
|
|
312
|
+
if table_data_raw and table_data_raw != '{{' + 'tableData}}':
|
|
313
|
+
td = json.loads(table_data_raw)
|
|
314
|
+
if td and len(td) > 0:
|
|
315
|
+
headers = list(td[0].keys())
|
|
316
|
+
data = [headers] + [[str(r.get(h, '')) for h in headers] for r in td]
|
|
317
|
+
t = Table(data, repeatRows=1)
|
|
318
|
+
t.setStyle(TableStyle([
|
|
319
|
+
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#4472C4')),
|
|
320
|
+
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
|
321
|
+
('FONTSIZE', (0, 0), (-1, 0), 10),
|
|
322
|
+
('FONTSIZE', (0, 1), (-1, -1), 9),
|
|
323
|
+
('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
|
|
324
|
+
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#D9E2F3')]),
|
|
325
|
+
]))
|
|
326
|
+
story.append(Spacer(1, 12))
|
|
327
|
+
story.append(t)
|
|
328
|
+
|
|
329
|
+
doc.build(story)
|
|
330
|
+
print('Generated: report.pdf')
|
|
331
|
+
`;
|
|
332
|
+
|
|
333
|
+
const SEED_CHART = `import os, json
|
|
334
|
+
import matplotlib
|
|
335
|
+
matplotlib.use('Agg')
|
|
336
|
+
import matplotlib.pyplot as plt
|
|
337
|
+
|
|
338
|
+
chart_type = '{{chartType}}'
|
|
339
|
+
title = json.loads('''{{title}}''') if '{{title}}'.startswith('"') else '{{title}}'
|
|
340
|
+
labels = json.loads('''{{labels}}''')
|
|
341
|
+
values = json.loads('''{{values}}''')
|
|
342
|
+
xlabel_raw = '''{{xlabel}}'''
|
|
343
|
+
ylabel_raw = '''{{ylabel}}'''
|
|
344
|
+
xlabel = xlabel_raw if xlabel_raw != '{{' + 'xlabel}}' else ''
|
|
345
|
+
ylabel = ylabel_raw if ylabel_raw != '{{' + 'ylabel}}' else ''
|
|
346
|
+
|
|
347
|
+
fig, ax = plt.subplots(figsize=(10, 6))
|
|
348
|
+
colors = ['#4472C4', '#ED7D31', '#A5A5A5', '#FFC000', '#5B9BD5', '#70AD47', '#264478', '#9B59B6']
|
|
349
|
+
|
|
350
|
+
if chart_type == 'bar':
|
|
351
|
+
bars = ax.bar(labels, values, color=colors[:len(labels)])
|
|
352
|
+
for bar, val in zip(bars, values):
|
|
353
|
+
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + max(values)*0.01,
|
|
354
|
+
f'{val:,.0f}' if isinstance(val, (int, float)) else str(val),
|
|
355
|
+
ha='center', va='bottom', fontsize=9)
|
|
356
|
+
if xlabel: ax.set_xlabel(xlabel)
|
|
357
|
+
if ylabel: ax.set_ylabel(ylabel)
|
|
358
|
+
elif chart_type == 'line':
|
|
359
|
+
ax.plot(labels, values, marker='o', linewidth=2, color='#4472C4', markersize=6)
|
|
360
|
+
for i, val in enumerate(values):
|
|
361
|
+
ax.annotate(f'{val:,.0f}', (labels[i], val), textcoords="offset points",
|
|
362
|
+
xytext=(0, 10), ha='center', fontsize=9)
|
|
363
|
+
if xlabel: ax.set_xlabel(xlabel)
|
|
364
|
+
if ylabel: ax.set_ylabel(ylabel)
|
|
365
|
+
elif chart_type == 'pie':
|
|
366
|
+
ax.pie(values, labels=labels, colors=colors[:len(labels)], autopct='%1.1f%%', startangle=90)
|
|
367
|
+
|
|
368
|
+
ax.set_title(title, fontsize=14, fontweight='bold', pad=15)
|
|
369
|
+
plt.tight_layout()
|
|
370
|
+
|
|
371
|
+
output_dir = os.environ.get('OUTPUT_DIR', '/output')
|
|
372
|
+
filepath = os.path.join(output_dir, 'chart.png')
|
|
373
|
+
fig.savefig(filepath, dpi=150, bbox_inches='tight')
|
|
374
|
+
plt.close()
|
|
375
|
+
print('Generated: chart.png')
|
|
376
|
+
`;
|
|
377
|
+
|
|
378
|
+
const SEED_INVOICE = `import os, json
|
|
379
|
+
from reportlab.lib.pagesizes import A4
|
|
380
|
+
from reportlab.pdfgen import canvas
|
|
381
|
+
from reportlab.lib.units import cm, mm
|
|
382
|
+
from reportlab.lib import colors
|
|
383
|
+
|
|
384
|
+
inv_number = json.loads('''{{invoiceNumber}}''') if '{{invoiceNumber}}'.startswith('"') else '{{invoiceNumber}}'
|
|
385
|
+
date = json.loads('''{{date}}''') if '{{date}}'.startswith('"') else '{{date}}'
|
|
386
|
+
company = json.loads('''{{companyName}}''') if '{{companyName}}'.startswith('"') else '{{companyName}}'
|
|
387
|
+
customer = json.loads('''{{customerName}}''') if '{{customerName}}'.startswith('"') else '{{customerName}}'
|
|
388
|
+
address_raw = '''{{customerAddress}}'''
|
|
389
|
+
address = json.loads(address_raw) if address_raw.startswith('"') else address_raw if address_raw != '{{' + 'customerAddress}}' else ''
|
|
390
|
+
items = json.loads('''{{items}}''')
|
|
391
|
+
currency_raw = '''{{currency}}'''
|
|
392
|
+
currency = currency_raw if currency_raw != '{{' + 'currency}}' else '$'
|
|
393
|
+
notes_raw = '''{{notes}}'''
|
|
394
|
+
notes = json.loads(notes_raw) if notes_raw.startswith('"') else notes_raw if notes_raw != '{{' + 'notes}}' else ''
|
|
395
|
+
|
|
396
|
+
output_dir = os.environ.get('OUTPUT_DIR', '/output')
|
|
397
|
+
filepath = os.path.join(output_dir, f'invoice_{inv_number}.pdf')
|
|
398
|
+
w, h = A4
|
|
399
|
+
c = canvas.Canvas(filepath, pagesize=A4)
|
|
400
|
+
|
|
401
|
+
# Header
|
|
402
|
+
c.setFont('Helvetica-Bold', 24)
|
|
403
|
+
c.setFillColor(colors.HexColor('#2C3E50'))
|
|
404
|
+
c.drawString(2*cm, h - 2.5*cm, 'INVOICE')
|
|
405
|
+
c.setFont('Helvetica', 10)
|
|
406
|
+
c.setFillColor(colors.HexColor('#7F8C8D'))
|
|
407
|
+
c.drawRightString(w - 2*cm, h - 2.5*cm, f'#{inv_number}')
|
|
408
|
+
c.drawRightString(w - 2*cm, h - 3*cm, f'Date: {date}')
|
|
409
|
+
|
|
410
|
+
# Company & Customer
|
|
411
|
+
y = h - 4.5*cm
|
|
412
|
+
c.setFillColor(colors.HexColor('#2C3E50'))
|
|
413
|
+
c.setFont('Helvetica-Bold', 11)
|
|
414
|
+
c.drawString(2*cm, y, 'From:')
|
|
415
|
+
c.drawString(10*cm, y, 'To:')
|
|
416
|
+
c.setFont('Helvetica', 10)
|
|
417
|
+
c.setFillColor(colors.black)
|
|
418
|
+
c.drawString(2*cm, y - 0.5*cm, company)
|
|
419
|
+
c.drawString(10*cm, y - 0.5*cm, customer)
|
|
420
|
+
if address:
|
|
421
|
+
for i, line in enumerate(address.split('\\n')):
|
|
422
|
+
c.drawString(10*cm, y - (1 + i*0.5)*cm, line)
|
|
423
|
+
|
|
424
|
+
# Table header
|
|
425
|
+
y = h - 8*cm
|
|
426
|
+
c.setFillColor(colors.HexColor('#4472C4'))
|
|
427
|
+
c.rect(2*cm, y - 0.1*cm, w - 4*cm, 0.7*cm, fill=1, stroke=0)
|
|
428
|
+
c.setFillColor(colors.white)
|
|
429
|
+
c.setFont('Helvetica-Bold', 9)
|
|
430
|
+
c.drawString(2.2*cm, y + 0.1*cm, 'Description')
|
|
431
|
+
c.drawRightString(12*cm, y + 0.1*cm, 'Qty')
|
|
432
|
+
c.drawRightString(15*cm, y + 0.1*cm, 'Unit Price')
|
|
433
|
+
c.drawRightString(w - 2.2*cm, y + 0.1*cm, 'Amount')
|
|
434
|
+
|
|
435
|
+
# Items
|
|
436
|
+
c.setFillColor(colors.black)
|
|
437
|
+
c.setFont('Helvetica', 9)
|
|
438
|
+
total = 0
|
|
439
|
+
for i, item in enumerate(items):
|
|
440
|
+
row_y = y - (i + 1) * 0.6*cm
|
|
441
|
+
qty = item.get('quantity', 0)
|
|
442
|
+
price = item.get('unitPrice', 0)
|
|
443
|
+
amount = qty * price
|
|
444
|
+
total += amount
|
|
445
|
+
if i % 2 == 1:
|
|
446
|
+
c.setFillColor(colors.HexColor('#F0F4F8'))
|
|
447
|
+
c.rect(2*cm, row_y - 0.1*cm, w - 4*cm, 0.6*cm, fill=1, stroke=0)
|
|
448
|
+
c.setFillColor(colors.black)
|
|
449
|
+
c.drawString(2.2*cm, row_y + 0.1*cm, str(item.get('description', '')))
|
|
450
|
+
c.drawRightString(12*cm, row_y + 0.1*cm, str(qty))
|
|
451
|
+
c.drawRightString(15*cm, row_y + 0.1*cm, f'{currency}{price:,.2f}')
|
|
452
|
+
c.drawRightString(w - 2.2*cm, row_y + 0.1*cm, f'{currency}{amount:,.2f}')
|
|
453
|
+
|
|
454
|
+
# Total
|
|
455
|
+
total_y = y - (len(items) + 1.5) * 0.6*cm
|
|
456
|
+
c.setStrokeColor(colors.HexColor('#4472C4'))
|
|
457
|
+
c.line(12*cm, total_y + 0.4*cm, w - 2*cm, total_y + 0.4*cm)
|
|
458
|
+
c.setFont('Helvetica-Bold', 11)
|
|
459
|
+
c.drawRightString(15*cm, total_y, 'Total:')
|
|
460
|
+
c.setFillColor(colors.HexColor('#2C3E50'))
|
|
461
|
+
c.drawRightString(w - 2.2*cm, total_y, f'{currency}{total:,.2f}')
|
|
462
|
+
|
|
463
|
+
# Notes
|
|
464
|
+
if notes:
|
|
465
|
+
notes_y = total_y - 2*cm
|
|
466
|
+
c.setFillColor(colors.HexColor('#7F8C8D'))
|
|
467
|
+
c.setFont('Helvetica-Bold', 9)
|
|
468
|
+
c.drawString(2*cm, notes_y, 'Notes:')
|
|
469
|
+
c.setFont('Helvetica', 9)
|
|
470
|
+
c.setFillColor(colors.black)
|
|
471
|
+
for i, line in enumerate(notes.split('\\n')):
|
|
472
|
+
c.drawString(2*cm, notes_y - (i + 1) * 0.4*cm, line)
|
|
473
|
+
|
|
474
|
+
c.save()
|
|
475
|
+
print(f'Generated: invoice_{inv_number}.pdf')
|
|
476
|
+
`;
|
|
477
|
+
|
|
478
|
+
const SEED_DATA_SUMMARY = `import os, json
|
|
479
|
+
import pandas as pd
|
|
480
|
+
from docx import Document
|
|
481
|
+
from docx.shared import Inches, Pt
|
|
482
|
+
from docx.enum.table import WD_TABLE_ALIGNMENT
|
|
483
|
+
|
|
484
|
+
title = json.loads('''{{title}}''') if '{{title}}'.startswith('"') else '{{title}}'
|
|
485
|
+
raw_data = json.loads('''{{data}}''')
|
|
486
|
+
group_by_raw = '''{{groupByField}}'''
|
|
487
|
+
group_by = group_by_raw if group_by_raw != '{{' + 'groupByField}}' else None
|
|
488
|
+
sort_by_raw = '''{{sortByField}}'''
|
|
489
|
+
sort_by = sort_by_raw if sort_by_raw != '{{' + 'sortByField}}' else None
|
|
490
|
+
top_n_raw = '''{{topN}}'''
|
|
491
|
+
top_n = int(top_n_raw) if top_n_raw != '{{' + 'topN}}' else 10
|
|
492
|
+
|
|
493
|
+
df = pd.DataFrame(raw_data)
|
|
494
|
+
doc = Document()
|
|
495
|
+
doc.add_heading(title, 0)
|
|
496
|
+
|
|
497
|
+
# Overview
|
|
498
|
+
doc.add_heading('Overview', level=1)
|
|
499
|
+
doc.add_paragraph(f'Total records: {len(df)}')
|
|
500
|
+
doc.add_paragraph(f'Fields: {", ".join(df.columns.tolist())}')
|
|
501
|
+
|
|
502
|
+
# Numeric summary
|
|
503
|
+
num_cols = df.select_dtypes(include='number').columns.tolist()
|
|
504
|
+
if num_cols:
|
|
505
|
+
doc.add_heading('Numeric Summary', level=1)
|
|
506
|
+
stats = df[num_cols].describe().round(2)
|
|
507
|
+
table = doc.add_table(rows=len(stats) + 1, cols=len(num_cols) + 1)
|
|
508
|
+
table.style = 'Light Grid Accent 1'
|
|
509
|
+
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
|
510
|
+
table.rows[0].cells[0].text = 'Metric'
|
|
511
|
+
for j, col in enumerate(num_cols):
|
|
512
|
+
table.rows[0].cells[j + 1].text = col
|
|
513
|
+
for i, (idx, row) in enumerate(stats.iterrows()):
|
|
514
|
+
table.rows[i + 1].cells[0].text = str(idx)
|
|
515
|
+
for j, col in enumerate(num_cols):
|
|
516
|
+
table.rows[i + 1].cells[j + 1].text = str(row[col])
|
|
517
|
+
|
|
518
|
+
# Group-by analysis
|
|
519
|
+
if group_by and group_by in df.columns:
|
|
520
|
+
doc.add_heading(f'Grouped by: {group_by}', level=1)
|
|
521
|
+
grouped = df.groupby(group_by)
|
|
522
|
+
summary_rows = []
|
|
523
|
+
for name, group in grouped:
|
|
524
|
+
row_info = {'Group': str(name), 'Count': len(group)}
|
|
525
|
+
for nc in num_cols:
|
|
526
|
+
row_info[f'{nc} (sum)'] = round(group[nc].sum(), 2)
|
|
527
|
+
row_info[f'{nc} (avg)'] = round(group[nc].mean(), 2)
|
|
528
|
+
summary_rows.append(row_info)
|
|
529
|
+
if summary_rows:
|
|
530
|
+
headers = list(summary_rows[0].keys())
|
|
531
|
+
table = doc.add_table(rows=len(summary_rows) + 1, cols=len(headers))
|
|
532
|
+
table.style = 'Light Grid Accent 1'
|
|
533
|
+
for j, h in enumerate(headers):
|
|
534
|
+
table.rows[0].cells[j].text = h
|
|
535
|
+
for i, sr in enumerate(summary_rows):
|
|
536
|
+
for j, h in enumerate(headers):
|
|
537
|
+
table.rows[i + 1].cells[j].text = str(sr[h])
|
|
538
|
+
|
|
539
|
+
# Top records
|
|
540
|
+
if sort_by and sort_by in df.columns:
|
|
541
|
+
df_sorted = df.sort_values(sort_by, ascending=False)
|
|
542
|
+
else:
|
|
543
|
+
df_sorted = df
|
|
544
|
+
top = df_sorted.head(top_n)
|
|
545
|
+
doc.add_heading(f'Top {top_n} Records', level=1)
|
|
546
|
+
cols = top.columns.tolist()
|
|
547
|
+
table = doc.add_table(rows=len(top) + 1, cols=len(cols))
|
|
548
|
+
table.style = 'Light Grid Accent 1'
|
|
549
|
+
for j, col in enumerate(cols):
|
|
550
|
+
table.rows[0].cells[j].text = str(col)
|
|
551
|
+
for i, (_, row) in enumerate(top.iterrows()):
|
|
552
|
+
for j, col in enumerate(cols):
|
|
553
|
+
table.rows[i + 1].cells[j].text = str(row[col])
|
|
554
|
+
|
|
555
|
+
output_dir = os.environ.get('OUTPUT_DIR', '/output')
|
|
556
|
+
filepath = os.path.join(output_dir, 'summary_report.docx')
|
|
557
|
+
doc.save(filepath)
|
|
558
|
+
print('Generated: summary_report.docx')
|
|
559
|
+
`;
|
|
560
|
+
|
|
561
|
+
const SEED_DATA_TRANSFORM = `const fs = require('fs');
|
|
562
|
+
const path = require('path');
|
|
563
|
+
|
|
564
|
+
const data = {{data}};
|
|
565
|
+
const format = '{{format}}';
|
|
566
|
+
const filename = '{{filename}}' !== '{{' + 'filename}}' ? '{{filename}}' : 'result';
|
|
567
|
+
const outputDir = process.env.OUTPUT_DIR || '/output';
|
|
568
|
+
|
|
569
|
+
if (format === 'csv') {
|
|
570
|
+
const headers = Object.keys(data[0] || {});
|
|
571
|
+
const csv = [
|
|
572
|
+
headers.join(','),
|
|
573
|
+
...data.map(row => headers.map(h => JSON.stringify(row[h] ?? '')).join(','))
|
|
574
|
+
].join('\\n');
|
|
575
|
+
const outPath = path.join(outputDir, filename + '.csv');
|
|
576
|
+
fs.writeFileSync(outPath, csv, 'utf-8');
|
|
577
|
+
console.log('Generated: ' + filename + '.csv');
|
|
578
|
+
} else {
|
|
579
|
+
const outPath = path.join(outputDir, filename + '.json');
|
|
580
|
+
fs.writeFileSync(outPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
581
|
+
console.log('Generated: ' + filename + '.json');
|
|
582
|
+
}
|
|
583
|
+
`;
|