easypost-mcp 2.1.0 → 3.1.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/package.json +1 -1
- package/src/audit/AuditLogger.js +20 -0
- package/src/constants/toolRisk.js +36 -0
- package/src/elicitation/fallback.js +37 -0
- package/src/elicitation/fields/catalog.js +171 -0
- package/src/pipeline/ExecutionPipeline.js +226 -0
- package/src/resources/ResourceManager.js +244 -0
- package/src/resources/cache.js +41 -0
- package/src/resources/fuzzy.js +57 -0
- package/src/resources/staticResources.js +89 -0
- package/src/schemas/addressSchemas.js +3 -1
- package/src/schemas/batchSchemas.js +2 -1
- package/src/schemas/commonSchemas.js +5 -1
- package/src/schemas/pickupSchemas.js +4 -1
- package/src/schemas/resourceSchemas.js +16 -0
- package/src/schemas/returnSchemas.js +5 -1
- package/src/schemas/shipmentSchemas.js +8 -6
- package/src/server/createServer.js +25 -1
- package/src/services/AddressService.js +53 -3
- package/src/services/BatchService.js +8 -1
- package/src/services/ConfirmationService.js +45 -0
- package/src/services/ElicitationService.js +230 -0
- package/src/services/IdempotencyStore.js +40 -0
- package/src/services/PickupService.js +47 -20
- package/src/services/ReturnService.js +37 -7
- package/src/services/ShipmentService.js +149 -17
- package/src/services/WorkflowStateStore.js +70 -0
- package/src/services/index.js +20 -5
- package/src/tools/ToolDefinition.js +13 -2
- package/src/tools/definitions/pickups.js +2 -2
- package/src/tools/definitions/resources.js +59 -0
- package/src/tools/definitions/shipments.js +3 -3
- package/src/tools/index.js +2 -0
- package/src/validators/antiHallucination.js +109 -8
- package/src/validators/validate.js +2 -2
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { mapShipment, mapRate, mapCollection } from "../adapters/easypost/responseMappers.js";
|
|
2
2
|
import { NotFoundError } from "../errors/AppError.js";
|
|
3
|
+
import { createFallbackResponse } from "../elicitation/fallback.js";
|
|
4
|
+
|
|
5
|
+
function mapSelectableRates(rates) {
|
|
6
|
+
return rates.map((rate, index) => ({
|
|
7
|
+
option: index + 1,
|
|
8
|
+
...mapRate(rate),
|
|
9
|
+
}));
|
|
10
|
+
}
|
|
3
11
|
|
|
4
12
|
class ShipmentService {
|
|
5
|
-
constructor(easypostClient) {
|
|
13
|
+
constructor(easypostClient, { confirmations, elicitation, idempotency } = {}) {
|
|
6
14
|
this.easypost = easypostClient;
|
|
15
|
+
this.confirmations = confirmations;
|
|
16
|
+
this.elicitation = elicitation;
|
|
17
|
+
this.idempotency = idempotency;
|
|
7
18
|
}
|
|
8
19
|
|
|
9
20
|
async createShipment(input, context) {
|
|
10
|
-
const
|
|
21
|
+
const operation = async () => {
|
|
22
|
+
const shipment = await this.easypost.execute(
|
|
11
23
|
"shipment.create",
|
|
12
24
|
(client) => client.Shipment.create({
|
|
13
25
|
from_address: input.from_address,
|
|
@@ -19,9 +31,30 @@ class ShipmentService {
|
|
|
19
31
|
reference: input.reference,
|
|
20
32
|
}),
|
|
21
33
|
context
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const rawRates = shipment.rates || [];
|
|
37
|
+
const rates = mapSelectableRates(rawRates);
|
|
38
|
+
context.audit?.record("rates_returned", {
|
|
39
|
+
correlationId: context.correlationId,
|
|
40
|
+
shipment_id: shipment.id,
|
|
41
|
+
rate_count: rates.length,
|
|
42
|
+
rate_ids: rates.map((rate) => rate.id),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
ok: true,
|
|
47
|
+
shipment: mapShipment(shipment),
|
|
48
|
+
rates,
|
|
49
|
+
next_step: rates.length
|
|
50
|
+
? "Choose a numbered rate option or exact rate_id from rates and call buy_shipping_label with confirm=true."
|
|
51
|
+
: "No rates were returned by the carrier accounts for this shipment.",
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (!this.idempotency || !input.idempotency_key) return operation();
|
|
56
|
+
const { reused, result } = await this.idempotency.run(`create_shipment:${input.idempotency_key}`, operation);
|
|
57
|
+
return reused ? { ...result, idempotent_replay: true } : result;
|
|
25
58
|
}
|
|
26
59
|
|
|
27
60
|
async buyShippingLabel(input, context) {
|
|
@@ -33,19 +66,104 @@ class ShipmentService {
|
|
|
33
66
|
|
|
34
67
|
if (!shipment) throw new NotFoundError("shipment", input.shipment_id);
|
|
35
68
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
69
|
+
const rates = shipment.rates || [];
|
|
70
|
+
if (!input.rate_id && !input.rate_option) {
|
|
71
|
+
const elicited = await this.elicitation?.selectRateForLabel({
|
|
72
|
+
server: context.server,
|
|
73
|
+
shipmentId: input.shipment_id,
|
|
74
|
+
rates,
|
|
75
|
+
confirmDefault: input.confirm === true,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (elicited?.selected) {
|
|
79
|
+
input.rate_option = elicited.rate_option;
|
|
80
|
+
input.confirm = elicited.confirm;
|
|
81
|
+
context.audit?.record("rate_selection_elicited", {
|
|
82
|
+
correlationId: context.correlationId,
|
|
83
|
+
shipment_id: input.shipment_id,
|
|
84
|
+
rate_option: input.rate_option,
|
|
85
|
+
confirmed: input.confirm,
|
|
86
|
+
});
|
|
87
|
+
} else {
|
|
88
|
+
context.audit?.record("rate_selection_fallback", {
|
|
89
|
+
correlationId: context.correlationId,
|
|
90
|
+
shipment_id: input.shipment_id,
|
|
91
|
+
reason: elicited?.reason,
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
...createFallbackResponse({
|
|
95
|
+
errorCode: "RATE_SELECTION_REQUIRED",
|
|
96
|
+
message: "Select a rate using the client form if available, or provide one numbered rate_option from available_rates.",
|
|
97
|
+
missingFields: ["rate_option"],
|
|
98
|
+
availableOptions: mapSelectableRates(rates),
|
|
99
|
+
examples: { rate_option: [1, 2] },
|
|
100
|
+
nextAction: "Choose one available option and retry with rate_option, or use a client that supports elicitation.form.",
|
|
101
|
+
metadata: {
|
|
102
|
+
elicitation_supported: this.elicitation?.supportsForm(context.server) === true,
|
|
103
|
+
fallback_reason: elicited?.reason,
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
41
109
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
|
|
110
|
+
const rate = input.rate_id
|
|
111
|
+
? rates.find((candidate) => candidate.id === input.rate_id)
|
|
112
|
+
: rates[input.rate_option - 1];
|
|
113
|
+
|
|
114
|
+
if (!rate) {
|
|
115
|
+
context.audit?.record("rate_selection_failed", {
|
|
116
|
+
correlationId: context.correlationId,
|
|
117
|
+
shipment_id: input.shipment_id,
|
|
118
|
+
requested_rate_id: input.rate_id,
|
|
119
|
+
requested_rate_option: input.rate_option,
|
|
120
|
+
available_rate_ids: rates.map((candidate) => candidate.id),
|
|
121
|
+
});
|
|
122
|
+
return {
|
|
123
|
+
...createFallbackResponse({
|
|
124
|
+
errorCode: "RATE_ID_NOT_AVAILABLE",
|
|
125
|
+
message: "The requested rate selection is not available on this shipment. Select one option number or exact id from available_rates.",
|
|
126
|
+
missingFields: ["rate_option"],
|
|
127
|
+
availableOptions: mapSelectableRates(rates),
|
|
128
|
+
examples: { rate_option: [1], rate_id: rates[0]?.id ? [rates[0].id] : [] },
|
|
129
|
+
nextAction: "Retry with a valid rate_option or exact rate_id from available_options.",
|
|
130
|
+
metadata: {
|
|
131
|
+
requested_rate_id: input.rate_id,
|
|
132
|
+
requested_rate_option: input.rate_option,
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await this.confirmations?.requireConfirmed(input, {
|
|
139
|
+
action: "buy_shipping_label",
|
|
140
|
+
message: `You are about to buy a ${rate.carrier} ${rate.service} label for ${rate.rate} ${rate.currency || "USD"}. Proceed?`,
|
|
141
|
+
details: {
|
|
142
|
+
shipment_id: input.shipment_id,
|
|
143
|
+
rate: mapRate(rate),
|
|
144
|
+
},
|
|
145
|
+
}, context);
|
|
146
|
+
|
|
147
|
+
const operation = async () => {
|
|
148
|
+
context.audit?.record("label_purchase_confirmed", {
|
|
149
|
+
correlationId: context.correlationId,
|
|
150
|
+
shipment_id: input.shipment_id,
|
|
151
|
+
rate_id: rate.id,
|
|
152
|
+
rate_option: input.rate_option,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const bought = await this.easypost.execute(
|
|
156
|
+
"shipment.buy",
|
|
157
|
+
(client) => client.Shipment.buy(input.shipment_id, rate, input.insurance),
|
|
158
|
+
context
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
return { ok: true, shipment: mapShipment(bought), purchased_rate: mapRate(rate) };
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
if (!this.idempotency || !input.idempotency_key) return operation();
|
|
165
|
+
const { reused, result } = await this.idempotency.run(`buy_shipping_label:${input.idempotency_key}`, operation);
|
|
166
|
+
return reused ? { ...result, idempotent_replay: true } : result;
|
|
49
167
|
}
|
|
50
168
|
|
|
51
169
|
async getShipment(input, context) {
|
|
@@ -72,6 +190,13 @@ class ShipmentService {
|
|
|
72
190
|
}
|
|
73
191
|
|
|
74
192
|
async refundShipment(input, context) {
|
|
193
|
+
await this.confirmations?.requireConfirmed(input, {
|
|
194
|
+
action: "refund_shipment",
|
|
195
|
+
message: `You are about to void/refund shipment ${input.shipment_id}. Proceed?`,
|
|
196
|
+
details: { shipment_id: input.shipment_id },
|
|
197
|
+
typedConfirmation: "REFUND",
|
|
198
|
+
}, context);
|
|
199
|
+
|
|
75
200
|
const shipment = await this.easypost.execute(
|
|
76
201
|
"shipment.refund",
|
|
77
202
|
(client) => client.Shipment.refund(input.shipment_id),
|
|
@@ -85,6 +210,13 @@ class ShipmentService {
|
|
|
85
210
|
}
|
|
86
211
|
|
|
87
212
|
async insureShipment(input, context) {
|
|
213
|
+
await this.confirmations?.requireConfirmed(input, {
|
|
214
|
+
action: "insure_shipment",
|
|
215
|
+
message: `You are about to add insurance for ${input.amount} to shipment ${input.shipment_id}. Proceed?`,
|
|
216
|
+
details: { shipment_id: input.shipment_id, amount: input.amount },
|
|
217
|
+
typedConfirmation: Number(input.amount) >= 500 ? "INSURE" : undefined,
|
|
218
|
+
}, context);
|
|
219
|
+
|
|
88
220
|
const shipment = await this.easypost.execute(
|
|
89
221
|
"shipment.insure",
|
|
90
222
|
(client) => client.Shipment.insure(input.shipment_id, input.amount),
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
class WorkflowStateStore {
|
|
4
|
+
constructor({ ttlMs = 30 * 60 * 1000 } = {}) {
|
|
5
|
+
this.ttlMs = ttlMs;
|
|
6
|
+
this.entries = new Map();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
_purgeExpired() {
|
|
10
|
+
const now = Date.now();
|
|
11
|
+
for (const [key, entry] of this.entries.entries()) {
|
|
12
|
+
if (entry.expiresAt <= now) this.entries.delete(key);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
create({ toolName, input = {}, reason, correlationId }) {
|
|
17
|
+
this._purgeExpired();
|
|
18
|
+
const workflowId = `wf_${randomUUID()}`;
|
|
19
|
+
this.entries.set(workflowId, {
|
|
20
|
+
toolName,
|
|
21
|
+
input,
|
|
22
|
+
reason,
|
|
23
|
+
correlationId,
|
|
24
|
+
createdAt: new Date().toISOString(),
|
|
25
|
+
expiresAt: Date.now() + this.ttlMs,
|
|
26
|
+
});
|
|
27
|
+
return workflowId;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get(workflowId) {
|
|
31
|
+
if (!workflowId) return null;
|
|
32
|
+
this._purgeExpired();
|
|
33
|
+
return this.entries.get(workflowId) || null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
merge(workflowId, input = {}) {
|
|
37
|
+
const state = this.get(workflowId);
|
|
38
|
+
if (!state) return input;
|
|
39
|
+
return deepMerge(state.input, input);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
update(workflowId, input) {
|
|
43
|
+
const state = this.get(workflowId);
|
|
44
|
+
if (!state) return;
|
|
45
|
+
this.entries.set(workflowId, {
|
|
46
|
+
...state,
|
|
47
|
+
input,
|
|
48
|
+
expiresAt: Date.now() + this.ttlMs,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isPlainObject(value) {
|
|
54
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function deepMerge(base, override) {
|
|
58
|
+
const output = { ...(isPlainObject(base) ? base : {}) };
|
|
59
|
+
for (const [key, value] of Object.entries(override || {})) {
|
|
60
|
+
if (key === "workflow_id") continue;
|
|
61
|
+
if (isPlainObject(value) && isPlainObject(output[key])) {
|
|
62
|
+
output[key] = deepMerge(output[key], value);
|
|
63
|
+
} else {
|
|
64
|
+
output[key] = value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return output;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export { WorkflowStateStore, deepMerge };
|
package/src/services/index.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { EasyPostClient } from "../adapters/easypost/EasyPostClient.js";
|
|
2
|
+
import { ResourceManager } from "../resources/ResourceManager.js";
|
|
3
|
+
import { ConfirmationService } from "./ConfirmationService.js";
|
|
4
|
+
import { ElicitationService } from "./ElicitationService.js";
|
|
5
|
+
import { IdempotencyStore } from "./IdempotencyStore.js";
|
|
6
|
+
import { WorkflowStateStore } from "./WorkflowStateStore.js";
|
|
2
7
|
import { ShipmentService } from "./ShipmentService.js";
|
|
3
8
|
import { AddressService } from "./AddressService.js";
|
|
4
9
|
import { TrackingService } from "./TrackingService.js";
|
|
@@ -9,14 +14,24 @@ import { OrderService } from "./OrderService.js";
|
|
|
9
14
|
|
|
10
15
|
function createServices() {
|
|
11
16
|
const easypostClient = new EasyPostClient();
|
|
17
|
+
const resources = new ResourceManager(easypostClient);
|
|
18
|
+
const confirmations = new ConfirmationService();
|
|
19
|
+
const elicitation = new ElicitationService();
|
|
20
|
+
const idempotency = new IdempotencyStore();
|
|
21
|
+
const workflowState = new WorkflowStateStore();
|
|
12
22
|
|
|
13
23
|
return {
|
|
14
|
-
|
|
15
|
-
|
|
24
|
+
resources,
|
|
25
|
+
confirmations,
|
|
26
|
+
elicitation,
|
|
27
|
+
idempotency,
|
|
28
|
+
workflowState,
|
|
29
|
+
shipments: new ShipmentService(easypostClient, { confirmations, elicitation, idempotency }),
|
|
30
|
+
addresses: new AddressService(easypostClient, { confirmations }),
|
|
16
31
|
tracking: new TrackingService(easypostClient),
|
|
17
|
-
returns: new ReturnService(easypostClient),
|
|
18
|
-
pickups: new PickupService(easypostClient),
|
|
19
|
-
batches: new BatchService(easypostClient),
|
|
32
|
+
returns: new ReturnService(easypostClient, { confirmations, elicitation }),
|
|
33
|
+
pickups: new PickupService(easypostClient, { confirmations, idempotency }),
|
|
34
|
+
batches: new BatchService(easypostClient, { confirmations }),
|
|
20
35
|
orders: new OrderService(easypostClient),
|
|
21
36
|
};
|
|
22
37
|
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { toInputSchema, validateInput } from "../validators/validate.js";
|
|
2
|
+
import { riskForTool } from "../constants/toolRisk.js";
|
|
2
3
|
|
|
3
4
|
class ToolDefinition {
|
|
4
|
-
constructor({ name, title, description, category, schema, handler, annotations = {} }) {
|
|
5
|
+
constructor({ name, title, description, category, schema, handler, risk, annotations = {} }) {
|
|
5
6
|
this.name = name;
|
|
6
7
|
this.title = title;
|
|
7
8
|
this.description = description;
|
|
8
9
|
this.category = category;
|
|
9
10
|
this.schema = schema;
|
|
10
11
|
this.handler = handler;
|
|
12
|
+
this.risk = risk || riskForTool(name);
|
|
11
13
|
this.annotations = annotations;
|
|
12
14
|
}
|
|
13
15
|
|
|
@@ -19,13 +21,22 @@ class ToolDefinition {
|
|
|
19
21
|
inputSchema: toInputSchema(this.schema, this.name),
|
|
20
22
|
annotations: {
|
|
21
23
|
category: this.category,
|
|
24
|
+
risk: this.risk,
|
|
22
25
|
...this.annotations,
|
|
23
26
|
},
|
|
24
27
|
};
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
async execute(args, context) {
|
|
28
|
-
|
|
31
|
+
if (context?.pipeline) {
|
|
32
|
+
return context.pipeline.run(this, args, context);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const input = validateInput(this.schema, args, {
|
|
36
|
+
resourceManager: context?.resources,
|
|
37
|
+
toolName: this.name,
|
|
38
|
+
risk: this.risk,
|
|
39
|
+
});
|
|
29
40
|
return this.handler(input, context);
|
|
30
41
|
}
|
|
31
42
|
}
|
|
@@ -8,7 +8,7 @@ function pickupTools(services) {
|
|
|
8
8
|
name: "schedule_pickup",
|
|
9
9
|
title: "Schedule Pickup",
|
|
10
10
|
category: categories.PICKUPS,
|
|
11
|
-
description: "
|
|
11
|
+
description: "Create a carrier pickup request for one or more shipments. Requires confirm=true and never auto-buys a pickup rate.",
|
|
12
12
|
schema: schedulePickupSchema,
|
|
13
13
|
handler: (input, context) => services.pickups.schedulePickup(input, context),
|
|
14
14
|
}),
|
|
@@ -16,7 +16,7 @@ function pickupTools(services) {
|
|
|
16
16
|
name: "cancel_pickup",
|
|
17
17
|
title: "Cancel Pickup",
|
|
18
18
|
category: categories.PICKUPS,
|
|
19
|
-
description: "Cancel a scheduled carrier pickup.",
|
|
19
|
+
description: "Cancel a scheduled carrier pickup. Requires confirm=true.",
|
|
20
20
|
schema: cancelPickupSchema,
|
|
21
21
|
handler: (input, context) => services.pickups.cancelPickup(input, context),
|
|
22
22
|
}),
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ToolDefinition } from "../ToolDefinition.js";
|
|
2
|
+
import categories from "../../constants/toolCategories.js";
|
|
3
|
+
import { TOOL_RISK } from "../../constants/toolRisk.js";
|
|
4
|
+
import { listResourcesSchema, validateCarrierSchema, validateServiceSchema } from "../../schemas/resourceSchemas.js";
|
|
5
|
+
|
|
6
|
+
function resourceTools(services) {
|
|
7
|
+
return [
|
|
8
|
+
new ToolDefinition({
|
|
9
|
+
name: "get_carriers",
|
|
10
|
+
title: "Get Carriers",
|
|
11
|
+
category: categories.SHIPMENTS,
|
|
12
|
+
risk: TOOL_RISK.LOW,
|
|
13
|
+
description: "Return compact authoritative carrier metadata from the server-side resource cache.",
|
|
14
|
+
schema: listResourcesSchema,
|
|
15
|
+
handler: async (input, context) => {
|
|
16
|
+
if (input.refresh) await services.resources.refresh(context);
|
|
17
|
+
return {
|
|
18
|
+
ok: true,
|
|
19
|
+
carriers: services.resources.getCarriers(),
|
|
20
|
+
resource_context: services.resources.getOperationalContext(),
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
}),
|
|
24
|
+
new ToolDefinition({
|
|
25
|
+
name: "validate_carrier",
|
|
26
|
+
title: "Validate Carrier",
|
|
27
|
+
category: categories.SHIPMENTS,
|
|
28
|
+
risk: TOOL_RISK.LOW,
|
|
29
|
+
description: "Validate a carrier with exact matching. Fuzzy matches are returned only as suggestions.",
|
|
30
|
+
schema: validateCarrierSchema,
|
|
31
|
+
handler: async (input) => {
|
|
32
|
+
const result = services.resources.validateCarrier(input.carrier);
|
|
33
|
+
return {
|
|
34
|
+
ok: result.valid,
|
|
35
|
+
validation: result,
|
|
36
|
+
clarification_required: !result.valid && result.suggestions.length > 0,
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
new ToolDefinition({
|
|
41
|
+
name: "validate_service",
|
|
42
|
+
title: "Validate Service",
|
|
43
|
+
category: categories.SHIPMENTS,
|
|
44
|
+
risk: TOOL_RISK.LOW,
|
|
45
|
+
description: "Validate an exact carrier/service combination against cached authoritative resources.",
|
|
46
|
+
schema: validateServiceSchema,
|
|
47
|
+
handler: async (input) => {
|
|
48
|
+
const result = services.resources.validateService(input.carrier, input.service);
|
|
49
|
+
return {
|
|
50
|
+
ok: result.valid,
|
|
51
|
+
validation: result,
|
|
52
|
+
clarification_required: !result.valid && result.suggestions.length > 0,
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { resourceTools };
|
|
@@ -16,7 +16,7 @@ function shipmentTools(services) {
|
|
|
16
16
|
name: "buy_shipping_label",
|
|
17
17
|
title: "Buy Shipping Label",
|
|
18
18
|
category: categories.SHIPMENTS,
|
|
19
|
-
description: "Buy a shipping label for an existing shipment using
|
|
19
|
+
description: "Buy a shipping label for an existing shipment using client elicitation, exact returned rate_id, or numbered rate_option. Requires confirmation and idempotency_key.",
|
|
20
20
|
schema: schemas.buyShippingLabelSchema,
|
|
21
21
|
handler: (input, context) => services.shipments.buyShippingLabel(input, context),
|
|
22
22
|
}),
|
|
@@ -40,7 +40,7 @@ function shipmentTools(services) {
|
|
|
40
40
|
name: "cancel_shipment",
|
|
41
41
|
title: "Cancel Shipment",
|
|
42
42
|
category: categories.SHIPMENTS,
|
|
43
|
-
description: "Cancel or void a shipment when supported by the carrier.
|
|
43
|
+
description: "Cancel or void a shipment when supported by the carrier. Requires confirm=true and uses EasyPost refund semantics for purchased labels.",
|
|
44
44
|
schema: schemas.cancelShipmentSchema,
|
|
45
45
|
handler: (input, context) => services.shipments.cancelShipment(input, context),
|
|
46
46
|
}),
|
|
@@ -48,7 +48,7 @@ function shipmentTools(services) {
|
|
|
48
48
|
name: "refund_shipment",
|
|
49
49
|
title: "Refund Shipment",
|
|
50
50
|
category: categories.SHIPMENTS,
|
|
51
|
-
description: "Request a refund for a purchased shipment label.",
|
|
51
|
+
description: "Request a refund for a purchased shipment label. Requires confirm=true.",
|
|
52
52
|
schema: schemas.refundShipmentSchema,
|
|
53
53
|
handler: (input, context) => services.shipments.refundShipment(input, context),
|
|
54
54
|
}),
|
package/src/tools/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { returnTools } from "./definitions/returns.js";
|
|
|
6
6
|
import { pickupTools } from "./definitions/pickups.js";
|
|
7
7
|
import { batchTools } from "./definitions/batches.js";
|
|
8
8
|
import { orderTools } from "./definitions/orders.js";
|
|
9
|
+
import { resourceTools } from "./definitions/resources.js";
|
|
9
10
|
|
|
10
11
|
function createToolRegistry(services) {
|
|
11
12
|
const registry = new ToolRegistry();
|
|
@@ -17,6 +18,7 @@ function createToolRegistry(services) {
|
|
|
17
18
|
...pickupTools(services),
|
|
18
19
|
...batchTools(services),
|
|
19
20
|
...orderTools(services),
|
|
21
|
+
...resourceTools(services),
|
|
20
22
|
]);
|
|
21
23
|
return registry;
|
|
22
24
|
}
|
|
@@ -4,17 +4,31 @@ const PLACEHOLDER_PATTERNS = [
|
|
|
4
4
|
/\b(unknown|n\/a|na|null|none|tbd|todo|test|sample|example|placeholder)\b/i,
|
|
5
5
|
/\b(123 main|main street|fake street|any street|your address|shipping address)\b/i,
|
|
6
6
|
/\b(john doe|jane doe|acme|foo|bar)\b/i,
|
|
7
|
-
/^(.)\1{4,}
|
|
7
|
+
/^(.)\1{4,}$/i,
|
|
8
|
+
/^(0{6,}|1{6,}|9{6,}|123456\d*|987654\d*)$/i,
|
|
9
|
+
/\b(lorem ipsum|asdf|qwerty|abcdef)\b/i,
|
|
8
10
|
];
|
|
9
11
|
|
|
12
|
+
function issue(path, code, message, confidence = 0.2, suggestions = []) {
|
|
13
|
+
return { path, code, message, confidence, suggestions };
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
function inspectString(path, value, issues) {
|
|
11
17
|
if (!value.trim()) {
|
|
12
|
-
issues.push(
|
|
18
|
+
issues.push(issue(path, "EMPTY_STRING", "Empty string", 0));
|
|
13
19
|
return;
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
if (PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(value))) {
|
|
17
|
-
issues.push(
|
|
23
|
+
issues.push(issue(path, "SUSPICIOUS_PLACEHOLDER", "Looks like autogenerated placeholder data", 0.1));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (/(.)\1{5,}/i.test(value.replace(/\s+/g, ""))) {
|
|
27
|
+
issues.push(issue(path, "REPEATED_CHARACTER_GARBAGE", "Contains repeated-character garbage", 0.1));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (value.length > 8 && /^[^a-z0-9]+$/i.test(value)) {
|
|
31
|
+
issues.push(issue(path, "NON_SEMANTIC_STRING", "String contains no semantic content", 0.1));
|
|
18
32
|
}
|
|
19
33
|
}
|
|
20
34
|
|
|
@@ -34,16 +48,103 @@ function inspectObject(value, path, issues) {
|
|
|
34
48
|
}
|
|
35
49
|
}
|
|
36
50
|
|
|
37
|
-
function
|
|
51
|
+
function inspectResources(input, resourceManager, issues) {
|
|
52
|
+
if (!resourceManager || !input || typeof input !== "object") return;
|
|
53
|
+
|
|
54
|
+
inspectResourceFields(input, "", resourceManager, issues);
|
|
55
|
+
|
|
56
|
+
if (Array.isArray(input.carrier_accounts)) {
|
|
57
|
+
for (const [index, carrierAccount] of input.carrier_accounts.entries()) {
|
|
58
|
+
const result = resourceManager.validateCarrierAccount(carrierAccount);
|
|
59
|
+
if (!result.valid) {
|
|
60
|
+
issues.push(issue(`carrier_accounts[${index}]`, "UNKNOWN_CARRIER_ACCOUNT", `Unknown carrier account: ${carrierAccount}`, 0, result.suggestions));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function inspectResourceFields(value, path, resourceManager, issues) {
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
value.forEach((item, index) => inspectResourceFields(item, `${path}[${index}]`, resourceManager, issues));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!value || typeof value !== "object") return;
|
|
74
|
+
|
|
75
|
+
if (value.country) {
|
|
76
|
+
const result = resourceManager.validateCountry(value.country);
|
|
77
|
+
if (!result.valid) {
|
|
78
|
+
issues.push(issue(path ? `${path}.country` : "country", "UNKNOWN_COUNTRY", `Unknown country: ${value.country}`, 0, result.suggestions));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (value.country && value.state) {
|
|
83
|
+
const result = resourceManager.validateState(value.country, value.state);
|
|
84
|
+
if (!result.valid) {
|
|
85
|
+
issues.push(issue(path ? `${path}.state` : "state", "UNKNOWN_STATE", `Unknown state/province for ${value.country}: ${value.state}`, 0, result.suggestions));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (value.payment_method) {
|
|
90
|
+
const result = resourceManager.validatePaymentMethod(value.payment_method);
|
|
91
|
+
if (!result.valid) {
|
|
92
|
+
issues.push(issue(path ? `${path}.payment_method` : "payment_method", "UNKNOWN_PAYMENT_METHOD", `Unknown payment method: ${value.payment_method}`, 0, result.suggestions));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (value.predefined_package) {
|
|
97
|
+
const result = resourceManager.validatePackageType(value.predefined_package);
|
|
98
|
+
if (!result.valid) {
|
|
99
|
+
issues.push(issue(path ? `${path}.predefined_package` : "predefined_package", "UNKNOWN_PACKAGE_TYPE", `Unknown package type: ${value.predefined_package}`, 0, result.suggestions));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (value.carrier) {
|
|
104
|
+
const result = resourceManager.validateCarrier(value.carrier);
|
|
105
|
+
if (!result.valid) {
|
|
106
|
+
issues.push(issue(path ? `${path}.carrier` : "carrier", "UNKNOWN_CARRIER", `Unsupported carrier: ${value.carrier}`, 0, result.suggestions));
|
|
107
|
+
} else if (value.service) {
|
|
108
|
+
const serviceResult = resourceManager.validateService(value.carrier, value.service);
|
|
109
|
+
if (!serviceResult.valid) {
|
|
110
|
+
issues.push(issue(path ? `${path}.service` : "service", "UNSUPPORTED_SERVICE_FOR_CARRIER", serviceResult.issues?.[0]?.message || "Unsupported carrier/service combination", 0, serviceResult.suggestions));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
116
|
+
inspectResourceFields(nested, path ? `${path}.${key}` : key, resourceManager, issues);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function validateAntiHallucination(input, options = {}) {
|
|
38
121
|
const issues = [];
|
|
39
122
|
inspectObject(input, "", issues);
|
|
123
|
+
if (!["validate_carrier", "validate_service"].includes(options.toolName)) {
|
|
124
|
+
inspectResources(input, options.resourceManager, issues);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const confidence = issues.length
|
|
128
|
+
? Math.min(...issues.map((item) => item.confidence ?? 0.2))
|
|
129
|
+
: 1;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
valid: issues.length === 0,
|
|
133
|
+
confidence,
|
|
134
|
+
issues,
|
|
135
|
+
suggestions: issues.flatMap((item) => item.suggestions || []),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function assertNotHallucinated(input, options = {}) {
|
|
140
|
+
const result = validateAntiHallucination(input, options);
|
|
40
141
|
|
|
41
|
-
if (
|
|
142
|
+
if (!result.valid) {
|
|
42
143
|
throw new AntiHallucinationError(
|
|
43
|
-
"Input appears
|
|
44
|
-
|
|
144
|
+
"Input appears unsupported, ambiguous, or AI-generated. Provide exact values from verified resources.",
|
|
145
|
+
result
|
|
45
146
|
);
|
|
46
147
|
}
|
|
47
148
|
}
|
|
48
149
|
|
|
49
|
-
export { assertNotHallucinated };
|
|
150
|
+
export { assertNotHallucinated, validateAntiHallucination };
|
|
@@ -3,7 +3,7 @@ import { ValidationError } from "../errors/AppError.js";
|
|
|
3
3
|
import { sanitizeInput } from "../utils/sanitize.js";
|
|
4
4
|
import { assertNotHallucinated } from "./antiHallucination.js";
|
|
5
5
|
|
|
6
|
-
function validateInput(schema, input) {
|
|
6
|
+
function validateInput(schema, input, options = {}) {
|
|
7
7
|
const sanitized = sanitizeInput(input || {});
|
|
8
8
|
const result = schema.safeParse(sanitized);
|
|
9
9
|
|
|
@@ -11,7 +11,7 @@ function validateInput(schema, input) {
|
|
|
11
11
|
throw new ValidationError("Tool input validation failed", result.error.issues);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
assertNotHallucinated(result.data);
|
|
14
|
+
assertNotHallucinated(result.data, options);
|
|
15
15
|
return result.data;
|
|
16
16
|
}
|
|
17
17
|
|