@agifyai/leadify-mcp 9.14.0 → 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 +30 -8
- package/dist/correlation.js +1 -1
- package/dist/tools/leads.js +2 -2
- 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
|
@@ -211,9 +211,9 @@ Les corrections ajoutent un fait immuable et mettent à jour le snapshot
|
|
|
211
211
|
courant dans la même opération ; elles exigent un motif. Il n’existe aucun tool
|
|
212
212
|
de suppression de participation ou de preuve. Archiver un événement le retire
|
|
213
213
|
des sélecteurs courants sans effacer son historique. Les champs legacy
|
|
214
|
-
`congress_presence`, `next_congress
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
`congress_presence`, `next_congress` et `next_congress_date` restent disponibles
|
|
215
|
+
pour compatibilité/export, mais ne sont jamais synchronisés depuis ce modèle.
|
|
216
|
+
L'ancienne carte `card_congress` est supprimée (PRD-2831) et refusée à l'écriture.
|
|
217
217
|
|
|
218
218
|
Événements d’une personne :
|
|
219
219
|
|
|
@@ -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
|
}
|
package/dist/tools/leads.js
CHANGED
|
@@ -279,7 +279,7 @@ export function registerLeadTools(server, client = getClient()) {
|
|
|
279
279
|
"An unknown field is refused with the name of the closest existing field: declare a new field first with `update_schema`. " +
|
|
280
280
|
"`expect` (optional) protects against overwriting a newer value: pass, for each field you depend on, the value you read " +
|
|
281
281
|
"({ property_name, value }). The write is refused with LEAD_WRITE_PRECONDITION_FAILED, and the value actually in place is " +
|
|
282
|
-
"returned, if any of them changed since your read.
|
|
282
|
+
"returned, if any of them changed since your read. To protect the first write of an empty field, expect null: it matches a field that is null or absent. Without `expect`, the write is unconditional. " +
|
|
283
283
|
"`reason` (optional) is one sentence saying why you change the lead; it is recorded in the lead history. " +
|
|
284
284
|
"For bulk updates across many leads, call this tool once per lead. A write during a platform agent run on this lead (external agent only) is refused with 409 QUALIFICATION_RUN_IN_PROGRESS (runId in the answer); nothing is written: wait for the run to end, read the lead again, then retry.", {
|
|
285
285
|
lead_id: z.string().describe("ID of the lead to update."),
|
|
@@ -290,7 +290,7 @@ export function registerLeadTools(server, client = getClient()) {
|
|
|
290
290
|
expect: z
|
|
291
291
|
.array(z.object({
|
|
292
292
|
property_name: z.string().trim().min(1).describe("Field you read."),
|
|
293
|
-
value: z.unknown().describe("Exact value you read for that field. Required key
|
|
293
|
+
value: z.unknown().describe("Exact value you read for that field. Required key; null means the field was empty (null or absent)."),
|
|
294
294
|
}).strict())
|
|
295
295
|
.optional()
|
|
296
296
|
.describe("Optional preconditions: the values you read earlier. The write is refused (LEAD_WRITE_PRECONDITION_FAILED, with the current values) if any field no longer holds its expected value. Deep equality, checked in the same transaction as the write."),
|
|
@@ -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
|
}
|