@agifyai/leadify-mcp 9.14.1 → 9.15.0
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 +27 -5
- package/dist/correlation.js +1 -1
- package/dist/tools/outreach_review.d.ts +4 -4
- package/dist/tools/outreach_review.js +48 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -277,6 +277,7 @@ Leadify et ne doit jamais être envoyée par un agent MCP.
|
|
|
277
277
|
| `get_follow_up_cadence` | Rythme de relance de l'organisation (lecture seule). |
|
|
278
278
|
| `get_prospect_approach` | Avant d'écrire à une personne : son Approche (quotidien, problématique, ce qu'elle veut, ce qu'on lui apporte). |
|
|
279
279
|
| `list_outreach_review_queue` | La file « À valider » de Leadify (page `/to-review`) en un appel, en lecture seule : filtres campagne, canal, type de message et état (`TO_REVIEW` par défaut), `total`, `counts`, `truncated`. Le texte des messages seulement avec `include_text`. |
|
|
280
|
+
| `revise_review_queue_message` | Remplacer le texte d'**un** message en `TO_REVIEW`, à la demande explicite du relecteur, au nom de l'agent nommé par `LEADIFY_EXTERNAL_AGENT_NAME` : relecture complète en réponse, version attendue, clé d'idempotence, limites du canal. Le message reste à valider : rien ne valide, ne programme, n'envoie ni ne refuse. Organisation d'Agify seulement. |
|
|
280
281
|
| `preview_move_leads` | Essai à blanc du déplacement de fiches vers un autre groupe de la même organisation : sélection par identifiants, vue ou filtre (500 fiches au plus par appel, lots ensuite), refus par fiche avec motif, doublons dans le groupe cible, champs de schéma et relations à ajouter, blocages et empreinte du plan. |
|
|
281
282
|
| `move_leads` | Appliquer un déplacement prévisualisé ; exige l'empreinte de la prévisualisation et une clé d'idempotence. La fiche garde son identifiant et tout son historique ; jamais de fusion. |
|
|
282
283
|
| `undo_move_leads` / `get_lead_move` | Annuler ou relire une opération de déplacement par son identifiant. |
|
|
@@ -502,17 +503,38 @@ de son extrait de 500 caractères.
|
|
|
502
503
|
les messages de campagne qui attendent une décision humaine, dans l'ordre de la
|
|
503
504
|
page, au plus 500, sans pagination. Filtres : `campaign_id`, `channel` (`EMAIL`,
|
|
504
505
|
`LINKEDIN`), `message_type_id` et `state` (`TO_REVIEW` par défaut, `VALIDATED`,
|
|
505
|
-
`CORRECTED`, `REFUSED`, ou `ALL`). Les identifiants des filtres viennent de
|
|
506
|
+
`CORRECTED`, `REWRITTEN`, `REFUSED`, ou `ALL`). Les identifiants des filtres viennent de
|
|
506
507
|
`filters.campaigns` et `filters.messageTypes`. Chaque message garde l'essentiel
|
|
507
508
|
(prospect, entreprise, campagne, canal, étape, type, départ prévu, créneau
|
|
508
|
-
manqué, statut, objet)
|
|
509
|
-
|
|
509
|
+
manqué, statut, objet), sa `version`, et l'auteur de sa dernière modification
|
|
510
|
+
(`lastEditedBy` : `none`, `human` ou `agent`, avec `lastEditedByName`) ; la date de
|
|
511
|
+
création et les identifiants de campagne et de type, déjà dans `filters`, sont
|
|
512
|
+
retirés. `decisions` sépare les messages validés : `validatedAsIs`,
|
|
513
|
+
`validatedAfterAgentRewrite`, `correctedByHuman`. Sans le texte des
|
|
510
514
|
messages, la file réelle de 73 messages tient en environ 30 000 caractères, sous
|
|
511
515
|
la limite de 50 000 d'Hermès ; `include_text: true` ajoute `body`, le message
|
|
512
516
|
entier (environ 500 caractères de plus par message) : au-delà d'une quarantaine
|
|
513
517
|
de messages, filtrer d'abord. Si `truncated` vaut `true`, filtrer par campagne.
|
|
514
|
-
|
|
515
|
-
|
|
518
|
+
Route : `GET /api/outreach-review/queue` (permission `read`).
|
|
519
|
+
|
|
520
|
+
`revise_review_queue_message` remplace le texte d'un seul message en `TO_REVIEW`,
|
|
521
|
+
à la demande explicite du relecteur : le corps, et l'objet d'un email. Le message
|
|
522
|
+
reste à valider ; l'état, l'heure de départ, la campagne, le destinataire et le
|
|
523
|
+
canal ne changent pas. Entrées : `organization_id`, `message_id`, `body`,
|
|
524
|
+
`subject` (facultatif), `expected_version`, `idempotency_key`. La réponse est une
|
|
525
|
+
relecture complète : texte enregistré, `version`, auteur `agent`, état et
|
|
526
|
+
`history` (les textes précédents). Chaque écriture crée la version suivante sous
|
|
527
|
+
un **nouvel identifiant** : utiliser `messageId` et `version` de la réponse pour la
|
|
528
|
+
suite. Rejouer la même clé rend le même résultat (`replayed: true`). Refus sans
|
|
529
|
+
écriture : `MESSAGE_NOT_TO_REVIEW`, `MESSAGE_VERSION_STALE`,
|
|
530
|
+
`IDEMPOTENCY_KEY_REUSED`, `EDIT_INVALID` (note d'invitation LinkedIn : 300 caractères
|
|
531
|
+
au plus ; message LinkedIn et email : 1 500 ; objet : 90 ; pas d'objet sur
|
|
532
|
+
LinkedIn), `AGENT_REVISION_ORGANIZATION_FORBIDDEN` (organisation d'Agify seulement).
|
|
533
|
+
Exige `LEADIFY_EXTERNAL_AGENT_NAME`, et une clé avec la permission `write`. Route :
|
|
534
|
+
`POST /api/outreach-review/messages/:id/revise`.
|
|
535
|
+
|
|
536
|
+
Aucun outil ne valide, ne programme, n'envoie ni ne refuse : cela reste humain, dans
|
|
537
|
+
Leadify.
|
|
516
538
|
|
|
517
539
|
### Déplacer des prospects vers un autre groupe (PRD-2478)
|
|
518
540
|
|
package/dist/correlation.js
CHANGED
|
@@ -9,7 +9,7 @@ export const EXTERNAL_MISSION_VERSION = "external-agent.v1";
|
|
|
9
9
|
const PROCESS_SESSION = randomUUID().replace(/-/g, "").slice(0, 16);
|
|
10
10
|
export class ExternalAgentNameMissingError extends Error {
|
|
11
11
|
constructor() {
|
|
12
|
-
super(`${EXTERNAL_AGENT_ENV} n'est pas défini : une écriture de qualification doit être faite au nom d'un agent identifié. ` +
|
|
12
|
+
super(`${EXTERNAL_AGENT_ENV} n'est pas défini : une écriture de qualification ou une réécriture de message doit être faite au nom d'un agent identifié. ` +
|
|
13
13
|
`Lancez le MCP avec ${EXTERNAL_AGENT_ENV}=<Nom> (par exemple Louise). Les lectures restent disponibles sans ce nom.`);
|
|
14
14
|
this.name = "ExternalAgentNameMissingError";
|
|
15
15
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { type LeadifyClient } from "../client.js";
|
|
3
|
-
export declare const OUTREACH_REVIEW_TOOL_NAMES: readonly ["list_outreach_review_queue"];
|
|
4
|
-
export declare const OUTREACH_REVIEW_STATES: readonly ["ALL", "TO_REVIEW", "VALIDATED", "CORRECTED", "REFUSED"];
|
|
3
|
+
export declare const OUTREACH_REVIEW_TOOL_NAMES: readonly ["list_outreach_review_queue", "revise_review_queue_message"];
|
|
4
|
+
export declare const OUTREACH_REVIEW_STATES: readonly ["ALL", "TO_REVIEW", "VALIDATED", "CORRECTED", "REWRITTEN", "REFUSED"];
|
|
5
5
|
export declare const OUTREACH_REVIEW_CHANNELS: readonly ["EMAIL", "LINKEDIN"];
|
|
6
|
-
type Client = Pick<LeadifyClient, "get">;
|
|
6
|
+
type Client = Pick<LeadifyClient, "get" | "post">;
|
|
7
7
|
/**
|
|
8
|
-
* One queue item
|
|
8
|
+
* One queue item, with version (the revision a write must name), without createdAt and
|
|
9
9
|
* without the campaign and message type ids, which filters.campaigns and
|
|
10
10
|
* filters.messageTypes carry once. The body only with include_text.
|
|
11
11
|
*/
|
|
@@ -1,29 +1,32 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getClient } from "../client.js";
|
|
3
|
+
import { ExternalAgentNameMissingError, externalAgentName } from "../correlation.js";
|
|
3
4
|
import { handleToolError } from "../types.js";
|
|
4
|
-
// PRD-2815 — la file « À valider » de Leadify (page /to-review)
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
export const
|
|
5
|
+
// PRD-2815 — la file « À valider » de Leadify (page /to-review), par la route serveur
|
|
6
|
+
// GET /api/outreach-review/queue. Une seule écriture : un agent nommé remplace le texte
|
|
7
|
+
// d'un message à valider, par POST /api/outreach-review/messages/:id/revise. Valider,
|
|
8
|
+
// programmer, envoyer ou refuser reste humain : aucun outil ici ne le fait.
|
|
9
|
+
export const OUTREACH_REVIEW_TOOL_NAMES = ["list_outreach_review_queue", "revise_review_queue_message"];
|
|
10
|
+
export const OUTREACH_REVIEW_STATES = ["ALL", "TO_REVIEW", "VALIDATED", "CORRECTED", "REWRITTEN", "REFUSED"];
|
|
9
11
|
export const OUTREACH_REVIEW_CHANNELS = ["EMAIL", "LINKEDIN"];
|
|
10
12
|
const id = z.string().trim().min(1).max(200);
|
|
11
13
|
const READ_ONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true };
|
|
14
|
+
const REVISE = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true };
|
|
12
15
|
const object = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
13
16
|
/** One line of JSON: an agent runtime sets aside tool results above 50,000 characters. */
|
|
14
17
|
const compact = (data) => ({ content: [{ type: "text", text: JSON.stringify(data) }] });
|
|
15
18
|
/**
|
|
16
|
-
* One queue item
|
|
19
|
+
* One queue item, with version (the revision a write must name), without createdAt and
|
|
17
20
|
* without the campaign and message type ids, which filters.campaigns and
|
|
18
21
|
* filters.messageTypes carry once. The body only with include_text.
|
|
19
22
|
*/
|
|
20
23
|
export function projectReviewItem(item, includeText) {
|
|
21
24
|
const i = object(item);
|
|
22
25
|
const result = {
|
|
23
|
-
id: i.id, leadId: i.leadId, leadName: i.leadName, companyName: i.companyName, campaignName: i.campaignName,
|
|
26
|
+
id: i.id, version: i.revision, leadId: i.leadId, leadName: i.leadName, companyName: i.companyName, campaignName: i.campaignName,
|
|
24
27
|
channel: i.channel, step: i.step, stepKind: i.stepKind, messageTypeName: object(i.messageType).name ?? null,
|
|
25
28
|
plannedDepartureAt: i.plannedDepartureAt, missedSlot: i.missedSlot, status: i.status, editedByHuman: i.editedByHuman,
|
|
26
|
-
subject: i.subject,
|
|
29
|
+
lastEditedBy: i.lastEditedBy, lastEditedByName: i.lastEditedByName, subject: i.subject,
|
|
27
30
|
};
|
|
28
31
|
if (includeText)
|
|
29
32
|
result.body = i.body;
|
|
@@ -32,16 +35,19 @@ export function projectReviewItem(item, includeText) {
|
|
|
32
35
|
export function registerOutreachReviewTools(server, client = getClient()) {
|
|
33
36
|
server.tool("list_outreach_review_queue", "The Leadify 'À valider' queue (page /to-review) in ONE call: the campaign messages waiting for a human decision, in " +
|
|
34
37
|
"the page's order. Read-only: validating, correcting or refusing a message stays human, in Leadify. Answer: " +
|
|
35
|
-
"{organizationId, state, limit, total, counts {all, toReview, validated, corrected, refused},
|
|
36
|
-
"
|
|
38
|
+
"{organizationId, state, limit, total, counts {all, toReview, validated, corrected, rewritten, refused}, " +
|
|
39
|
+
"decisions {validatedAsIs, validatedAfterAgentRewrite, correctedByHuman}, truncated, items, " +
|
|
40
|
+
"filters {campaigns [{id, name}], messageTypes [{id, name}]}, notContactedToday {count, reasons}}. decisions splits the " +
|
|
41
|
+
"validated messages by who last wrote their text. Each item: id, version (what revise_review_queue_message expects), " +
|
|
37
42
|
"leadId, leadName, companyName, campaignName, channel, step, stepKind, messageTypeName, plannedDepartureAt, " +
|
|
38
|
-
"missedSlot, status (TO_REVIEW, APPROVED, SCHEDULED, SENT, REJECTED), editedByHuman,
|
|
43
|
+
"missedSlot, status (TO_REVIEW, APPROVED, SCHEDULED, SENT, REJECTED), editedByHuman, lastEditedBy (none, human or agent), " +
|
|
44
|
+
"lastEditedByName (the agent's name), subject. To stay well under " +
|
|
39
45
|
"50,000 characters, the message text is left out by default (about 30,000 characters for 73 messages); " +
|
|
40
46
|
"include_text=true adds body, the whole message, about 500 characters more per message (about 68,000 for those 73): " +
|
|
41
47
|
"above about 40 messages, narrow with campaign_id, channel or message_type_id first. At most 500 messages, no pagination: when truncated " +
|
|
42
48
|
"is true, filter by campaign_id. Pass the ids found in filters.campaigns and filters.messageTypes.", {
|
|
43
49
|
organization_id: id.describe("Organization to read. Must be in the API key's scope."),
|
|
44
|
-
state: z.enum(OUTREACH_REVIEW_STATES).optional().describe("TO_REVIEW (default): waiting for validation. VALIDATED, CORRECTED, REFUSED: decided in the last 7 days. ALL: TO_REVIEW plus the messages validated, scheduled, sent or refused in the last 7 days."),
|
|
50
|
+
state: z.enum(OUTREACH_REVIEW_STATES).optional().describe("TO_REVIEW (default): waiting for validation. VALIDATED, CORRECTED, REFUSED: decided in the last 7 days. CORRECTED: last written by a human. REWRITTEN: last written by an agent. ALL: TO_REVIEW plus the messages validated, scheduled, sent or refused in the last 7 days."),
|
|
45
51
|
campaign_id: id.optional().describe("Only this campaign (filters.campaigns[].id)."),
|
|
46
52
|
channel: z.enum(OUTREACH_REVIEW_CHANNELS).optional().describe("EMAIL or LINKEDIN."),
|
|
47
53
|
message_type_id: id.optional().describe("Only this message type (filters.messageTypes[].id)."),
|
|
@@ -59,4 +65,34 @@ export function registerOutreachReviewTools(server, client = getClient()) {
|
|
|
59
65
|
return handleToolError(error);
|
|
60
66
|
}
|
|
61
67
|
}));
|
|
68
|
+
server.tool("revise_review_queue_message", "Replace the text of ONE campaign message waiting in the Leadify 'À valider' queue, at the reviewer's explicit request. " +
|
|
69
|
+
"Changes only the body, and the subject of an email. The message stays TO_REVIEW: state, planned departure, campaign, " +
|
|
70
|
+
"recipient and channel never change, and the reviewer still validates it by hand in Leadify. Nothing here validates, " +
|
|
71
|
+
"schedules, sends or refuses. Each write creates the next version under a NEW id: use the returned messageId and version " +
|
|
72
|
+
"for anything after it. The previous text stays in history. Answer, read back after writing: {organizationId, messageId, " +
|
|
73
|
+
"previousMessageId, version, status, campaignId, leadId, channel, step, subject, body, lastEditedBy, lastEditedByName, " +
|
|
74
|
+
"unchanged, replayed, latestMessageId, updatedAt, history [{messageId, version, status, subject, body, lastEditedBy, " +
|
|
75
|
+
"lastEditedByName, createdAt}]}. Replaying the same idempotency_key with the same call returns the same message " +
|
|
76
|
+
"(replayed: true). Refusals, nothing written: MESSAGE_NOT_TO_REVIEW (APPROVED, SCHEDULED, SENT, REJECTED...), " +
|
|
77
|
+
"MESSAGE_VERSION_STALE (reread the queue, latestMessageId gives the current one), IDEMPOTENCY_KEY_REUSED, EDIT_INVALID " +
|
|
78
|
+
"(empty, too long: LinkedIn invitation note 300 characters, LinkedIn message 1500, email 1500 and subject 90; or a subject " +
|
|
79
|
+
"on LinkedIn), AGENT_REVISION_ORGANIZATION_FORBIDDEN. Requires LEADIFY_EXTERNAL_AGENT_NAME: the author is noted as that agent.", {
|
|
80
|
+
organization_id: id.describe("Organization of the message. Must be in the API key's scope."),
|
|
81
|
+
message_id: id.describe("id of the message in list_outreach_review_queue."),
|
|
82
|
+
body: z.string().min(1).max(16000).describe("The whole new message text, not a diff."),
|
|
83
|
+
subject: z.string().min(1).max(600).optional().describe("New subject, email only. Omit it to keep the current subject."),
|
|
84
|
+
expected_version: z.number().int().min(1).describe("version of the message as read in list_outreach_review_queue."),
|
|
85
|
+
idempotency_key: z.string().trim().min(8).max(200).describe("A new unique key per new text; reuse it only to replay the same call after an uncertain result."),
|
|
86
|
+
}, REVISE, (async ({ organization_id, message_id, body, subject, expected_version, idempotency_key }) => {
|
|
87
|
+
try {
|
|
88
|
+
if (!externalAgentName())
|
|
89
|
+
throw new ExternalAgentNameMissingError();
|
|
90
|
+
return compact(await client.post(`/api/outreach-review/messages/${encodeURIComponent(message_id)}/revise`, {
|
|
91
|
+
organizationId: organization_id, body, ...(subject === undefined ? {} : { subject }), expectedVersion: expected_version, idempotencyKey: idempotency_key,
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return handleToolError(error);
|
|
96
|
+
}
|
|
97
|
+
}));
|
|
62
98
|
}
|