@indemn/cli 1.7.0 → 1.7.2
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CONTRIBUTING.md +2 -2
- package/dist/mcp/registry.d.ts.map +1 -1
- package/dist/mcp/registry.js +6 -4
- package/dist/mcp/registry.js.map +1 -1
- package/dist/mcp/tool-meta.d.ts.map +1 -1
- package/dist/mcp/tool-meta.js +35 -9
- package/dist/mcp/tool-meta.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +232 -49
- package/dist/mcp/tools.js.map +1 -1
- package/dist/report/styles.d.ts +1 -0
- package/dist/report/styles.d.ts.map +1 -1
- package/dist/report/styles.js +5 -1
- package/dist/report/styles.js.map +1 -1
- package/dist/sdk/auth.js +2 -2
- package/dist/sdk/auth.js.map +1 -1
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/client.js +11 -1
- package/dist/sdk/client.js.map +1 -1
- package/dist/sdk/config-change.d.ts +4 -0
- package/dist/sdk/config-change.d.ts.map +1 -1
- package/dist/sdk/config-change.js +27 -5
- package/dist/sdk/config-change.js.map +1 -1
- package/dist/sdk/kb.d.ts +12 -0
- package/dist/sdk/kb.d.ts.map +1 -1
- package/dist/sdk/kb.js +45 -5
- package/dist/sdk/kb.js.map +1 -1
- package/dist/sdk/voice.js +5 -5
- package/dist/sdk/voice.js.map +1 -1
- package/package.json +4 -6
- package/src/mcp/registry.ts +9 -4
- package/src/mcp/tool-meta.ts +35 -9
- package/src/mcp/tools.ts +235 -49
- package/src/report/styles.ts +5 -1
- package/src/sdk/auth.ts +2 -2
- package/src/sdk/client.ts +5 -1
- package/src/sdk/config-change.ts +27 -5
- package/src/sdk/kb.ts +49 -8
- package/src/sdk/voice.ts +5 -5
- package/test/mcp/__snapshots__/tools-snapshot.test.ts.snap +1 -0
- package/test/mcp/darwin-fixes.test.ts +231 -0
- package/test/mcp/tools-snapshot.test.ts +1 -1
package/dist/mcp/tools.js
CHANGED
|
@@ -47,9 +47,11 @@ function withAuthorization(args) {
|
|
|
47
47
|
}
|
|
48
48
|
return { ...args, configuration: { ...(args.configuration ?? {}), authorization } };
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
/** The CI gate's threshold when the caller gives none (evaluations repo scripts/eval_gate.py uses the same). */
|
|
51
|
+
const DEFAULT_MIN_PASS_RATE = 0.8;
|
|
52
|
+
// Helper: format a successful MCP tool result (the registry adds the context and formats it)
|
|
51
53
|
function success(data) {
|
|
52
|
-
return { content: [{ type: 'text', text: JSON.stringify(data
|
|
54
|
+
return { content: [{ type: 'text', text: JSON.stringify(data) }] };
|
|
53
55
|
}
|
|
54
56
|
// Helper: format an error MCP tool result
|
|
55
57
|
function error(err) {
|
|
@@ -121,6 +123,19 @@ const parseJsonText = (v) => {
|
|
|
121
123
|
}
|
|
122
124
|
};
|
|
123
125
|
const jsonObject = () => z.preprocess(parseJsonText, z.record(z.any()));
|
|
126
|
+
// A list a model may also write as JSON text.
|
|
127
|
+
const jsonArray = (item) => z.preprocess(parseJsonText, z.array(item));
|
|
128
|
+
// What the knowledge base import accepts: typed, so a wrong shape is refused here instead of being
|
|
129
|
+
// "imported" as nothing (copilot answers success for any body).
|
|
130
|
+
const kbImportSchema = z.preprocess(parseJsonText, z.object({
|
|
131
|
+
type: z.enum(['file']).describe('Kind of import; files from the organization\'s file store'),
|
|
132
|
+
sources: z.array(z.object({
|
|
133
|
+
name: z.string().optional().describe('File name shown in the knowledge base'),
|
|
134
|
+
file_type: z.string().optional().describe('Extension, e.g. pdf, txt, docx (taken from the name when missing)'),
|
|
135
|
+
url: z.string().describe('The file\'s link in the file store'),
|
|
136
|
+
}).passthrough()).min(1),
|
|
137
|
+
is_qna_import: z.boolean().optional().describe('The file holds question/answer pairs'),
|
|
138
|
+
}).passthrough());
|
|
124
139
|
// A skill for bulk_create_skills with flat fields (the configuration is built as create_function builds it);
|
|
125
140
|
// a proposal from generate_skills_from_doc keeps its own configurationSchema.
|
|
126
141
|
const skillDefinition = z.preprocess(parseJsonText, z.object({
|
|
@@ -159,12 +174,23 @@ export function registerTools(server, client) {
|
|
|
159
174
|
}, (err) => { if (what && notFound.test(err instanceof Error ? err.message : String(err)))
|
|
160
175
|
missing.push(`${what} ${id}`); })
|
|
161
176
|
: undefined;
|
|
177
|
+
const agentId = typeof args.agent_id === 'string' ? args.agent_id : undefined;
|
|
178
|
+
const label = (name) => ({ name: typeof name === 'string' && name ? name : undefined });
|
|
162
179
|
await Promise.all([
|
|
163
180
|
add(args.agent_id ?? args.bot_id, 'agent', (id) => agents.getAgent(id)),
|
|
164
|
-
|
|
181
|
+
agentId ? add(args.function_id, 'skill', (id) => skillOf(agentId, id)) : undefined,
|
|
165
182
|
add(args.kb_id, 'knowledge base', (id) => kb.getKB(id)),
|
|
183
|
+
...(Array.isArray(args.kb_ids) ? args.kb_ids : []).map((id) => add(id, 'knowledge base', (x) => kb.getKB(x))),
|
|
166
184
|
add(args.test_set_id, 'test set', (id) => testSets.getTestSet(id)),
|
|
167
185
|
add(args.rubric_id, 'rubric', (id) => rubrics.getRubric(id)),
|
|
186
|
+
// The rest name things the cards used to show as raw ids.
|
|
187
|
+
typeof args.kb_id === 'string' ? add(args.source_id, null, (id) => kb.findDataSource(String(args.kb_id), id).then((d) => label(d?.question ?? d?.name ?? d?.source_url))) : undefined,
|
|
188
|
+
add(args.ticket_id, null, (id) => tickets.get(id).then((t) => { const x = t; return label([x.ticket_ref, x.title].filter(Boolean).join(' ')); })),
|
|
189
|
+
add(args.rule_id, null, (id) => tickets.listRules({ includeTrashed: true }).then((rules) => label(rules.find((r) => (r._id ?? r.id) === id)?.name))),
|
|
190
|
+
add(args.distribution_id, null, (id) => distribution.list().then((d) => { const p = (d.paths ?? []).find((x) => x.id === id); return label(p ? `${p.path_name ?? ''} (${p.host_name ?? '*'}${p.path_expression ?? ''})`.trim() : undefined); })),
|
|
191
|
+
add(args.template_id, null, (id) => distribution.listTemplates().then((ts) => label(ts.find((t) => t._id === id)?.name))),
|
|
192
|
+
add(args.owner_id, null, (id) => orgs.listMembers().then((ms) => { const m = ms.find((x) => x.user_id === id); return label(m?.name ?? m?.email); })),
|
|
193
|
+
agentId && typeof args.function_id === 'string' ? add(args.parameter_id, null, (id) => skillOf(agentId, String(args.function_id)).then((s) => functions.listParameters(agentId, s._id)).then((ps) => label(ps.find((p) => p._id === id)?.name))) : undefined,
|
|
168
194
|
add(orgId, null, (id) => client.copilotGet(`/organization/${id}`).then((r) => r.organization)),
|
|
169
195
|
]);
|
|
170
196
|
return { names, missing };
|
|
@@ -214,6 +240,20 @@ export function registerTools(server, client) {
|
|
|
214
240
|
const config = new ConfigSDK(client);
|
|
215
241
|
const functions = new FunctionsSDK(client);
|
|
216
242
|
// A skill of an agent by its id.
|
|
243
|
+
/** An export result with a signed download_url: the services hand out unsigned file-store links (403 in a browser). */
|
|
244
|
+
const withDownloadLink = async (result, stored) => {
|
|
245
|
+
if (!stored || typeof stored !== 'string')
|
|
246
|
+
return result;
|
|
247
|
+
const signed = /X-Amz-Signature=/.test(stored) ? stored : await kb.signedLink(stored);
|
|
248
|
+
return { ...result, download_url: signed ?? stored, ...(signed ? {} : { note: 'The link could not be signed; it may need signing in to the dashboard.' }) };
|
|
249
|
+
};
|
|
250
|
+
/** A report file written by this process, put in the organization's file store so the person can download it. */
|
|
251
|
+
const deliver = async (result, upload) => {
|
|
252
|
+
if (!upload)
|
|
253
|
+
return result;
|
|
254
|
+
const { signedUrl, url } = await kb.storeFile(result.file_path);
|
|
255
|
+
return { ...result, download_url: signedUrl ?? url };
|
|
256
|
+
};
|
|
217
257
|
const skillOf = async (agentId, id) => {
|
|
218
258
|
const skill = (await functions.listFunctions(agentId)).find((f) => f._id === id);
|
|
219
259
|
if (!skill)
|
|
@@ -240,10 +280,14 @@ export function registerTools(server, client) {
|
|
|
240
280
|
// ---------------------------------------------------------------------------
|
|
241
281
|
// Agent tools
|
|
242
282
|
// ---------------------------------------------------------------------------
|
|
243
|
-
reg.tool('list_agents', 'List the agents (bots) of the organization: id, name, channels. Start here to find an agent id.', {
|
|
283
|
+
reg.tool('list_agents', 'List the agents (bots) of the organization: id, name, channels. Start here to find an agent id.', {
|
|
284
|
+
channel: z.string().optional().describe('Only agents on this channel, e.g. web, voice, email'),
|
|
285
|
+
search: z.string().optional().describe('Only agents whose name contains this text'),
|
|
286
|
+
}, async (args) => {
|
|
244
287
|
try {
|
|
245
288
|
const pages = await orgPages(client);
|
|
246
289
|
const wanted = args.channel?.trim().toUpperCase();
|
|
290
|
+
const needle = args.search?.trim().toLowerCase();
|
|
247
291
|
// What a person needs, not the stored record (a page of full records ran to 73k characters).
|
|
248
292
|
const rows = (await agents.listAgents()).map((a) => ({
|
|
249
293
|
_id: a._id,
|
|
@@ -255,7 +299,8 @@ export function registerTools(server, client) {
|
|
|
255
299
|
updated_at: a.updatedAt,
|
|
256
300
|
url: `${pages}/ai-studio/bot/${a._id}/configurations`,
|
|
257
301
|
}));
|
|
258
|
-
|
|
302
|
+
const byChannel = wanted ? rows.filter((a) => a.channels.some((c) => c.toUpperCase() === wanted)) : rows;
|
|
303
|
+
return success(needle ? byChannel.filter((a) => a.name.toLowerCase().includes(needle)) : byChannel);
|
|
259
304
|
}
|
|
260
305
|
catch (err) {
|
|
261
306
|
return error(err);
|
|
@@ -363,7 +408,7 @@ export function registerTools(server, client) {
|
|
|
363
408
|
preview: async (a) => {
|
|
364
409
|
const agent = await agents.getAgent(String(a.agent_id));
|
|
365
410
|
const lifecycle = await lifecycleOf(String(a.agent_id));
|
|
366
|
-
return `This will delete agent "${agent.name}"${mayBeLive(lifecycle) ? `, which ${liveText(lifecycle)}` : ''}. It cannot be restored
|
|
411
|
+
return `This will delete agent "${agent.name}"${mayBeLive(lifecycle) ? `, which ${liveText(lifecycle)}` : ''}. It cannot be restored.`;
|
|
367
412
|
},
|
|
368
413
|
});
|
|
369
414
|
const insights = new AgentInsightsSDK(client);
|
|
@@ -410,7 +455,16 @@ export function registerTools(server, client) {
|
|
|
410
455
|
channels: z.array(z.string()).optional().describe('Channels of the copy'),
|
|
411
456
|
}, async (args) => {
|
|
412
457
|
try {
|
|
413
|
-
|
|
458
|
+
const copy = await agents.cloneAgent(args.agent_id, { name: args.name, channels: args.channels });
|
|
459
|
+
// copilot's clone answers { new_agent_id, new_agent_name, ... } and leaves the description empty:
|
|
460
|
+
// carry the original's over.
|
|
461
|
+
const copyId = copy._id ?? copy.new_agent_id;
|
|
462
|
+
const source = await agents.getAgent(args.agent_id);
|
|
463
|
+
if (source.description && !copy.description && copyId) {
|
|
464
|
+
const described = await agents.updateAgent(copyId, { description: source.description }).then(() => true).catch(() => false);
|
|
465
|
+
return success({ ...copy, ...(described ? { description: source.description } : {}) });
|
|
466
|
+
}
|
|
467
|
+
return success(copy);
|
|
414
468
|
}
|
|
415
469
|
catch (err) {
|
|
416
470
|
return error(err);
|
|
@@ -425,7 +479,7 @@ export function registerTools(server, client) {
|
|
|
425
479
|
}
|
|
426
480
|
});
|
|
427
481
|
reg.tool('import_agent', 'Create a new agent in this org from a bundle made by export_agent (possibly from another org or platform). Lists skills that failed and secrets to fill in.', {
|
|
428
|
-
bundle:
|
|
482
|
+
bundle: jsonObject().describe('The bundle returned by export_agent (an object, or JSON text)'),
|
|
429
483
|
name: z.string().optional().describe('Name for the new agent (default: the bundle name)'),
|
|
430
484
|
link_knowledge_bases: z.boolean().optional().describe('Link the same knowledge base ids if they exist in this org'),
|
|
431
485
|
allow_duplicate: z.boolean().optional(),
|
|
@@ -466,12 +520,14 @@ export function registerTools(server, client) {
|
|
|
466
520
|
system_prompt: a.system_prompt,
|
|
467
521
|
greeting: a.first_message,
|
|
468
522
|
model: a.model ? modelArg(String(a.model)) : undefined,
|
|
523
|
+
parameters: a.temperature !== undefined ? { temperature: a.temperature } : undefined,
|
|
469
524
|
});
|
|
470
|
-
reg.tool('set_config', 'Change an agent\'s prompt, greeting and/or
|
|
525
|
+
reg.tool('set_config', 'Change an agent\'s prompt, greeting, model and/or temperature. The confirmation shows the prompt diff and whether the agent is published; published agents need confirm_published too. The result says what was saved (read back from the platform).', {
|
|
471
526
|
agent_id: z.string().describe('The agent ID'),
|
|
472
527
|
system_prompt: z.string().optional().describe('New system prompt (whole text)'),
|
|
473
528
|
first_message: z.string().optional().describe('New greeting shown to users'),
|
|
474
529
|
model: z.string().optional().describe('provider/model, e.g. openai/gpt-4.1 (see list_llm_models)'),
|
|
530
|
+
temperature: z.number().min(0).max(2).optional().describe('Model temperature (0 = most predictable); only for models that expose it'),
|
|
475
531
|
confirm_published: z.boolean().optional().describe('Required with confirm when the agent is published and the user explicitly agreed'),
|
|
476
532
|
}, async (args, meta) => {
|
|
477
533
|
try {
|
|
@@ -528,9 +584,15 @@ export function registerTools(server, client) {
|
|
|
528
584
|
...skillAuthFields,
|
|
529
585
|
configuration: jsonObject().optional().describe('Other configurationSchema fields (an object, or JSON text); the fields above win'),
|
|
530
586
|
transfer_to: z.string().optional().describe('transfer_call: phone number (E.164, e.g. +15551234567) or SIP address (sip:...) calls are transferred to'),
|
|
587
|
+
parameters: jsonArray(z.object({
|
|
588
|
+
name: z.string(), type: z.string().describe('string, number or boolean'),
|
|
589
|
+
description: z.string().optional(), required: z.boolean().optional(),
|
|
590
|
+
allowed_values: z.array(z.string()).optional().describe('Fixed choices the customer picks from'),
|
|
591
|
+
})).optional().describe('What the skill collects from the customer before it runs ({{name}} placeholders in url/payload)'),
|
|
531
592
|
}, async (args) => {
|
|
532
593
|
try {
|
|
533
594
|
const { agent_id, type, name, description } = args;
|
|
595
|
+
const parameters = (args.parameters ?? []);
|
|
534
596
|
const configurationSchema = skillConfiguration(type, withAuthorization(args));
|
|
535
597
|
// copilot sends a REST answer straight to the customer unless craftResponse is on; with nothing to say
|
|
536
598
|
// how to present it, let the agent phrase it.
|
|
@@ -549,10 +611,18 @@ export function registerTools(server, client) {
|
|
|
549
611
|
throw new Error(`The agent already has a skill named "${name}" (function_id ${same._id}). Change it with update_function instead of creating another.`);
|
|
550
612
|
// copilot turns configurationSchema into the executable form only on update: create, then configure.
|
|
551
613
|
const created = await functions.createFunction(agent_id, { type, name, description });
|
|
614
|
+
// Parameters are their own records: a skill created with parameters used to lose them silently.
|
|
615
|
+
const addParameters = async () => {
|
|
616
|
+
const saved = [];
|
|
617
|
+
for (const { allowed_values, ...p } of parameters)
|
|
618
|
+
saved.push(await functions.addParameter(agent_id, created._id, { ...p, allowedValues: allowed_values }));
|
|
619
|
+
return saved;
|
|
620
|
+
};
|
|
552
621
|
if (!configurationSchema)
|
|
553
|
-
return success(created);
|
|
622
|
+
return success(parameters.length ? { ...created, parameters: await addParameters() } : created);
|
|
554
623
|
try {
|
|
555
|
-
|
|
624
|
+
const configured = await functions.updateFunction(agent_id, created._id, { type, description: description ?? '', configurationSchema });
|
|
625
|
+
return success(parameters.length ? { ...configured, parameters: await addParameters() } : configured);
|
|
556
626
|
}
|
|
557
627
|
catch (err) {
|
|
558
628
|
// Leave no half-made skill behind: remove the empty one, then say why nothing was saved.
|
|
@@ -637,10 +707,10 @@ export function registerTools(server, client) {
|
|
|
637
707
|
return error(err);
|
|
638
708
|
}
|
|
639
709
|
});
|
|
640
|
-
reg.tool('export_functions', 'Export every skill of an agent as a JSON file (to copy them or keep a backup before import_functions).', { agent_id: z.string().describe('The agent ID') }, async (args) => {
|
|
710
|
+
reg.tool('export_functions', 'Export every skill of an agent as a JSON file (to copy them or keep a backup before import_functions); download_url is the link to give the person.', { agent_id: z.string().describe('The agent ID') }, async (args) => {
|
|
641
711
|
try {
|
|
642
712
|
const result = await functions.exportFunctions(args.agent_id);
|
|
643
|
-
return success(result);
|
|
713
|
+
return success(await withDownloadLink(result, result.signedUrl ?? result.fileUrl ?? result.url));
|
|
644
714
|
}
|
|
645
715
|
catch (err) {
|
|
646
716
|
return error(err);
|
|
@@ -723,7 +793,7 @@ export function registerTools(server, client) {
|
|
|
723
793
|
catch (err) {
|
|
724
794
|
return error(err);
|
|
725
795
|
}
|
|
726
|
-
});
|
|
796
|
+
}, liveChange('a skill parameter', ['function_id']));
|
|
727
797
|
reg.tool('update_function_parameter', 'Change a skill parameter.', {
|
|
728
798
|
agent_id: z.string().describe('The agent ID'),
|
|
729
799
|
function_id: z.string().describe('The function ID'),
|
|
@@ -742,7 +812,7 @@ export function registerTools(server, client) {
|
|
|
742
812
|
catch (err) {
|
|
743
813
|
return error(err);
|
|
744
814
|
}
|
|
745
|
-
});
|
|
815
|
+
}, liveChange('a skill parameter', ['function_id', 'parameter_id']));
|
|
746
816
|
reg.tool('delete_function_parameter', 'Remove a skill parameter.', {
|
|
747
817
|
agent_id: z.string().describe('The agent ID'),
|
|
748
818
|
function_id: z.string().describe('The function ID'),
|
|
@@ -837,10 +907,11 @@ export function registerTools(server, client) {
|
|
|
837
907
|
return error(err);
|
|
838
908
|
}
|
|
839
909
|
});
|
|
840
|
-
reg.tool('list_kb_data', 'The question/answer pairs typed into a knowledge base (website and file sources are listed by get_knowledge_base).', {
|
|
910
|
+
reg.tool('list_kb_data', 'The question/answer pairs typed into a knowledge base (website and file sources are listed by get_knowledge_base). Each entry: id, question, answer, whether it is trained. full: true adds the stored chunks.', {
|
|
841
911
|
kb_id: z.string().describe('The knowledge base ID'),
|
|
842
912
|
page: z.number().optional().describe('Page number'),
|
|
843
913
|
limit: z.number().optional().describe('Items per page'),
|
|
914
|
+
full: z.boolean().optional().describe('Include the chunks and statistics of every entry'),
|
|
844
915
|
}, async (args) => {
|
|
845
916
|
try {
|
|
846
917
|
const query = {};
|
|
@@ -849,7 +920,16 @@ export function registerTools(server, client) {
|
|
|
849
920
|
if (args.limit !== undefined)
|
|
850
921
|
query.limit = String(args.limit);
|
|
851
922
|
const result = await kb.listDataSources(args.kb_id, Object.keys(query).length ? query : undefined);
|
|
852
|
-
|
|
923
|
+
if (args.full)
|
|
924
|
+
return success(result);
|
|
925
|
+
// The stored record repeats every chunk twice (chunks, chunks_text): a page of it is tens of kilobytes.
|
|
926
|
+
const entries = (result.dataSources ?? []).map((d) => ({
|
|
927
|
+
_id: d._id, type: d.type, question: d.question ?? d.name, answer: d.answer,
|
|
928
|
+
...(d.source_url ? { source_url: d.source_url } : {}),
|
|
929
|
+
trained: d.trained, ...(d.status ? { status: d.status } : {}),
|
|
930
|
+
...(d.error_message ? { error: d.error_message } : {}),
|
|
931
|
+
}));
|
|
932
|
+
return success({ ...result, dataSources: entries });
|
|
853
933
|
}
|
|
854
934
|
catch (err) {
|
|
855
935
|
return error(err);
|
|
@@ -1003,25 +1083,32 @@ export function registerTools(server, client) {
|
|
|
1003
1083
|
return error(err);
|
|
1004
1084
|
}
|
|
1005
1085
|
});
|
|
1006
|
-
reg.tool('export_kb', 'Export the question/answer pairs of a knowledge base as a CSV or XLSX
|
|
1086
|
+
reg.tool('export_kb', 'Export the question/answer pairs of a knowledge base as a CSV or XLSX file; download_url is the link to give the person (valid for a while).', {
|
|
1007
1087
|
kb_id: z.string().describe('The knowledge base ID'),
|
|
1008
|
-
format: z.string().optional().describe('Export format (
|
|
1088
|
+
format: z.string().optional().describe('Export format: csv (default) or xlsx'),
|
|
1009
1089
|
}, async (args) => {
|
|
1010
1090
|
try {
|
|
1011
|
-
const result = await kb.exportKB(args.kb_id, args.format || '
|
|
1012
|
-
return success(result);
|
|
1091
|
+
const result = await kb.exportKB(args.kb_id, args.format || 'csv');
|
|
1092
|
+
return success(await withDownloadLink(result, result.signedUrl ?? result.fileUrl));
|
|
1013
1093
|
}
|
|
1014
1094
|
catch (err) {
|
|
1015
1095
|
return error(err);
|
|
1016
1096
|
}
|
|
1017
1097
|
});
|
|
1018
|
-
reg.tool('import_kb', 'Import
|
|
1098
|
+
reg.tool('import_kb', 'Import files (already in the organization\'s file store) into a knowledge base; processed in the background (see kb_scrape_status). For a website use scrape_url.', {
|
|
1019
1099
|
kb_id: z.string().describe('The knowledge base ID'),
|
|
1020
|
-
data:
|
|
1100
|
+
data: kbImportSchema.describe('{ type: "file", sources: [{ name, file_type, url }] } (an object, or JSON text)'),
|
|
1021
1101
|
}, async (args) => {
|
|
1022
1102
|
try {
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1103
|
+
const data = args.data;
|
|
1104
|
+
// The import service tells the file's kind from file_type: fill it from the name or the link.
|
|
1105
|
+
const sources = data.sources.map((s) => ({
|
|
1106
|
+
...s,
|
|
1107
|
+
name: s.name ?? s.url.split('?')[0].split('/').pop() ?? 'file',
|
|
1108
|
+
file_type: (s.file_type ?? (s.name ?? s.url.split('?')[0]).split('.').pop() ?? '').toLowerCase(),
|
|
1109
|
+
}));
|
|
1110
|
+
const result = await kb.importKB(args.kb_id, { ...data, sources });
|
|
1111
|
+
return success({ ...result, imported: sources.map((s) => s.name) });
|
|
1025
1112
|
}
|
|
1026
1113
|
catch (err) {
|
|
1027
1114
|
return error(err);
|
|
@@ -1067,7 +1154,8 @@ export function registerTools(server, client) {
|
|
|
1067
1154
|
return success({
|
|
1068
1155
|
conversation_id: conversation.request_id, test: conversation.isTestMode === true,
|
|
1069
1156
|
url: conversationUrl(client, projectId, conversation.request_id),
|
|
1070
|
-
|
|
1157
|
+
// System lines carry the dashboard's template ("{{userLabel}} has ended the call"): say it in words.
|
|
1158
|
+
messages: messages.map((m) => ({ role: m.role, text: (m.text ?? '').replace(/\{\{\s*userLabel\s*\}\}/g, 'The customer'), at: m.createdAt })),
|
|
1071
1159
|
});
|
|
1072
1160
|
}
|
|
1073
1161
|
catch (err) {
|
|
@@ -1157,9 +1245,16 @@ export function registerTools(server, client) {
|
|
|
1157
1245
|
return error(err);
|
|
1158
1246
|
}
|
|
1159
1247
|
});
|
|
1160
|
-
reg.tool('list_agent_numbers', 'Phone numbers attached to an agent, or (without agent_id) every number the org owns.', { agent_id: z.string().optional() }, async (args) => {
|
|
1248
|
+
reg.tool('list_agent_numbers', 'Phone numbers attached to an agent, or (without agent_id) every number the org owns: the ones attached to an agent (with the agent) and the free ones.', { agent_id: z.string().optional() }, async (args) => {
|
|
1161
1249
|
try {
|
|
1162
|
-
|
|
1250
|
+
if (args.agent_id)
|
|
1251
|
+
return success(await voice.agentNumbers(args.agent_id));
|
|
1252
|
+
// copilot's org list holds only unattached ("reserve") numbers; the attached ones sit on the agents.
|
|
1253
|
+
const [free, all] = await Promise.all([voice.orgNumbers(), agents.listAgents()]);
|
|
1254
|
+
const attached = all
|
|
1255
|
+
.filter((a) => a.connected_phone)
|
|
1256
|
+
.map((a) => ({ phone_number: a.connected_phone, attached: true, agent_id: a._id, agent_name: a.name }));
|
|
1257
|
+
return success([...attached, ...free.map((n) => ({ phone_number: n.phoneNumber, attached: false, name: n.friendlyName, type: n.type }))]);
|
|
1163
1258
|
}
|
|
1164
1259
|
catch (err) {
|
|
1165
1260
|
return error(err);
|
|
@@ -1204,7 +1299,7 @@ export function registerTools(server, client) {
|
|
|
1204
1299
|
try {
|
|
1205
1300
|
// A purchase needs the person, not the model: confirm: true from a host without approval forms is not enough.
|
|
1206
1301
|
if (!meta.confirmedByPerson)
|
|
1207
|
-
throw new Error('Buying a number needs a person to approve it in an approval form, and this host cannot show one. Nothing was bought. Buy it in the dashboard
|
|
1302
|
+
throw new Error('Buying a number needs a person to approve it in an approval form, and this host cannot show one. Nothing was bought. Buy it in the dashboard instead.');
|
|
1208
1303
|
return success(await voice.buyNumber(args.agent_id, args.phone_number, { country: args.country, tollFree: args.toll_free }));
|
|
1209
1304
|
}
|
|
1210
1305
|
catch (err) {
|
|
@@ -1365,17 +1460,63 @@ export function registerTools(server, client) {
|
|
|
1365
1460
|
return error(err);
|
|
1366
1461
|
}
|
|
1367
1462
|
});
|
|
1368
|
-
|
|
1463
|
+
const DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
1464
|
+
const clock = (hhmm) => {
|
|
1465
|
+
const [h, m] = hhmm.split(':').map(Number);
|
|
1466
|
+
const hour = ((h + 11) % 12) + 1;
|
|
1467
|
+
return `${hour}${m ? `:${String(m).padStart(2, '0')}` : ''}${h < 12 ? 'am' : 'pm'}`;
|
|
1468
|
+
};
|
|
1469
|
+
/** What agents say about the hours, built from the hours themselves so it never contradicts them. */
|
|
1470
|
+
const hoursMessage = (days) => {
|
|
1471
|
+
const open = days.filter((d) => d.slots.length).sort((a, b) => a.day - b.day);
|
|
1472
|
+
if (!open.length)
|
|
1473
|
+
return 'We are currently closed.';
|
|
1474
|
+
const parts = open.map((d) => `${DAY_NAMES[d.day]} ${d.slots.map((s) => `${clock(s.start)} to ${clock(s.end)}`).join(' and ')}`);
|
|
1475
|
+
return `We're open ${parts.join('; ')}.`;
|
|
1476
|
+
};
|
|
1477
|
+
reg.tool('set_business_hours', 'Replace the org business hours: every day not listed is closed. days: 0 Sunday to 6. enabled turns enforcement on or off (unchanged when omitted). The out-of-hours message is rebuilt from the hours unless given.', {
|
|
1478
|
+
enabled: z.boolean().optional().describe('Enforce the hours (agents say the out-of-hours message outside them); unchanged when omitted'),
|
|
1479
|
+
hours: jsonArray(z.object({ day: z.number().int().min(0).max(6), slots: z.array(z.object({ start: z.string().describe('HH:MM'), end: z.string().describe('HH:MM') })) })).optional(),
|
|
1480
|
+
out_of_hours_message: z.string().optional(),
|
|
1481
|
+
}, async (args) => {
|
|
1369
1482
|
try {
|
|
1370
|
-
|
|
1483
|
+
const current = (await orgSettings.get());
|
|
1484
|
+
const enabled = args.enabled ?? Boolean(current.businessHoursConfig?.businessHoursEnabled);
|
|
1485
|
+
const given = (args.hours ?? []);
|
|
1486
|
+
// Replace means replace: a day the person did not mention is closed.
|
|
1487
|
+
const days = args.hours ? DAY_NAMES.map((_, day) => ({ day, slots: given.filter((d) => d.day === day).flatMap((d) => d.slots) })) : undefined;
|
|
1488
|
+
return success(await orgSettings.setBusinessHours({
|
|
1489
|
+
businessHoursEnabled: enabled,
|
|
1490
|
+
...(days ? { businessHours: days, businessHoursUtterance: args.out_of_hours_message ?? hoursMessage(days) } : {}),
|
|
1491
|
+
...(!days && args.out_of_hours_message ? { businessHoursUtterance: args.out_of_hours_message } : {}),
|
|
1492
|
+
}));
|
|
1493
|
+
}
|
|
1494
|
+
catch (err) {
|
|
1495
|
+
return error(err);
|
|
1496
|
+
}
|
|
1497
|
+
}, { preview: async (a) => {
|
|
1498
|
+
const hours = (a.hours ?? []);
|
|
1499
|
+
const what = a.hours ? hoursMessage(DAY_NAMES.map((_, day) => ({ day, slots: hours.filter((d) => d.day === day).flatMap((d) => d.slots) }))) : 'the hours unchanged';
|
|
1500
|
+
return `This will replace the org business hours: ${what}${a.enabled === undefined ? '' : a.enabled ? ' Enforced: agents say the out-of-hours message outside them.' : ' Not enforced.'}`;
|
|
1501
|
+
} });
|
|
1502
|
+
reg.tool('add_holiday', 'Close the org on a date.', {
|
|
1503
|
+
date: z.string().describe('YYYY-MM-DD'), name: z.string(),
|
|
1504
|
+
message: z.string().optional().describe('What agents say that day (default: closed on account of the holiday)'),
|
|
1505
|
+
type: z.string().optional().describe('public or custom (default custom)'),
|
|
1506
|
+
}, async (args) => {
|
|
1507
|
+
try {
|
|
1508
|
+
return success(await orgSettings.addHolidays([{
|
|
1509
|
+
name: args.name, date: args.date, type: args.type ?? 'custom',
|
|
1510
|
+
holidayFallbackUtterance: args.message ?? `Our offices are currently closed on account of ${args.name}.`,
|
|
1511
|
+
}]));
|
|
1371
1512
|
}
|
|
1372
1513
|
catch (err) {
|
|
1373
1514
|
return error(err);
|
|
1374
1515
|
}
|
|
1375
1516
|
});
|
|
1376
|
-
reg.tool('
|
|
1517
|
+
reg.tool('remove_holiday', 'Open the org again on a date that was a holiday.', { date: z.string().describe('YYYY-MM-DD, as get_org_settings lists it') }, async (args) => {
|
|
1377
1518
|
try {
|
|
1378
|
-
return success(await orgSettings.
|
|
1519
|
+
return success(await orgSettings.removeHolidays([args.date]));
|
|
1379
1520
|
}
|
|
1380
1521
|
catch (err) {
|
|
1381
1522
|
return error(err);
|
|
@@ -1409,7 +1550,14 @@ export function registerTools(server, client) {
|
|
|
1409
1550
|
}, { preview: async (a) => `This will ${(await orgSettings.planAdmin(a)).changes.join('; ')}.` });
|
|
1410
1551
|
reg.tool('list_audit_logs', 'Who changed what in the org, newest first, filtered here (the server has no filters).', { since: z.string().optional(), action: z.string().optional(), resource_type: z.string().optional(), user_id: z.string().optional(), limit: z.number().int().min(1).max(500).optional() }, async (args) => {
|
|
1411
1552
|
try {
|
|
1412
|
-
|
|
1553
|
+
const entries = await new AuditSDK(client).list({ since: args.since, action: args.action, resourceType: args.resource_type, user: args.user_id, limit: args.limit });
|
|
1554
|
+
// copilot's log carries the person's id only: name them from the member list (one read).
|
|
1555
|
+
const members = entries.some((e) => e.id_user) ? await orgs.listMembers().catch(() => []) : [];
|
|
1556
|
+
const byId = new Map(members.map((m) => [m.user_id, m]));
|
|
1557
|
+
return success(entries.map((e) => {
|
|
1558
|
+
const m = byId.get(String(e.id_user ?? ''));
|
|
1559
|
+
return m ? { ...e, user_name: m.name, user_email: m.email } : e;
|
|
1560
|
+
}));
|
|
1413
1561
|
}
|
|
1414
1562
|
catch (err) {
|
|
1415
1563
|
return error(err);
|
|
@@ -1450,10 +1598,12 @@ export function registerTools(server, client) {
|
|
|
1450
1598
|
return error(err);
|
|
1451
1599
|
}
|
|
1452
1600
|
});
|
|
1453
|
-
reg.tool('update_ticket', 'Change a ticket: fields, assignee (user id, or unassign: true), or close it with resolve: true
|
|
1454
|
-
ticket_id: z.string(), title: z.string().optional(), description: z.string().optional(),
|
|
1601
|
+
reg.tool('update_ticket', 'Change a ticket: fields, a note (added to its notes, or the closing note with resolve), assignee (user id, or unassign: true), or close it with resolve: true.', {
|
|
1602
|
+
ticket_id: z.string(), title: z.string().optional(), description: z.string().optional(),
|
|
1603
|
+
notes: z.string().optional().describe('Replace the whole notes text'),
|
|
1455
1604
|
status: z.enum(['not_started', 'in_progress']).optional(), priority: ticketPriority.optional(),
|
|
1456
|
-
assignee_user_id: z.string().optional(), unassign: z.boolean().optional(), resolve: z.boolean().optional(),
|
|
1605
|
+
assignee_user_id: z.string().optional(), unassign: z.boolean().optional(), resolve: z.boolean().optional(),
|
|
1606
|
+
note: z.string().optional().describe('A note to add: appended to the ticket\'s notes, or the closing note when resolving'),
|
|
1457
1607
|
link_ticket_id: z.string().optional().describe('Link to another ticket'),
|
|
1458
1608
|
}, async (args) => {
|
|
1459
1609
|
try {
|
|
@@ -1462,6 +1612,11 @@ export function registerTools(server, client) {
|
|
|
1462
1612
|
for (const k of ['title', 'description', 'notes', 'status', 'priority'])
|
|
1463
1613
|
if (args[k] !== undefined)
|
|
1464
1614
|
changes[k] = args[k];
|
|
1615
|
+
// A note outside resolve used to be dropped silently: it goes onto the ticket's notes.
|
|
1616
|
+
if (args.note && !args.resolve) {
|
|
1617
|
+
const existing = String((await tickets.get(args.ticket_id)).notes ?? '').trim();
|
|
1618
|
+
changes.notes = args.notes !== undefined ? `${args.notes}\n${args.note}`.trim() : existing ? `${existing}\n${args.note}` : args.note;
|
|
1619
|
+
}
|
|
1465
1620
|
if (Object.keys(changes).length)
|
|
1466
1621
|
out.updated = await tickets.update(args.ticket_id, changes);
|
|
1467
1622
|
if (args.assignee_user_id || args.unassign)
|
|
@@ -1573,7 +1728,7 @@ export function registerTools(server, client) {
|
|
|
1573
1728
|
return error(err);
|
|
1574
1729
|
}
|
|
1575
1730
|
});
|
|
1576
|
-
reg.tool('platform_save', 'Create (no id) or change (id) a V2 agent, template, connector binding or scenario. Components and connectors are read-only shared catalogs.', { resource: z.enum(['agents', 'templates', 'connector-bindings', 'scenarios']), id: z.string().optional(), body:
|
|
1731
|
+
reg.tool('platform_save', 'Create (no id) or change (id) a V2 agent, template, connector binding or scenario. Components and connectors are read-only shared catalogs.', { resource: z.enum(['agents', 'templates', 'connector-bindings', 'scenarios']), id: z.string().optional(), body: jsonObject().describe('The item to save (an object, or JSON text)') }, async (args) => {
|
|
1577
1732
|
try {
|
|
1578
1733
|
return success(args.id ? await platform.update(args.resource, args.id, args.body) : await platform.create(args.resource, args.body));
|
|
1579
1734
|
}
|
|
@@ -1758,7 +1913,7 @@ export function registerTools(server, client) {
|
|
|
1758
1913
|
});
|
|
1759
1914
|
reg.tool('eval_gate_verdict', 'Judge a finished run like the CI gate: pass rate = passed/total, optional p95 limit, failed runs fail by default.', {
|
|
1760
1915
|
run_id: z.string(),
|
|
1761
|
-
min_pass_rate: z.number().min(0).max(1).optional(),
|
|
1916
|
+
min_pass_rate: z.number().min(0).max(1).optional().describe('Pass rate the run must reach (default 0.8)'),
|
|
1762
1917
|
max_p95_ms: z.number().optional(),
|
|
1763
1918
|
error_policy: z.enum(['block', 'warn']).optional(),
|
|
1764
1919
|
fail_on_error: z.boolean().optional(),
|
|
@@ -1767,7 +1922,9 @@ export function registerTools(server, client) {
|
|
|
1767
1922
|
const run = await evals.getRunInOrg(args.run_id);
|
|
1768
1923
|
if (run.status !== 'completed' && run.status !== 'failed')
|
|
1769
1924
|
return success({ finished: false, status: run.status });
|
|
1770
|
-
|
|
1925
|
+
// Without a threshold any finished run "passed" (a 1-of-3 run was reported as a pass): judge against 80%.
|
|
1926
|
+
const minPassRate = args.min_pass_rate ?? DEFAULT_MIN_PASS_RATE;
|
|
1927
|
+
return success({ finished: true, min_pass_rate: minPassRate, ...gateVerdict(run, { minPassRate, maxP95Ms: args.max_p95_ms, errorPolicy: args.error_policy, failOnError: args.fail_on_error }) });
|
|
1771
1928
|
}
|
|
1772
1929
|
catch (err) {
|
|
1773
1930
|
return error(err);
|
|
@@ -1862,19 +2019,41 @@ export function registerTools(server, client) {
|
|
|
1862
2019
|
return error(err);
|
|
1863
2020
|
}
|
|
1864
2021
|
});
|
|
1865
|
-
|
|
2022
|
+
// get_conversation says "agent"; the evaluations service says "bot". Both are accepted.
|
|
2023
|
+
const transcriptTurn = z.object({ sender: z.enum(['user', 'bot', 'agent', 'system']), text: z.string() });
|
|
2024
|
+
const transcriptForService = (turns) => turns
|
|
2025
|
+
.filter((t) => t.sender !== 'system')
|
|
2026
|
+
.map((t) => ({ sender: (t.sender === 'user' ? 'user' : 'bot'), text: t.text }));
|
|
2027
|
+
const transcriptPreconditions = async (a) => {
|
|
2028
|
+
const agentId = String(a.agent_id);
|
|
2029
|
+
await evals.assertAgentInOrg(agentId);
|
|
2030
|
+
const configs = await evals.listEvaluatorConfigs(agentId);
|
|
2031
|
+
const count = Array.isArray(configs) ? configs.length : (configs.items ?? []).length;
|
|
2032
|
+
if (!count)
|
|
2033
|
+
throw new Error('This agent has no evaluator yet, so a transcript cannot be graded. Create a rubric and run an evaluation first (run_evaluation stores the evaluator).');
|
|
2034
|
+
if (JSON.stringify(a.baseline_transcript) === JSON.stringify(a.replay_transcript)) {
|
|
2035
|
+
throw new Error('The baseline and the replay are the same conversation; grading compares a replay with its baseline. Give the replayed conversation as replay_transcript.');
|
|
2036
|
+
}
|
|
2037
|
+
};
|
|
2038
|
+
reg.tool('evaluate_transcript', 'Grade a replayed conversation against a baseline with the agent\'s stored evaluator (paid model calls). The agent needs an evaluator (a finished run), and the replay must differ from the baseline.', {
|
|
1866
2039
|
agent_id: z.string(),
|
|
1867
|
-
baseline_transcript:
|
|
1868
|
-
replay_transcript:
|
|
2040
|
+
baseline_transcript: jsonArray(transcriptTurn),
|
|
2041
|
+
replay_transcript: jsonArray(transcriptTurn),
|
|
1869
2042
|
}, async (args) => {
|
|
1870
2043
|
try {
|
|
1871
|
-
await
|
|
1872
|
-
return success(await evals.evaluateTranscript(
|
|
2044
|
+
await transcriptPreconditions(args);
|
|
2045
|
+
return success(await evals.evaluateTranscript({
|
|
2046
|
+
agent_id: args.agent_id,
|
|
2047
|
+
baseline_transcript: transcriptForService(args.baseline_transcript),
|
|
2048
|
+
replay_transcript: transcriptForService(args.replay_transcript),
|
|
2049
|
+
}));
|
|
1873
2050
|
}
|
|
1874
2051
|
catch (err) {
|
|
1875
2052
|
return error(err);
|
|
1876
2053
|
}
|
|
1877
|
-
}
|
|
2054
|
+
},
|
|
2055
|
+
// Checked before the approval card: a card for something that cannot run wastes the person's answer.
|
|
2056
|
+
{ preview: async (a) => { await transcriptPreconditions(a); return `This will grade a replayed transcript for agent ${a.agent_id} with paid model calls.`; } });
|
|
1878
2057
|
reg.tool('list_evaluator_configs', 'Stored evaluator configs of an agent (which judges and rubric a run uses).', { agent_id: z.string() }, async (args) => {
|
|
1879
2058
|
try {
|
|
1880
2059
|
await evals.assertAgentInOrg(args.agent_id);
|
|
@@ -2060,13 +2239,15 @@ export function registerTools(server, client) {
|
|
|
2060
2239
|
// ---------------------------------------------------------------------------
|
|
2061
2240
|
// Export tools
|
|
2062
2241
|
// ---------------------------------------------------------------------------
|
|
2242
|
+
const uploadArg = z.boolean().optional().describe('Put the file in the organization\'s file store and return download_url (use it when the person cannot reach this machine\'s files)');
|
|
2063
2243
|
reg.tool('export_eval_markdown', 'Export evaluation results as a structured markdown file with conversations, scores, and criteria', {
|
|
2064
2244
|
run_id: z.string().describe('The evaluation run ID'),
|
|
2065
2245
|
output_path: z.string().optional().describe('Output file path (defaults to current directory)'),
|
|
2246
|
+
upload: uploadArg,
|
|
2066
2247
|
}, async (args) => {
|
|
2067
2248
|
try {
|
|
2068
2249
|
const result = await exportEvalMarkdown(args.run_id, args.output_path, client);
|
|
2069
|
-
return success(result);
|
|
2250
|
+
return success(await deliver(result, args.upload));
|
|
2070
2251
|
}
|
|
2071
2252
|
catch (err) {
|
|
2072
2253
|
return error(err);
|
|
@@ -2075,10 +2256,11 @@ export function registerTools(server, client) {
|
|
|
2075
2256
|
reg.tool('export_eval_report', 'Generate a branded PDF evaluation report with cover page, scores, and per-item details', {
|
|
2076
2257
|
run_id: z.string().describe('The evaluation run ID'),
|
|
2077
2258
|
output_path: z.string().optional().describe('Output file path (defaults to current directory)'),
|
|
2259
|
+
upload: uploadArg,
|
|
2078
2260
|
}, async (args) => {
|
|
2079
2261
|
try {
|
|
2080
2262
|
const result = await exportEvalReport(args.run_id, args.output_path, client);
|
|
2081
|
-
return success(result);
|
|
2263
|
+
return success(await deliver(result, args.upload));
|
|
2082
2264
|
}
|
|
2083
2265
|
catch (err) {
|
|
2084
2266
|
return error(err);
|
|
@@ -2087,10 +2269,11 @@ export function registerTools(server, client) {
|
|
|
2087
2269
|
reg.tool('export_agent_card', 'Generate a branded PDF agent card with system prompt, tools, KBs, LLM config, and evaluation history', {
|
|
2088
2270
|
agent_id: z.string().describe('The agent ID'),
|
|
2089
2271
|
output_path: z.string().optional().describe('Output file path (defaults to current directory)'),
|
|
2272
|
+
upload: uploadArg,
|
|
2090
2273
|
}, async (args) => {
|
|
2091
2274
|
try {
|
|
2092
2275
|
const result = await exportAgentCard(args.agent_id, args.output_path, client);
|
|
2093
|
-
return success(result);
|
|
2276
|
+
return success(await deliver(result, args.upload));
|
|
2094
2277
|
}
|
|
2095
2278
|
catch (err) {
|
|
2096
2279
|
return error(err);
|