@indemn/cli 1.7.1 → 1.7.3
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/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 +235 -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/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 +13 -0
- package/dist/sdk/kb.d.ts.map +1 -1
- package/dist/sdk/kb.js +48 -6
- 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 +2 -2
- package/src/mcp/registry.ts +9 -4
- package/src/mcp/tool-meta.ts +35 -9
- package/src/mcp/tools.ts +238 -49
- package/src/report/styles.ts +5 -1
- package/src/sdk/client.ts +5 -1
- package/src/sdk/config-change.ts +27 -5
- package/src/sdk/kb.ts +53 -10
- 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 +249 -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,26 @@ 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
|
+
// Q&A entries come from the entry list; files and pages only from the knowledge base's sources.
|
|
188
|
+
typeof args.kb_id === 'string' ? add(args.source_id, null, (id) => kb.findDataSource(String(args.kb_id), id).then((d) => d
|
|
189
|
+
? label(d.question ?? d.name ?? d.source_url)
|
|
190
|
+
: kb.scrapeStatus(String(args.kb_id)).then((all) => { const s = all.find((x) => x.id === id); return label(s?.name ?? s?.source); }))) : undefined,
|
|
191
|
+
add(args.ticket_id, null, (id) => tickets.get(id).then((t) => { const x = t; return label([x.ticket_ref, x.title].filter(Boolean).join(' ')); })),
|
|
192
|
+
add(args.rule_id, null, (id) => tickets.listRules({ includeTrashed: true }).then((rules) => label(rules.find((r) => (r._id ?? r.id) === id)?.name))),
|
|
193
|
+
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); })),
|
|
194
|
+
add(args.template_id, null, (id) => distribution.listTemplates().then((ts) => label(ts.find((t) => t._id === id)?.name))),
|
|
195
|
+
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); })),
|
|
196
|
+
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
197
|
add(orgId, null, (id) => client.copilotGet(`/organization/${id}`).then((r) => r.organization)),
|
|
169
198
|
]);
|
|
170
199
|
return { names, missing };
|
|
@@ -214,6 +243,20 @@ export function registerTools(server, client) {
|
|
|
214
243
|
const config = new ConfigSDK(client);
|
|
215
244
|
const functions = new FunctionsSDK(client);
|
|
216
245
|
// A skill of an agent by its id.
|
|
246
|
+
/** An export result with a signed download_url: the services hand out unsigned file-store links (403 in a browser). */
|
|
247
|
+
const withDownloadLink = async (result, stored) => {
|
|
248
|
+
if (!stored || typeof stored !== 'string')
|
|
249
|
+
return result;
|
|
250
|
+
const signed = /X-Amz-Signature=/.test(stored) ? stored : await kb.signedLink(stored);
|
|
251
|
+
return { ...result, download_url: signed ?? stored, ...(signed ? {} : { note: 'The link could not be signed; it may need signing in to the dashboard.' }) };
|
|
252
|
+
};
|
|
253
|
+
/** A report file written by this process, put in the organization's file store so the person can download it. */
|
|
254
|
+
const deliver = async (result, upload) => {
|
|
255
|
+
if (!upload)
|
|
256
|
+
return result;
|
|
257
|
+
const { signedUrl, url } = await kb.storeFile(result.file_path);
|
|
258
|
+
return { ...result, download_url: signedUrl ?? url };
|
|
259
|
+
};
|
|
217
260
|
const skillOf = async (agentId, id) => {
|
|
218
261
|
const skill = (await functions.listFunctions(agentId)).find((f) => f._id === id);
|
|
219
262
|
if (!skill)
|
|
@@ -240,10 +283,14 @@ export function registerTools(server, client) {
|
|
|
240
283
|
// ---------------------------------------------------------------------------
|
|
241
284
|
// Agent tools
|
|
242
285
|
// ---------------------------------------------------------------------------
|
|
243
|
-
reg.tool('list_agents', 'List the agents (bots) of the organization: id, name, channels. Start here to find an agent id.', {
|
|
286
|
+
reg.tool('list_agents', 'List the agents (bots) of the organization: id, name, channels. Start here to find an agent id.', {
|
|
287
|
+
channel: z.string().optional().describe('Only agents on this channel, e.g. web, voice, email'),
|
|
288
|
+
search: z.string().optional().describe('Only agents whose name contains this text'),
|
|
289
|
+
}, async (args) => {
|
|
244
290
|
try {
|
|
245
291
|
const pages = await orgPages(client);
|
|
246
292
|
const wanted = args.channel?.trim().toUpperCase();
|
|
293
|
+
const needle = args.search?.trim().toLowerCase();
|
|
247
294
|
// What a person needs, not the stored record (a page of full records ran to 73k characters).
|
|
248
295
|
const rows = (await agents.listAgents()).map((a) => ({
|
|
249
296
|
_id: a._id,
|
|
@@ -255,7 +302,8 @@ export function registerTools(server, client) {
|
|
|
255
302
|
updated_at: a.updatedAt,
|
|
256
303
|
url: `${pages}/ai-studio/bot/${a._id}/configurations`,
|
|
257
304
|
}));
|
|
258
|
-
|
|
305
|
+
const byChannel = wanted ? rows.filter((a) => a.channels.some((c) => c.toUpperCase() === wanted)) : rows;
|
|
306
|
+
return success(needle ? byChannel.filter((a) => a.name.toLowerCase().includes(needle)) : byChannel);
|
|
259
307
|
}
|
|
260
308
|
catch (err) {
|
|
261
309
|
return error(err);
|
|
@@ -363,7 +411,7 @@ export function registerTools(server, client) {
|
|
|
363
411
|
preview: async (a) => {
|
|
364
412
|
const agent = await agents.getAgent(String(a.agent_id));
|
|
365
413
|
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
|
|
414
|
+
return `This will delete agent "${agent.name}"${mayBeLive(lifecycle) ? `, which ${liveText(lifecycle)}` : ''}. It cannot be restored.`;
|
|
367
415
|
},
|
|
368
416
|
});
|
|
369
417
|
const insights = new AgentInsightsSDK(client);
|
|
@@ -410,7 +458,16 @@ export function registerTools(server, client) {
|
|
|
410
458
|
channels: z.array(z.string()).optional().describe('Channels of the copy'),
|
|
411
459
|
}, async (args) => {
|
|
412
460
|
try {
|
|
413
|
-
|
|
461
|
+
const copy = await agents.cloneAgent(args.agent_id, { name: args.name, channels: args.channels });
|
|
462
|
+
// copilot's clone answers { new_agent_id, new_agent_name, ... } and leaves the description empty:
|
|
463
|
+
// carry the original's over.
|
|
464
|
+
const copyId = copy._id ?? copy.new_agent_id;
|
|
465
|
+
const source = await agents.getAgent(args.agent_id);
|
|
466
|
+
if (source.description && !copy.description && copyId) {
|
|
467
|
+
const described = await agents.updateAgent(copyId, { description: source.description }).then(() => true).catch(() => false);
|
|
468
|
+
return success({ ...copy, ...(described ? { description: source.description } : {}) });
|
|
469
|
+
}
|
|
470
|
+
return success(copy);
|
|
414
471
|
}
|
|
415
472
|
catch (err) {
|
|
416
473
|
return error(err);
|
|
@@ -425,7 +482,7 @@ export function registerTools(server, client) {
|
|
|
425
482
|
}
|
|
426
483
|
});
|
|
427
484
|
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:
|
|
485
|
+
bundle: jsonObject().describe('The bundle returned by export_agent (an object, or JSON text)'),
|
|
429
486
|
name: z.string().optional().describe('Name for the new agent (default: the bundle name)'),
|
|
430
487
|
link_knowledge_bases: z.boolean().optional().describe('Link the same knowledge base ids if they exist in this org'),
|
|
431
488
|
allow_duplicate: z.boolean().optional(),
|
|
@@ -466,12 +523,14 @@ export function registerTools(server, client) {
|
|
|
466
523
|
system_prompt: a.system_prompt,
|
|
467
524
|
greeting: a.first_message,
|
|
468
525
|
model: a.model ? modelArg(String(a.model)) : undefined,
|
|
526
|
+
parameters: a.temperature !== undefined ? { temperature: a.temperature } : undefined,
|
|
469
527
|
});
|
|
470
|
-
reg.tool('set_config', 'Change an agent\'s prompt, greeting and/or
|
|
528
|
+
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
529
|
agent_id: z.string().describe('The agent ID'),
|
|
472
530
|
system_prompt: z.string().optional().describe('New system prompt (whole text)'),
|
|
473
531
|
first_message: z.string().optional().describe('New greeting shown to users'),
|
|
474
532
|
model: z.string().optional().describe('provider/model, e.g. openai/gpt-4.1 (see list_llm_models)'),
|
|
533
|
+
temperature: z.number().min(0).max(2).optional().describe('Model temperature (0 = most predictable); only for models that expose it'),
|
|
475
534
|
confirm_published: z.boolean().optional().describe('Required with confirm when the agent is published and the user explicitly agreed'),
|
|
476
535
|
}, async (args, meta) => {
|
|
477
536
|
try {
|
|
@@ -528,9 +587,15 @@ export function registerTools(server, client) {
|
|
|
528
587
|
...skillAuthFields,
|
|
529
588
|
configuration: jsonObject().optional().describe('Other configurationSchema fields (an object, or JSON text); the fields above win'),
|
|
530
589
|
transfer_to: z.string().optional().describe('transfer_call: phone number (E.164, e.g. +15551234567) or SIP address (sip:...) calls are transferred to'),
|
|
590
|
+
parameters: jsonArray(z.object({
|
|
591
|
+
name: z.string(), type: z.string().describe('string, number or boolean'),
|
|
592
|
+
description: z.string().optional(), required: z.boolean().optional(),
|
|
593
|
+
allowed_values: z.array(z.string()).optional().describe('Fixed choices the customer picks from'),
|
|
594
|
+
})).optional().describe('What the skill collects from the customer before it runs ({{name}} placeholders in url/payload)'),
|
|
531
595
|
}, async (args) => {
|
|
532
596
|
try {
|
|
533
597
|
const { agent_id, type, name, description } = args;
|
|
598
|
+
const parameters = (args.parameters ?? []);
|
|
534
599
|
const configurationSchema = skillConfiguration(type, withAuthorization(args));
|
|
535
600
|
// copilot sends a REST answer straight to the customer unless craftResponse is on; with nothing to say
|
|
536
601
|
// how to present it, let the agent phrase it.
|
|
@@ -549,10 +614,18 @@ export function registerTools(server, client) {
|
|
|
549
614
|
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
615
|
// copilot turns configurationSchema into the executable form only on update: create, then configure.
|
|
551
616
|
const created = await functions.createFunction(agent_id, { type, name, description });
|
|
617
|
+
// Parameters are their own records: a skill created with parameters used to lose them silently.
|
|
618
|
+
const addParameters = async () => {
|
|
619
|
+
const saved = [];
|
|
620
|
+
for (const { allowed_values, ...p } of parameters)
|
|
621
|
+
saved.push(await functions.addParameter(agent_id, created._id, { ...p, allowedValues: allowed_values }));
|
|
622
|
+
return saved;
|
|
623
|
+
};
|
|
552
624
|
if (!configurationSchema)
|
|
553
|
-
return success(created);
|
|
625
|
+
return success(parameters.length ? { ...created, parameters: await addParameters() } : created);
|
|
554
626
|
try {
|
|
555
|
-
|
|
627
|
+
const configured = await functions.updateFunction(agent_id, created._id, { type, description: description ?? '', configurationSchema });
|
|
628
|
+
return success(parameters.length ? { ...configured, parameters: await addParameters() } : configured);
|
|
556
629
|
}
|
|
557
630
|
catch (err) {
|
|
558
631
|
// Leave no half-made skill behind: remove the empty one, then say why nothing was saved.
|
|
@@ -637,10 +710,10 @@ export function registerTools(server, client) {
|
|
|
637
710
|
return error(err);
|
|
638
711
|
}
|
|
639
712
|
});
|
|
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) => {
|
|
713
|
+
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
714
|
try {
|
|
642
715
|
const result = await functions.exportFunctions(args.agent_id);
|
|
643
|
-
return success(result);
|
|
716
|
+
return success(await withDownloadLink(result, result.signedUrl ?? result.fileUrl ?? result.url));
|
|
644
717
|
}
|
|
645
718
|
catch (err) {
|
|
646
719
|
return error(err);
|
|
@@ -723,7 +796,7 @@ export function registerTools(server, client) {
|
|
|
723
796
|
catch (err) {
|
|
724
797
|
return error(err);
|
|
725
798
|
}
|
|
726
|
-
});
|
|
799
|
+
}, liveChange('a skill parameter', ['function_id']));
|
|
727
800
|
reg.tool('update_function_parameter', 'Change a skill parameter.', {
|
|
728
801
|
agent_id: z.string().describe('The agent ID'),
|
|
729
802
|
function_id: z.string().describe('The function ID'),
|
|
@@ -742,7 +815,7 @@ export function registerTools(server, client) {
|
|
|
742
815
|
catch (err) {
|
|
743
816
|
return error(err);
|
|
744
817
|
}
|
|
745
|
-
});
|
|
818
|
+
}, liveChange('a skill parameter', ['function_id', 'parameter_id']));
|
|
746
819
|
reg.tool('delete_function_parameter', 'Remove a skill parameter.', {
|
|
747
820
|
agent_id: z.string().describe('The agent ID'),
|
|
748
821
|
function_id: z.string().describe('The function ID'),
|
|
@@ -837,10 +910,11 @@ export function registerTools(server, client) {
|
|
|
837
910
|
return error(err);
|
|
838
911
|
}
|
|
839
912
|
});
|
|
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).', {
|
|
913
|
+
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
914
|
kb_id: z.string().describe('The knowledge base ID'),
|
|
842
915
|
page: z.number().optional().describe('Page number'),
|
|
843
916
|
limit: z.number().optional().describe('Items per page'),
|
|
917
|
+
full: z.boolean().optional().describe('Include the chunks and statistics of every entry'),
|
|
844
918
|
}, async (args) => {
|
|
845
919
|
try {
|
|
846
920
|
const query = {};
|
|
@@ -849,7 +923,16 @@ export function registerTools(server, client) {
|
|
|
849
923
|
if (args.limit !== undefined)
|
|
850
924
|
query.limit = String(args.limit);
|
|
851
925
|
const result = await kb.listDataSources(args.kb_id, Object.keys(query).length ? query : undefined);
|
|
852
|
-
|
|
926
|
+
if (args.full)
|
|
927
|
+
return success(result);
|
|
928
|
+
// The stored record repeats every chunk twice (chunks, chunks_text): a page of it is tens of kilobytes.
|
|
929
|
+
const entries = (result.dataSources ?? []).map((d) => ({
|
|
930
|
+
_id: d._id, type: d.type, question: d.question ?? d.name, answer: d.answer,
|
|
931
|
+
...(d.source_url ? { source_url: d.source_url } : {}),
|
|
932
|
+
trained: d.trained, ...(d.status ? { status: d.status } : {}),
|
|
933
|
+
...(d.error_message ? { error: d.error_message } : {}),
|
|
934
|
+
}));
|
|
935
|
+
return success({ ...result, dataSources: entries });
|
|
853
936
|
}
|
|
854
937
|
catch (err) {
|
|
855
938
|
return error(err);
|
|
@@ -1003,25 +1086,32 @@ export function registerTools(server, client) {
|
|
|
1003
1086
|
return error(err);
|
|
1004
1087
|
}
|
|
1005
1088
|
});
|
|
1006
|
-
reg.tool('export_kb', 'Export the question/answer pairs of a knowledge base as a CSV or XLSX
|
|
1089
|
+
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
1090
|
kb_id: z.string().describe('The knowledge base ID'),
|
|
1008
|
-
format: z.string().optional().describe('Export format (
|
|
1091
|
+
format: z.string().optional().describe('Export format: csv (default) or xlsx'),
|
|
1009
1092
|
}, async (args) => {
|
|
1010
1093
|
try {
|
|
1011
|
-
const result = await kb.exportKB(args.kb_id, args.format || '
|
|
1012
|
-
return success(result);
|
|
1094
|
+
const result = await kb.exportKB(args.kb_id, args.format || 'csv');
|
|
1095
|
+
return success(await withDownloadLink(result, result.signedUrl ?? result.fileUrl));
|
|
1013
1096
|
}
|
|
1014
1097
|
catch (err) {
|
|
1015
1098
|
return error(err);
|
|
1016
1099
|
}
|
|
1017
1100
|
});
|
|
1018
|
-
reg.tool('import_kb', 'Import
|
|
1101
|
+
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
1102
|
kb_id: z.string().describe('The knowledge base ID'),
|
|
1020
|
-
data:
|
|
1103
|
+
data: kbImportSchema.describe('{ type: "file", sources: [{ name, file_type, url }] } (an object, or JSON text)'),
|
|
1021
1104
|
}, async (args) => {
|
|
1022
1105
|
try {
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1106
|
+
const data = args.data;
|
|
1107
|
+
// The import service tells the file's kind from file_type: fill it from the name or the link.
|
|
1108
|
+
const sources = data.sources.map((s) => ({
|
|
1109
|
+
...s,
|
|
1110
|
+
name: s.name ?? s.url.split('?')[0].split('/').pop() ?? 'file',
|
|
1111
|
+
file_type: (s.file_type ?? (s.name ?? s.url.split('?')[0]).split('.').pop() ?? '').toLowerCase(),
|
|
1112
|
+
}));
|
|
1113
|
+
const result = await kb.importKB(args.kb_id, { ...data, sources });
|
|
1114
|
+
return success({ ...result, imported: sources.map((s) => s.name) });
|
|
1025
1115
|
}
|
|
1026
1116
|
catch (err) {
|
|
1027
1117
|
return error(err);
|
|
@@ -1067,7 +1157,8 @@ export function registerTools(server, client) {
|
|
|
1067
1157
|
return success({
|
|
1068
1158
|
conversation_id: conversation.request_id, test: conversation.isTestMode === true,
|
|
1069
1159
|
url: conversationUrl(client, projectId, conversation.request_id),
|
|
1070
|
-
|
|
1160
|
+
// System lines carry the dashboard's template ("{{userLabel}} has ended the call"): say it in words.
|
|
1161
|
+
messages: messages.map((m) => ({ role: m.role, text: (m.text ?? '').replace(/\{\{\s*userLabel\s*\}\}/g, 'The customer'), at: m.createdAt })),
|
|
1071
1162
|
});
|
|
1072
1163
|
}
|
|
1073
1164
|
catch (err) {
|
|
@@ -1157,9 +1248,16 @@ export function registerTools(server, client) {
|
|
|
1157
1248
|
return error(err);
|
|
1158
1249
|
}
|
|
1159
1250
|
});
|
|
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) => {
|
|
1251
|
+
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
1252
|
try {
|
|
1162
|
-
|
|
1253
|
+
if (args.agent_id)
|
|
1254
|
+
return success(await voice.agentNumbers(args.agent_id));
|
|
1255
|
+
// copilot's org list holds only unattached ("reserve") numbers; the attached ones sit on the agents.
|
|
1256
|
+
const [free, all] = await Promise.all([voice.orgNumbers(), agents.listAgents()]);
|
|
1257
|
+
const attached = all
|
|
1258
|
+
.filter((a) => a.connected_phone)
|
|
1259
|
+
.map((a) => ({ phone_number: a.connected_phone, attached: true, agent_id: a._id, agent_name: a.name }));
|
|
1260
|
+
return success([...attached, ...free.map((n) => ({ phone_number: n.phoneNumber, attached: false, name: n.friendlyName, type: n.type }))]);
|
|
1163
1261
|
}
|
|
1164
1262
|
catch (err) {
|
|
1165
1263
|
return error(err);
|
|
@@ -1204,7 +1302,7 @@ export function registerTools(server, client) {
|
|
|
1204
1302
|
try {
|
|
1205
1303
|
// A purchase needs the person, not the model: confirm: true from a host without approval forms is not enough.
|
|
1206
1304
|
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
|
|
1305
|
+
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
1306
|
return success(await voice.buyNumber(args.agent_id, args.phone_number, { country: args.country, tollFree: args.toll_free }));
|
|
1209
1307
|
}
|
|
1210
1308
|
catch (err) {
|
|
@@ -1365,17 +1463,63 @@ export function registerTools(server, client) {
|
|
|
1365
1463
|
return error(err);
|
|
1366
1464
|
}
|
|
1367
1465
|
});
|
|
1368
|
-
|
|
1466
|
+
const DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
1467
|
+
const clock = (hhmm) => {
|
|
1468
|
+
const [h, m] = hhmm.split(':').map(Number);
|
|
1469
|
+
const hour = ((h + 11) % 12) + 1;
|
|
1470
|
+
return `${hour}${m ? `:${String(m).padStart(2, '0')}` : ''}${h < 12 ? 'am' : 'pm'}`;
|
|
1471
|
+
};
|
|
1472
|
+
/** What agents say about the hours, built from the hours themselves so it never contradicts them. */
|
|
1473
|
+
const hoursMessage = (days) => {
|
|
1474
|
+
const open = days.filter((d) => d.slots.length).sort((a, b) => a.day - b.day);
|
|
1475
|
+
if (!open.length)
|
|
1476
|
+
return 'We are currently closed.';
|
|
1477
|
+
const parts = open.map((d) => `${DAY_NAMES[d.day]} ${d.slots.map((s) => `${clock(s.start)} to ${clock(s.end)}`).join(' and ')}`);
|
|
1478
|
+
return `We're open ${parts.join('; ')}.`;
|
|
1479
|
+
};
|
|
1480
|
+
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.', {
|
|
1481
|
+
enabled: z.boolean().optional().describe('Enforce the hours (agents say the out-of-hours message outside them); unchanged when omitted'),
|
|
1482
|
+
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(),
|
|
1483
|
+
out_of_hours_message: z.string().optional(),
|
|
1484
|
+
}, async (args) => {
|
|
1369
1485
|
try {
|
|
1370
|
-
|
|
1486
|
+
const current = (await orgSettings.get());
|
|
1487
|
+
const enabled = args.enabled ?? Boolean(current.businessHoursConfig?.businessHoursEnabled);
|
|
1488
|
+
const given = (args.hours ?? []);
|
|
1489
|
+
// Replace means replace: a day the person did not mention is closed.
|
|
1490
|
+
const days = args.hours ? DAY_NAMES.map((_, day) => ({ day, slots: given.filter((d) => d.day === day).flatMap((d) => d.slots) })) : undefined;
|
|
1491
|
+
return success(await orgSettings.setBusinessHours({
|
|
1492
|
+
businessHoursEnabled: enabled,
|
|
1493
|
+
...(days ? { businessHours: days, businessHoursUtterance: args.out_of_hours_message ?? hoursMessage(days) } : {}),
|
|
1494
|
+
...(!days && args.out_of_hours_message ? { businessHoursUtterance: args.out_of_hours_message } : {}),
|
|
1495
|
+
}));
|
|
1496
|
+
}
|
|
1497
|
+
catch (err) {
|
|
1498
|
+
return error(err);
|
|
1499
|
+
}
|
|
1500
|
+
}, { preview: async (a) => {
|
|
1501
|
+
const hours = (a.hours ?? []);
|
|
1502
|
+
const what = a.hours ? hoursMessage(DAY_NAMES.map((_, day) => ({ day, slots: hours.filter((d) => d.day === day).flatMap((d) => d.slots) }))) : 'the hours unchanged';
|
|
1503
|
+
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.'}`;
|
|
1504
|
+
} });
|
|
1505
|
+
reg.tool('add_holiday', 'Close the org on a date.', {
|
|
1506
|
+
date: z.string().describe('YYYY-MM-DD'), name: z.string(),
|
|
1507
|
+
message: z.string().optional().describe('What agents say that day (default: closed on account of the holiday)'),
|
|
1508
|
+
type: z.string().optional().describe('public or custom (default custom)'),
|
|
1509
|
+
}, async (args) => {
|
|
1510
|
+
try {
|
|
1511
|
+
return success(await orgSettings.addHolidays([{
|
|
1512
|
+
name: args.name, date: args.date, type: args.type ?? 'custom',
|
|
1513
|
+
holidayFallbackUtterance: args.message ?? `Our offices are currently closed on account of ${args.name}.`,
|
|
1514
|
+
}]));
|
|
1371
1515
|
}
|
|
1372
1516
|
catch (err) {
|
|
1373
1517
|
return error(err);
|
|
1374
1518
|
}
|
|
1375
1519
|
});
|
|
1376
|
-
reg.tool('
|
|
1520
|
+
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
1521
|
try {
|
|
1378
|
-
return success(await orgSettings.
|
|
1522
|
+
return success(await orgSettings.removeHolidays([args.date]));
|
|
1379
1523
|
}
|
|
1380
1524
|
catch (err) {
|
|
1381
1525
|
return error(err);
|
|
@@ -1409,7 +1553,14 @@ export function registerTools(server, client) {
|
|
|
1409
1553
|
}, { preview: async (a) => `This will ${(await orgSettings.planAdmin(a)).changes.join('; ')}.` });
|
|
1410
1554
|
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
1555
|
try {
|
|
1412
|
-
|
|
1556
|
+
const entries = await new AuditSDK(client).list({ since: args.since, action: args.action, resourceType: args.resource_type, user: args.user_id, limit: args.limit });
|
|
1557
|
+
// copilot's log carries the person's id only: name them from the member list (one read).
|
|
1558
|
+
const members = entries.some((e) => e.id_user) ? await orgs.listMembers().catch(() => []) : [];
|
|
1559
|
+
const byId = new Map(members.map((m) => [m.user_id, m]));
|
|
1560
|
+
return success(entries.map((e) => {
|
|
1561
|
+
const m = byId.get(String(e.id_user ?? ''));
|
|
1562
|
+
return m ? { ...e, user_name: m.name, user_email: m.email } : e;
|
|
1563
|
+
}));
|
|
1413
1564
|
}
|
|
1414
1565
|
catch (err) {
|
|
1415
1566
|
return error(err);
|
|
@@ -1450,10 +1601,12 @@ export function registerTools(server, client) {
|
|
|
1450
1601
|
return error(err);
|
|
1451
1602
|
}
|
|
1452
1603
|
});
|
|
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(),
|
|
1604
|
+
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.', {
|
|
1605
|
+
ticket_id: z.string(), title: z.string().optional(), description: z.string().optional(),
|
|
1606
|
+
notes: z.string().optional().describe('Replace the whole notes text'),
|
|
1455
1607
|
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(),
|
|
1608
|
+
assignee_user_id: z.string().optional(), unassign: z.boolean().optional(), resolve: z.boolean().optional(),
|
|
1609
|
+
note: z.string().optional().describe('A note to add: appended to the ticket\'s notes, or the closing note when resolving'),
|
|
1457
1610
|
link_ticket_id: z.string().optional().describe('Link to another ticket'),
|
|
1458
1611
|
}, async (args) => {
|
|
1459
1612
|
try {
|
|
@@ -1462,6 +1615,11 @@ export function registerTools(server, client) {
|
|
|
1462
1615
|
for (const k of ['title', 'description', 'notes', 'status', 'priority'])
|
|
1463
1616
|
if (args[k] !== undefined)
|
|
1464
1617
|
changes[k] = args[k];
|
|
1618
|
+
// A note outside resolve used to be dropped silently: it goes onto the ticket's notes.
|
|
1619
|
+
if (args.note && !args.resolve) {
|
|
1620
|
+
const existing = String((await tickets.get(args.ticket_id)).notes ?? '').trim();
|
|
1621
|
+
changes.notes = args.notes !== undefined ? `${args.notes}\n${args.note}`.trim() : existing ? `${existing}\n${args.note}` : args.note;
|
|
1622
|
+
}
|
|
1465
1623
|
if (Object.keys(changes).length)
|
|
1466
1624
|
out.updated = await tickets.update(args.ticket_id, changes);
|
|
1467
1625
|
if (args.assignee_user_id || args.unassign)
|
|
@@ -1573,7 +1731,7 @@ export function registerTools(server, client) {
|
|
|
1573
1731
|
return error(err);
|
|
1574
1732
|
}
|
|
1575
1733
|
});
|
|
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:
|
|
1734
|
+
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
1735
|
try {
|
|
1578
1736
|
return success(args.id ? await platform.update(args.resource, args.id, args.body) : await platform.create(args.resource, args.body));
|
|
1579
1737
|
}
|
|
@@ -1758,7 +1916,7 @@ export function registerTools(server, client) {
|
|
|
1758
1916
|
});
|
|
1759
1917
|
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
1918
|
run_id: z.string(),
|
|
1761
|
-
min_pass_rate: z.number().min(0).max(1).optional(),
|
|
1919
|
+
min_pass_rate: z.number().min(0).max(1).optional().describe('Pass rate the run must reach (default 0.8)'),
|
|
1762
1920
|
max_p95_ms: z.number().optional(),
|
|
1763
1921
|
error_policy: z.enum(['block', 'warn']).optional(),
|
|
1764
1922
|
fail_on_error: z.boolean().optional(),
|
|
@@ -1767,7 +1925,9 @@ export function registerTools(server, client) {
|
|
|
1767
1925
|
const run = await evals.getRunInOrg(args.run_id);
|
|
1768
1926
|
if (run.status !== 'completed' && run.status !== 'failed')
|
|
1769
1927
|
return success({ finished: false, status: run.status });
|
|
1770
|
-
|
|
1928
|
+
// Without a threshold any finished run "passed" (a 1-of-3 run was reported as a pass): judge against 80%.
|
|
1929
|
+
const minPassRate = args.min_pass_rate ?? DEFAULT_MIN_PASS_RATE;
|
|
1930
|
+
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
1931
|
}
|
|
1772
1932
|
catch (err) {
|
|
1773
1933
|
return error(err);
|
|
@@ -1862,19 +2022,41 @@ export function registerTools(server, client) {
|
|
|
1862
2022
|
return error(err);
|
|
1863
2023
|
}
|
|
1864
2024
|
});
|
|
1865
|
-
|
|
2025
|
+
// get_conversation says "agent"; the evaluations service says "bot". Both are accepted.
|
|
2026
|
+
const transcriptTurn = z.object({ sender: z.enum(['user', 'bot', 'agent', 'system']), text: z.string() });
|
|
2027
|
+
const transcriptForService = (turns) => turns
|
|
2028
|
+
.filter((t) => t.sender !== 'system')
|
|
2029
|
+
.map((t) => ({ sender: (t.sender === 'user' ? 'user' : 'bot'), text: t.text }));
|
|
2030
|
+
const transcriptPreconditions = async (a) => {
|
|
2031
|
+
const agentId = String(a.agent_id);
|
|
2032
|
+
await evals.assertAgentInOrg(agentId);
|
|
2033
|
+
const configs = await evals.listEvaluatorConfigs(agentId);
|
|
2034
|
+
const count = Array.isArray(configs) ? configs.length : (configs.items ?? []).length;
|
|
2035
|
+
if (!count)
|
|
2036
|
+
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).');
|
|
2037
|
+
if (JSON.stringify(a.baseline_transcript) === JSON.stringify(a.replay_transcript)) {
|
|
2038
|
+
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.');
|
|
2039
|
+
}
|
|
2040
|
+
};
|
|
2041
|
+
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
2042
|
agent_id: z.string(),
|
|
1867
|
-
baseline_transcript:
|
|
1868
|
-
replay_transcript:
|
|
2043
|
+
baseline_transcript: jsonArray(transcriptTurn),
|
|
2044
|
+
replay_transcript: jsonArray(transcriptTurn),
|
|
1869
2045
|
}, async (args) => {
|
|
1870
2046
|
try {
|
|
1871
|
-
await
|
|
1872
|
-
return success(await evals.evaluateTranscript(
|
|
2047
|
+
await transcriptPreconditions(args);
|
|
2048
|
+
return success(await evals.evaluateTranscript({
|
|
2049
|
+
agent_id: args.agent_id,
|
|
2050
|
+
baseline_transcript: transcriptForService(args.baseline_transcript),
|
|
2051
|
+
replay_transcript: transcriptForService(args.replay_transcript),
|
|
2052
|
+
}));
|
|
1873
2053
|
}
|
|
1874
2054
|
catch (err) {
|
|
1875
2055
|
return error(err);
|
|
1876
2056
|
}
|
|
1877
|
-
}
|
|
2057
|
+
},
|
|
2058
|
+
// Checked before the approval card: a card for something that cannot run wastes the person's answer.
|
|
2059
|
+
{ preview: async (a) => { await transcriptPreconditions(a); return `This will grade a replayed transcript for agent ${a.agent_id} with paid model calls.`; } });
|
|
1878
2060
|
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
2061
|
try {
|
|
1880
2062
|
await evals.assertAgentInOrg(args.agent_id);
|
|
@@ -2060,13 +2242,15 @@ export function registerTools(server, client) {
|
|
|
2060
2242
|
// ---------------------------------------------------------------------------
|
|
2061
2243
|
// Export tools
|
|
2062
2244
|
// ---------------------------------------------------------------------------
|
|
2245
|
+
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
2246
|
reg.tool('export_eval_markdown', 'Export evaluation results as a structured markdown file with conversations, scores, and criteria', {
|
|
2064
2247
|
run_id: z.string().describe('The evaluation run ID'),
|
|
2065
2248
|
output_path: z.string().optional().describe('Output file path (defaults to current directory)'),
|
|
2249
|
+
upload: uploadArg,
|
|
2066
2250
|
}, async (args) => {
|
|
2067
2251
|
try {
|
|
2068
2252
|
const result = await exportEvalMarkdown(args.run_id, args.output_path, client);
|
|
2069
|
-
return success(result);
|
|
2253
|
+
return success(await deliver(result, args.upload));
|
|
2070
2254
|
}
|
|
2071
2255
|
catch (err) {
|
|
2072
2256
|
return error(err);
|
|
@@ -2075,10 +2259,11 @@ export function registerTools(server, client) {
|
|
|
2075
2259
|
reg.tool('export_eval_report', 'Generate a branded PDF evaluation report with cover page, scores, and per-item details', {
|
|
2076
2260
|
run_id: z.string().describe('The evaluation run ID'),
|
|
2077
2261
|
output_path: z.string().optional().describe('Output file path (defaults to current directory)'),
|
|
2262
|
+
upload: uploadArg,
|
|
2078
2263
|
}, async (args) => {
|
|
2079
2264
|
try {
|
|
2080
2265
|
const result = await exportEvalReport(args.run_id, args.output_path, client);
|
|
2081
|
-
return success(result);
|
|
2266
|
+
return success(await deliver(result, args.upload));
|
|
2082
2267
|
}
|
|
2083
2268
|
catch (err) {
|
|
2084
2269
|
return error(err);
|
|
@@ -2087,10 +2272,11 @@ export function registerTools(server, client) {
|
|
|
2087
2272
|
reg.tool('export_agent_card', 'Generate a branded PDF agent card with system prompt, tools, KBs, LLM config, and evaluation history', {
|
|
2088
2273
|
agent_id: z.string().describe('The agent ID'),
|
|
2089
2274
|
output_path: z.string().optional().describe('Output file path (defaults to current directory)'),
|
|
2275
|
+
upload: uploadArg,
|
|
2090
2276
|
}, async (args) => {
|
|
2091
2277
|
try {
|
|
2092
2278
|
const result = await exportAgentCard(args.agent_id, args.output_path, client);
|
|
2093
|
-
return success(result);
|
|
2279
|
+
return success(await deliver(result, args.upload));
|
|
2094
2280
|
}
|
|
2095
2281
|
catch (err) {
|
|
2096
2282
|
return error(err);
|