@meuecommerce/frete-adapter-node 0.1.0 → 0.2.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/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/openAIDimensionEstimator.d.ts +24 -0
- package/dist/openAIDimensionEstimator.js +127 -0
- package/package.json +6 -3
package/dist/index.d.ts
CHANGED
|
@@ -10,3 +10,5 @@ export { InMemorySettingsStore } from "./inMemorySettingsStore.js";
|
|
|
10
10
|
export type { InMemorySettingsStoreOptions } from "./inMemorySettingsStore.js";
|
|
11
11
|
export { EnvSecretStore } from "./envSecretStore.js";
|
|
12
12
|
export type { EnvSecretStoreOptions } from "./envSecretStore.js";
|
|
13
|
+
export { createOpenAIDimensionEstimator } from "./openAIDimensionEstimator.js";
|
|
14
|
+
export type { OpenAIDimensionEstimatorOptions } from "./openAIDimensionEstimator.js";
|
package/dist/index.js
CHANGED
|
@@ -7,3 +7,4 @@
|
|
|
7
7
|
export { createCorreiosHttpClient } from "./correiosHttpClient.js";
|
|
8
8
|
export { InMemorySettingsStore } from "./inMemorySettingsStore.js";
|
|
9
9
|
export { EnvSecretStore } from "./envSecretStore.js";
|
|
10
|
+
export { createOpenAIDimensionEstimator } from "./openAIDimensionEstimator.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ProductDimensionEstimator` implemented with the OpenAI chat-completions API.
|
|
3
|
+
*
|
|
4
|
+
* Ports the dimension-estimation path from the Velo `openAI/` modules:
|
|
5
|
+
* - `ai-tools.js` -> the `estimate_dimensions` tool + request/response handling
|
|
6
|
+
* - `prompt-methods.js`-> `createDimensionEstimationPrompt`
|
|
7
|
+
* - `box-methods.js` -> `validateAndOptimizeDimensions` (post-process AI output)
|
|
8
|
+
*
|
|
9
|
+
* `fetch` is injectable (defaults to global fetch) so this also runs under Velo
|
|
10
|
+
* with `wix-fetch` passed in. The API key is passed in — the caller resolves it
|
|
11
|
+
* (e.g. via a `SecretStore`), keeping secret handling out of this module.
|
|
12
|
+
*/
|
|
13
|
+
import type { ProductDimensionEstimator } from "@meuecommerce/frete";
|
|
14
|
+
type FetchLike = typeof globalThis.fetch;
|
|
15
|
+
export interface OpenAIDimensionEstimatorOptions {
|
|
16
|
+
apiKey: string;
|
|
17
|
+
fetch?: FetchLike;
|
|
18
|
+
model?: string;
|
|
19
|
+
temperature?: number;
|
|
20
|
+
apiUrl?: string;
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
}
|
|
23
|
+
export declare function createOpenAIDimensionEstimator(options: OpenAIDimensionEstimatorOptions): ProductDimensionEstimator;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const ESTIMATE_DIMENSIONS_TOOL = {
|
|
2
|
+
type: "function",
|
|
3
|
+
function: {
|
|
4
|
+
name: "estimate_dimensions",
|
|
5
|
+
description: "Estima dimensões de produtos PRESERVANDO os nomes exatos fornecidos",
|
|
6
|
+
parameters: {
|
|
7
|
+
type: "object",
|
|
8
|
+
properties: {
|
|
9
|
+
products: {
|
|
10
|
+
type: "array",
|
|
11
|
+
items: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
name: { type: "string" },
|
|
15
|
+
length: { type: "number", description: "Comprimento em cm" },
|
|
16
|
+
width: { type: "number", description: "Largura em cm" },
|
|
17
|
+
height: { type: "number", description: "Altura em cm" },
|
|
18
|
+
},
|
|
19
|
+
required: ["name", "length", "width", "height"],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
required: ["products"],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
function largestBox(boxes) {
|
|
28
|
+
return boxes.reduce((largest, current) => current.width * current.height * current.length > largest.width * largest.height * largest.length ? current : largest);
|
|
29
|
+
}
|
|
30
|
+
function createPrompt(products, boxes, samples) {
|
|
31
|
+
const box = largestBox(boxes);
|
|
32
|
+
const maxWidth = Math.floor(box.width * 0.8);
|
|
33
|
+
const maxHeight = Math.floor(box.height * 0.8);
|
|
34
|
+
const maxLength = Math.floor(box.length * 0.8);
|
|
35
|
+
const samplesSection = samples.length > 0
|
|
36
|
+
? `\n🌟 EXEMPLOS DO USUÁRIO (PRIORIDADE MÁXIMA):\n${samples.map((s, i) => `${i + 1}. ${s}`).join("\n")}\n\n` +
|
|
37
|
+
`📋 INSTRUÇÕES:\n1. Para cada produto, procure um exemplo similar acima\n2. Match exato → use dimensões EXATAS\n3. Similar → ajuste\n4. Sem match → conhecimento geral\n`
|
|
38
|
+
: `\n📋 INSTRUÇÕES (SEM EXEMPLOS):\n1. Considere a embalagem comercial típica brasileira\n2. Use conhecimento sobre produtos similares\n3. Ajuste baseado no peso\n`;
|
|
39
|
+
return (`Você é um especialista em dimensões de produtos brasileiros para e-commerce.\n\n` +
|
|
40
|
+
`🚨 LIMITES FÍSICOS OBRIGATÓRIOS:\n- Largura máxima: ${maxWidth}cm\n- Altura máxima: ${maxHeight}cm\n- Comprimento máximo: ${maxLength}cm\n(Maior caixa: ${box.name ?? "—"})\n\n` +
|
|
41
|
+
`📦 PRODUTOS PARA ESTIMAR:\n${products.map((p, i) => `${i + 1}. ${p.name} - ${p.quantity}x (${p.weight}kg cada)`).join("\n")}\n` +
|
|
42
|
+
samplesSection +
|
|
43
|
+
`\n⚠️ REGRAS: respeite os limites; preserve os nomes EXATOS; retorne length×width×height em cm.\n` +
|
|
44
|
+
`Responda usando a função 'estimate_dimensions'.`);
|
|
45
|
+
}
|
|
46
|
+
/** Apply the largest-box physical limits + a minimum-volume floor to AI output. */
|
|
47
|
+
function validateAndOptimize(aiResult, products, boxes) {
|
|
48
|
+
const box = largestBox(boxes);
|
|
49
|
+
const maxWidth = Math.floor(box.width * 0.8);
|
|
50
|
+
const maxHeight = Math.floor(box.height * 0.8);
|
|
51
|
+
const maxLength = Math.floor(box.length * 0.8);
|
|
52
|
+
const optimized = aiResult.products.map((product) => {
|
|
53
|
+
let { length, width, height } = product;
|
|
54
|
+
if (length > maxLength)
|
|
55
|
+
length = maxLength;
|
|
56
|
+
if (width > maxWidth)
|
|
57
|
+
width = maxWidth;
|
|
58
|
+
if (height > maxHeight)
|
|
59
|
+
height = maxHeight;
|
|
60
|
+
const volume = length * width * height;
|
|
61
|
+
const original = products.find((p) => p.name === product.name);
|
|
62
|
+
const minVolume = (original?.weight || 0.1) * 500;
|
|
63
|
+
if (volume > 0 && volume < minVolume) {
|
|
64
|
+
const scale = Math.cbrt(minVolume / volume);
|
|
65
|
+
length = Math.min(maxLength, length * scale);
|
|
66
|
+
width = Math.min(maxWidth, width * scale);
|
|
67
|
+
height = Math.min(maxHeight, height * scale);
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
name: product.name,
|
|
71
|
+
length: Math.round(length * 10) / 10,
|
|
72
|
+
width: Math.round(width * 10) / 10,
|
|
73
|
+
height: Math.round(height * 10) / 10,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
return { products: optimized };
|
|
77
|
+
}
|
|
78
|
+
export function createOpenAIDimensionEstimator(options) {
|
|
79
|
+
const { apiKey, fetch = globalThis.fetch, model = "gpt-4o-mini", temperature = 0.1, apiUrl = "https://api.openai.com/v1/chat/completions", timeoutMs = 20000, } = options;
|
|
80
|
+
if (!apiKey)
|
|
81
|
+
throw new Error("createOpenAIDimensionEstimator: apiKey is required");
|
|
82
|
+
if (typeof fetch !== "function")
|
|
83
|
+
throw new Error("createOpenAIDimensionEstimator: no fetch available");
|
|
84
|
+
return {
|
|
85
|
+
async estimate(products, boxes, productSamples = []) {
|
|
86
|
+
if (!boxes.length)
|
|
87
|
+
throw new Error("estimate: no boxes provided");
|
|
88
|
+
const body = {
|
|
89
|
+
model,
|
|
90
|
+
messages: [
|
|
91
|
+
{
|
|
92
|
+
role: "system",
|
|
93
|
+
content: "Você é um especialista em dimensões de produtos. Use os exemplos personalizados como prioridade máxima quando o nome bater.",
|
|
94
|
+
},
|
|
95
|
+
{ role: "user", content: createPrompt(products, boxes, productSamples) },
|
|
96
|
+
],
|
|
97
|
+
tools: [ESTIMATE_DIMENSIONS_TOOL],
|
|
98
|
+
tool_choice: { type: "function", function: { name: "estimate_dimensions" } },
|
|
99
|
+
temperature,
|
|
100
|
+
};
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
103
|
+
let response;
|
|
104
|
+
try {
|
|
105
|
+
response = await fetch(apiUrl, {
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
108
|
+
body: JSON.stringify(body),
|
|
109
|
+
signal: controller.signal,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
}
|
|
115
|
+
if (!response.ok)
|
|
116
|
+
throw new Error(`OpenAI API Error: ${response.status} ${response.statusText}`);
|
|
117
|
+
const json = (await response.json());
|
|
118
|
+
if (json.error)
|
|
119
|
+
throw new Error(`OpenAI Error: ${json.error.message}`);
|
|
120
|
+
const args = json.choices?.[0]?.message?.tool_calls?.[0]?.function?.arguments;
|
|
121
|
+
if (!args)
|
|
122
|
+
throw new Error("IA não conseguiu estimar dimensões dos produtos");
|
|
123
|
+
const parsed = JSON.parse(args);
|
|
124
|
+
return validateAndOptimize(parsed, products, boxes);
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meuecommerce/frete-adapter-node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Node implementation of @meuecommerce/frete ports (Correios HTTP client) using global fetch. Used by the Shopify/Fly deployment and the MCP server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -11,12 +11,15 @@
|
|
|
11
11
|
"import": "./dist/index.js"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
|
-
"files": [
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
15
17
|
"engines": {
|
|
16
18
|
"node": ">=18.18"
|
|
17
19
|
},
|
|
18
20
|
"scripts": {
|
|
19
21
|
"build": "tsc -p tsconfig.json",
|
|
22
|
+
"prepublishOnly": "npm run build",
|
|
20
23
|
"typecheck": "tsc --noEmit",
|
|
21
24
|
"test": "vitest run",
|
|
22
25
|
"test:watch": "vitest"
|
|
@@ -26,6 +29,6 @@
|
|
|
26
29
|
"access": "public"
|
|
27
30
|
},
|
|
28
31
|
"dependencies": {
|
|
29
|
-
"@meuecommerce/frete": "^0.
|
|
32
|
+
"@meuecommerce/frete": "^0.2.0"
|
|
30
33
|
}
|
|
31
34
|
}
|