@mostajs/agora-sourcing 0.0.1
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 +54 -0
- package/dist/cache.d.ts +30 -0
- package/dist/cache.js +52 -0
- package/dist/config.d.ts +32 -0
- package/dist/config.js +54 -0
- package/dist/discover-llm.d.ts +15 -0
- package/dist/discover-llm.js +23 -0
- package/dist/discover.d.ts +6 -0
- package/dist/discover.js +51 -0
- package/dist/http.d.ts +25 -0
- package/dist/http.js +101 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +121 -0
- package/dist/landed.d.ts +28 -0
- package/dist/landed.js +32 -0
- package/dist/optimize.d.ts +65 -0
- package/dist/optimize.js +332 -0
- package/dist/registry.d.ts +25 -0
- package/dist/registry.js +47 -0
- package/dist/relevance.d.ts +13 -0
- package/dist/relevance.js +25 -0
- package/dist/rfq.d.ts +37 -0
- package/dist/rfq.js +28 -0
- package/dist/scrapers/aliexpress.d.ts +31 -0
- package/dist/scrapers/aliexpress.js +123 -0
- package/dist/scrapers/generic.d.ts +8 -0
- package/dist/scrapers/generic.js +112 -0
- package/dist/scrapers/site-profile.d.ts +24 -0
- package/dist/scrapers/site-profile.js +68 -0
- package/dist/tool.d.ts +54 -0
- package/dist/tool.js +53 -0
- package/dist/types.d.ts +121 -0
- package/dist/types.js +5 -0
- package/llms.txt +37 -0
- package/package.json +64 -0
package/dist/landed.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mostajs/agora-sourcing — coût RENDU (landed cost) + normalisation FX.
|
|
3
|
+
* Convertit chaque offre vers une devise cible et intègre livraison + droits → un **prix unitaire
|
|
4
|
+
* comparable**. On normalise PUIS on optimise → l'optimisation porte sur le coût réel, pas le prix nu.
|
|
5
|
+
* Prérequis du multi-objectif (NSGA-II, ro-pla ⏳).
|
|
6
|
+
* @author Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
7
|
+
*/
|
|
8
|
+
import type { Offer } from "./types.js";
|
|
9
|
+
/** Taux de change : `rates[devise]` = valeur de 1 unité de cette devise dans la devise CIBLE. */
|
|
10
|
+
export type FxRates = Record<string, number>;
|
|
11
|
+
export interface LandedOptions {
|
|
12
|
+
/** Devise cible (ex. "EUR"). */
|
|
13
|
+
target: string;
|
|
14
|
+
/** Table FX (cible → 1). Devise absente ⇒ 1 (supposée déjà cible). */
|
|
15
|
+
rates?: FxRates;
|
|
16
|
+
/** Taux de droits/douane appliqué à la marchandise (ex. 0.05 = 5 %). */
|
|
17
|
+
dutyRate?: number;
|
|
18
|
+
}
|
|
19
|
+
/** Convertit un montant de `from` vers la cible via la table FX. */
|
|
20
|
+
export declare function convert(amount: number, from: string | undefined, opts: LandedOptions): number;
|
|
21
|
+
/** Prix unitaire RENDU dans la devise cible : (prix·(1+droits) + livraison/u), converti. */
|
|
22
|
+
export declare function landedUnit(unitPrice: number, offer: Offer, opts: LandedOptions): number;
|
|
23
|
+
/**
|
|
24
|
+
* Normalise des offres vers la devise cible en **coût rendu** : `unitPrice` (et chaque palier)
|
|
25
|
+
* deviennent le landed cost en `target`. L'original est conservé dans `raw.nominal`.
|
|
26
|
+
* On peut ensuite passer ces offres à `optimizeOffers` → optimisation sur le coût réel.
|
|
27
|
+
*/
|
|
28
|
+
export declare function normalizeOffers(offers: Offer[], opts: LandedOptions): Offer[];
|
package/dist/landed.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Convertit un montant de `from` vers la cible via la table FX. */
|
|
2
|
+
export function convert(amount, from, opts) {
|
|
3
|
+
const f = (from || opts.target).toUpperCase();
|
|
4
|
+
if (f === opts.target.toUpperCase())
|
|
5
|
+
return amount;
|
|
6
|
+
const rate = opts.rates?.[f];
|
|
7
|
+
return rate != null ? amount * rate : amount; // taux inconnu → pas de conversion (prudence)
|
|
8
|
+
}
|
|
9
|
+
/** Prix unitaire RENDU dans la devise cible : (prix·(1+droits) + livraison/u), converti. */
|
|
10
|
+
export function landedUnit(unitPrice, offer, opts) {
|
|
11
|
+
const goods = convert(unitPrice, offer.currency, opts) * (1 + (opts.dutyRate ?? 0));
|
|
12
|
+
const ship = convert(offer.shippingCost ?? 0, offer.currency, opts);
|
|
13
|
+
return goods + ship;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Normalise des offres vers la devise cible en **coût rendu** : `unitPrice` (et chaque palier)
|
|
17
|
+
* deviennent le landed cost en `target`. L'original est conservé dans `raw.nominal`.
|
|
18
|
+
* On peut ensuite passer ces offres à `optimizeOffers` → optimisation sur le coût réel.
|
|
19
|
+
*/
|
|
20
|
+
export function normalizeOffers(offers, opts) {
|
|
21
|
+
return offers.map((o) => {
|
|
22
|
+
const tiers = o.priceTiers?.map((t) => ({ minQty: t.minQty, unitPrice: landedUnit(t.unitPrice, o, opts) }));
|
|
23
|
+
return {
|
|
24
|
+
...o,
|
|
25
|
+
unitPrice: landedUnit(o.unitPrice, o, opts),
|
|
26
|
+
currency: opts.target.toUpperCase().slice(0, 3),
|
|
27
|
+
priceTiers: tiers,
|
|
28
|
+
shippingCost: 0, // déjà intégré au prix rendu
|
|
29
|
+
raw: { ...o.raw, nominal: { unitPrice: o.unitPrice, currency: o.currency, shippingCost: o.shippingCost } },
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mostajs/agora-sourcing — OPTIMISATION d'achat (le différenciateur vs LLM).
|
|
3
|
+
* Transforme des offres scrappées en problème résolu EXACTEMENT par @mostajs/ro-pla
|
|
4
|
+
* (solveur injecté, DI) : achat fractionné (min-cost-flow), sélection sous contraintes (MILP).
|
|
5
|
+
* @author Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
6
|
+
*/
|
|
7
|
+
import type { Offer, Allocation, OptimizeConstraints, SolverPort } from "./types.js";
|
|
8
|
+
/** Top-N par prix croissant (référence — ce que fait un LLM « au mieux »). */
|
|
9
|
+
export declare function topNByPrice(offers: Offer[], n?: number): Offer[];
|
|
10
|
+
/**
|
|
11
|
+
* Achat FRACTIONNÉ multi-fournisseurs au coût total minimal sous capacité (stock).
|
|
12
|
+
* Modèle flot : source → fournisseur (cap=stock, cost=prix u.) → puits (cap=stock, cost=0).
|
|
13
|
+
* `arcFlows[2i]` = quantité achetée au fournisseur i. Sans capacité suffisante → flot maximal (partial).
|
|
14
|
+
*/
|
|
15
|
+
export declare function optimizeSplitBuy(offers: Offer[], qty: number, solver: SolverPort, c?: OptimizeConstraints): Promise<Allocation>;
|
|
16
|
+
/**
|
|
17
|
+
* Sélection SOUS CONTRAINTES (budget, nb max de fournisseurs, MOQ par fournisseur) via MILP.
|
|
18
|
+
* Variables : q_i (entiers, qté par fournisseur) puis y_i (binaires, fournisseur retenu).
|
|
19
|
+
* Objectif : min Σ prix_i·q_i. Contraintes : Σq=qty ; q_i≤cap·y_i ; q_i≥moq·y_i ; Σy≤max ; Σprix·q≤budget ; y_i≤1.
|
|
20
|
+
*/
|
|
21
|
+
export declare function optimizeConstrained(offers: Offer[], qty: number, c: OptimizeConstraints, solver: SolverPort): Promise<Allocation>;
|
|
22
|
+
/**
|
|
23
|
+
* Optimisation avec PALIERS de prix dégressifs (B2B « all-units » : Alibaba/1688). Pour chaque
|
|
24
|
+
* (offre, palier) éligible (minQty ≤ qté) : variable entière `q` + binaire `z`. Un fournisseur
|
|
25
|
+
* n'opère qu'UN palier (Σz ≤ 1) ; `q ∈ [minQty, borne_sup]` du palier actif ; Σq = qté ;
|
|
26
|
+
* objectif min Σ prix_palier·q. Résolu en **MILP** par `@mostajs/ro-pla` (branch-and-bound).
|
|
27
|
+
*/
|
|
28
|
+
export declare function optimizeTiered(offers: Offer[], qty: number, solver: SolverPort, c?: OptimizeConstraints): Promise<Allocation>;
|
|
29
|
+
export interface AssignLine {
|
|
30
|
+
key: string;
|
|
31
|
+
qty: number;
|
|
32
|
+
offers: Offer[];
|
|
33
|
+
}
|
|
34
|
+
export interface LineAssignment {
|
|
35
|
+
line: string;
|
|
36
|
+
supplier: string;
|
|
37
|
+
unitPrice: number;
|
|
38
|
+
qty: number;
|
|
39
|
+
cost: number;
|
|
40
|
+
url: string;
|
|
41
|
+
}
|
|
42
|
+
/** Wrapper direct du dialecte `assignment` (hongrois) : matrice de coûts → affectation optimale. */
|
|
43
|
+
export declare function optimizeAssignment(cost: number[][], solver: SolverPort): Promise<{
|
|
44
|
+
assignment: number[];
|
|
45
|
+
total: number;
|
|
46
|
+
}>;
|
|
47
|
+
/**
|
|
48
|
+
* Affecte N **lignes** de commande à des fournisseurs **distincts** (diversification / 1 fournisseur
|
|
49
|
+
* par produit), au coût total minimal — via le **hongrois** (ro-pla). Colonnes = fournisseurs candidats.
|
|
50
|
+
*/
|
|
51
|
+
export declare function assignLinesToSuppliers(lines: AssignLine[], solver: SolverPort): Promise<{
|
|
52
|
+
assignment: LineAssignment[];
|
|
53
|
+
totalCost: number;
|
|
54
|
+
status: "optimal" | "infeasible";
|
|
55
|
+
}>;
|
|
56
|
+
/**
|
|
57
|
+
* Allocation **continue** (biens divisibles : vrac/poids/volume) au coût min, avec contraintes
|
|
58
|
+
* linéaires — notamment une **part maximale par fournisseur** que le flot n'exprime pas simplement.
|
|
59
|
+
* Résolu en **LP (simplexe)** par ro-pla.
|
|
60
|
+
*/
|
|
61
|
+
export declare function optimizeContinuous(offers: Offer[], qty: number, solver: SolverPort, c?: OptimizeConstraints & {
|
|
62
|
+
maxSharePerSupplier?: number;
|
|
63
|
+
}): Promise<Allocation>;
|
|
64
|
+
/** Optimise : paliers (B2B) → MILP par paliers ; sinon contraintes → MILP ; sinon achat fractionné (flot). */
|
|
65
|
+
export declare function optimizeOffers(offers: Offer[], qty: number, solver: SolverPort, c?: OptimizeConstraints): Promise<Allocation>;
|
package/dist/optimize.js
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/** Top-N par prix croissant (référence — ce que fait un LLM « au mieux »). */
|
|
2
|
+
export function topNByPrice(offers, n = 5) {
|
|
3
|
+
return offers.filter((o) => Number.isFinite(o.unitPrice)).sort((a, b) => a.unitPrice - b.unitPrice).slice(0, n);
|
|
4
|
+
}
|
|
5
|
+
function eligible(offers, c) {
|
|
6
|
+
let xs = offers.filter((o) => Number.isFinite(o.unitPrice) && o.unitPrice >= 0);
|
|
7
|
+
if (c?.allowedCountries?.length) {
|
|
8
|
+
const set = new Set(c.allowedCountries.map((s) => s.toUpperCase()));
|
|
9
|
+
xs = xs.filter((o) => !o.country || set.has(o.country.toUpperCase()));
|
|
10
|
+
}
|
|
11
|
+
return xs;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Achat FRACTIONNÉ multi-fournisseurs au coût total minimal sous capacité (stock).
|
|
15
|
+
* Modèle flot : source → fournisseur (cap=stock, cost=prix u.) → puits (cap=stock, cost=0).
|
|
16
|
+
* `arcFlows[2i]` = quantité achetée au fournisseur i. Sans capacité suffisante → flot maximal (partial).
|
|
17
|
+
*/
|
|
18
|
+
export async function optimizeSplitBuy(offers, qty, solver, c) {
|
|
19
|
+
const items = eligible(offers, c);
|
|
20
|
+
if (!items.length || qty <= 0)
|
|
21
|
+
return { bySupplier: [], totalCost: 0, filled: 0, status: "infeasible" };
|
|
22
|
+
const n = items.length;
|
|
23
|
+
const source = 0;
|
|
24
|
+
const sink = n + 1;
|
|
25
|
+
const cap = (o) => (Number.isFinite(o.stock) && o.stock > 0 ? o.stock : qty);
|
|
26
|
+
const arcs = [];
|
|
27
|
+
for (let i = 0; i < n; i++) {
|
|
28
|
+
arcs.push({ from: source, to: i + 1, capacity: cap(items[i]), cost: items[i].unitPrice });
|
|
29
|
+
arcs.push({ from: i + 1, to: sink, capacity: cap(items[i]), cost: 0 });
|
|
30
|
+
}
|
|
31
|
+
const totalCap = items.reduce((s, o) => s + cap(o), 0);
|
|
32
|
+
const problem = { kind: "flow", arcs, source, sink, nodes: n + 2, ...(totalCap >= qty ? { value: qty } : {}) };
|
|
33
|
+
const sol = (await solver.solve(problem));
|
|
34
|
+
if (!sol || sol.status === "infeasible")
|
|
35
|
+
return { bySupplier: [], totalCost: 0, filled: 0, status: "infeasible" };
|
|
36
|
+
const bySupplier = [];
|
|
37
|
+
for (let i = 0; i < n; i++) {
|
|
38
|
+
const q = sol.arcFlows[2 * i] ?? 0;
|
|
39
|
+
if (q > 0) {
|
|
40
|
+
const o = items[i];
|
|
41
|
+
bySupplier.push({ supplier: o.supplier, qty: q, unitPrice: o.unitPrice, currency: o.currency, cost: q * o.unitPrice, url: o.url });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const filled = bySupplier.reduce((s, b) => s + b.qty, 0);
|
|
45
|
+
return { bySupplier, totalCost: sol.cost, filled, status: filled >= qty ? "optimal" : "partial" };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Sélection SOUS CONTRAINTES (budget, nb max de fournisseurs, MOQ par fournisseur) via MILP.
|
|
49
|
+
* Variables : q_i (entiers, qté par fournisseur) puis y_i (binaires, fournisseur retenu).
|
|
50
|
+
* Objectif : min Σ prix_i·q_i. Contraintes : Σq=qty ; q_i≤cap·y_i ; q_i≥moq·y_i ; Σy≤max ; Σprix·q≤budget ; y_i≤1.
|
|
51
|
+
*/
|
|
52
|
+
export async function optimizeConstrained(offers, qty, c, solver) {
|
|
53
|
+
const items = eligible(offers, c);
|
|
54
|
+
if (!items.length || qty <= 0)
|
|
55
|
+
return { bySupplier: [], totalCost: 0, filled: 0, status: "infeasible" };
|
|
56
|
+
const n = items.length;
|
|
57
|
+
const cap = (o) => (Number.isFinite(o.stock) && o.stock > 0 ? Math.min(o.stock, qty) : qty);
|
|
58
|
+
const nv = 2 * n; // [q_0..q_{n-1}, y_0..y_{n-1}]
|
|
59
|
+
const c_obj = [...items.map((o) => o.unitPrice), ...new Array(n).fill(0)];
|
|
60
|
+
const A = [];
|
|
61
|
+
const b = [];
|
|
62
|
+
const rel = [];
|
|
63
|
+
const row = () => new Array(nv).fill(0);
|
|
64
|
+
// Σ q_i = qty
|
|
65
|
+
{
|
|
66
|
+
const r = row();
|
|
67
|
+
for (let i = 0; i < n; i++)
|
|
68
|
+
r[i] = 1;
|
|
69
|
+
A.push(r);
|
|
70
|
+
b.push(qty);
|
|
71
|
+
rel.push("=");
|
|
72
|
+
}
|
|
73
|
+
for (let i = 0; i < n; i++) {
|
|
74
|
+
// q_i - cap_i·y_i <= 0
|
|
75
|
+
{
|
|
76
|
+
const r = row();
|
|
77
|
+
r[i] = 1;
|
|
78
|
+
r[n + i] = -cap(items[i]);
|
|
79
|
+
A.push(r);
|
|
80
|
+
b.push(0);
|
|
81
|
+
rel.push("<=");
|
|
82
|
+
}
|
|
83
|
+
// q_i - moq_i·y_i >= 0
|
|
84
|
+
const moq = items[i].moq ?? 0;
|
|
85
|
+
if (moq > 0) {
|
|
86
|
+
const r = row();
|
|
87
|
+
r[i] = 1;
|
|
88
|
+
r[n + i] = -moq;
|
|
89
|
+
A.push(r);
|
|
90
|
+
b.push(0);
|
|
91
|
+
rel.push(">=");
|
|
92
|
+
}
|
|
93
|
+
// y_i <= 1
|
|
94
|
+
{
|
|
95
|
+
const r = row();
|
|
96
|
+
r[n + i] = 1;
|
|
97
|
+
A.push(r);
|
|
98
|
+
b.push(1);
|
|
99
|
+
rel.push("<=");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (c.maxSuppliers != null) {
|
|
103
|
+
const r = row();
|
|
104
|
+
for (let i = 0; i < n; i++)
|
|
105
|
+
r[n + i] = 1;
|
|
106
|
+
A.push(r);
|
|
107
|
+
b.push(c.maxSuppliers);
|
|
108
|
+
rel.push("<=");
|
|
109
|
+
}
|
|
110
|
+
if (c.budget != null) {
|
|
111
|
+
const r = row();
|
|
112
|
+
for (let i = 0; i < n; i++)
|
|
113
|
+
r[i] = items[i].unitPrice;
|
|
114
|
+
A.push(r);
|
|
115
|
+
b.push(c.budget);
|
|
116
|
+
rel.push("<=");
|
|
117
|
+
}
|
|
118
|
+
const problem = { kind: "milp", c: c_obj, A, b, relations: rel, objective: "min", integers: Array.from({ length: nv }, (_v, i) => i) };
|
|
119
|
+
const sol = (await solver.solve(problem));
|
|
120
|
+
if (!sol || sol.status !== "optimal")
|
|
121
|
+
return { bySupplier: [], totalCost: 0, filled: 0, status: "infeasible" };
|
|
122
|
+
const bySupplier = [];
|
|
123
|
+
for (let i = 0; i < n; i++) {
|
|
124
|
+
const q = Math.round(sol.x[i] ?? 0);
|
|
125
|
+
if (q > 0) {
|
|
126
|
+
const o = items[i];
|
|
127
|
+
bySupplier.push({ supplier: o.supplier, qty: q, unitPrice: o.unitPrice, currency: o.currency, cost: q * o.unitPrice, url: o.url });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const filled = bySupplier.reduce((s, b2) => s + b2.qty, 0);
|
|
131
|
+
return { bySupplier, totalCost: bySupplier.reduce((s, b2) => s + b2.cost, 0), filled, status: filled >= qty ? "optimal" : "partial" };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Optimisation avec PALIERS de prix dégressifs (B2B « all-units » : Alibaba/1688). Pour chaque
|
|
135
|
+
* (offre, palier) éligible (minQty ≤ qté) : variable entière `q` + binaire `z`. Un fournisseur
|
|
136
|
+
* n'opère qu'UN palier (Σz ≤ 1) ; `q ∈ [minQty, borne_sup]` du palier actif ; Σq = qté ;
|
|
137
|
+
* objectif min Σ prix_palier·q. Résolu en **MILP** par `@mostajs/ro-pla` (branch-and-bound).
|
|
138
|
+
*/
|
|
139
|
+
export async function optimizeTiered(offers, qty, solver, c) {
|
|
140
|
+
const items = eligible(offers, c);
|
|
141
|
+
if (!items.length || qty <= 0)
|
|
142
|
+
return { bySupplier: [], totalCost: 0, filled: 0, status: "infeasible" };
|
|
143
|
+
const pairs = [];
|
|
144
|
+
const offerPairs = items.map(() => []);
|
|
145
|
+
items.forEach((o, i) => {
|
|
146
|
+
const tiers = (o.priceTiers && o.priceTiers.length
|
|
147
|
+
? [...o.priceTiers]
|
|
148
|
+
: [{ minQty: o.moq && o.moq > 0 ? o.moq : 1, unitPrice: o.unitPrice }]).sort((a, b) => a.minQty - b.minQty);
|
|
149
|
+
tiers.forEach((t, k) => {
|
|
150
|
+
if (t.minQty > qty)
|
|
151
|
+
return; // palier inatteignable seul
|
|
152
|
+
const upper = Math.min(tiers[k + 1] ? tiers[k + 1].minQty - 1 : qty, qty);
|
|
153
|
+
if (upper < t.minQty)
|
|
154
|
+
return;
|
|
155
|
+
offerPairs[i].push(pairs.length);
|
|
156
|
+
pairs.push({ i, price: t.unitPrice, minQty: t.minQty, upper });
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
if (!pairs.length)
|
|
160
|
+
return { bySupplier: [], totalCost: 0, filled: 0, status: "infeasible" };
|
|
161
|
+
const nP = pairs.length;
|
|
162
|
+
const nv = 2 * nP; // [q_0..q_{nP-1}, z_0..z_{nP-1}]
|
|
163
|
+
const cObj = [...pairs.map((p) => p.price), ...new Array(nP).fill(0)];
|
|
164
|
+
const A = [];
|
|
165
|
+
const b = [];
|
|
166
|
+
const rel = [];
|
|
167
|
+
const row = () => new Array(nv).fill(0);
|
|
168
|
+
{
|
|
169
|
+
const r = row();
|
|
170
|
+
for (let p = 0; p < nP; p++)
|
|
171
|
+
r[p] = 1;
|
|
172
|
+
A.push(r);
|
|
173
|
+
b.push(qty);
|
|
174
|
+
rel.push("=");
|
|
175
|
+
} // Σq = qty
|
|
176
|
+
for (let p = 0; p < nP; p++) {
|
|
177
|
+
{
|
|
178
|
+
const r = row();
|
|
179
|
+
r[p] = 1;
|
|
180
|
+
r[nP + p] = -pairs[p].minQty;
|
|
181
|
+
A.push(r);
|
|
182
|
+
b.push(0);
|
|
183
|
+
rel.push(">=");
|
|
184
|
+
} // q ≥ minQty·z
|
|
185
|
+
{
|
|
186
|
+
const r = row();
|
|
187
|
+
r[p] = 1;
|
|
188
|
+
r[nP + p] = -pairs[p].upper;
|
|
189
|
+
A.push(r);
|
|
190
|
+
b.push(0);
|
|
191
|
+
rel.push("<=");
|
|
192
|
+
} // q ≤ upper·z
|
|
193
|
+
{
|
|
194
|
+
const r = row();
|
|
195
|
+
r[nP + p] = 1;
|
|
196
|
+
A.push(r);
|
|
197
|
+
b.push(1);
|
|
198
|
+
rel.push("<=");
|
|
199
|
+
} // z ≤ 1
|
|
200
|
+
}
|
|
201
|
+
for (const ps of offerPairs) { // un seul palier/offre
|
|
202
|
+
if (ps.length <= 1)
|
|
203
|
+
continue;
|
|
204
|
+
const r = row();
|
|
205
|
+
for (const p of ps)
|
|
206
|
+
r[nP + p] = 1;
|
|
207
|
+
A.push(r);
|
|
208
|
+
b.push(1);
|
|
209
|
+
rel.push("<=");
|
|
210
|
+
}
|
|
211
|
+
if (c?.maxSuppliers != null) {
|
|
212
|
+
const r = row();
|
|
213
|
+
for (let p = 0; p < nP; p++)
|
|
214
|
+
r[nP + p] = 1;
|
|
215
|
+
A.push(r);
|
|
216
|
+
b.push(c.maxSuppliers);
|
|
217
|
+
rel.push("<=");
|
|
218
|
+
}
|
|
219
|
+
if (c?.budget != null) {
|
|
220
|
+
const r = row();
|
|
221
|
+
for (let p = 0; p < nP; p++)
|
|
222
|
+
r[p] = pairs[p].price;
|
|
223
|
+
A.push(r);
|
|
224
|
+
b.push(c.budget);
|
|
225
|
+
rel.push("<=");
|
|
226
|
+
}
|
|
227
|
+
const problem = { kind: "milp", c: cObj, A, b, relations: rel, objective: "min", integers: Array.from({ length: nv }, (_v, i) => i) };
|
|
228
|
+
const sol = (await solver.solve(problem, { maxNodes: 20000 }));
|
|
229
|
+
if (!sol || sol.status !== "optimal")
|
|
230
|
+
return { bySupplier: [], totalCost: 0, filled: 0, status: "infeasible" };
|
|
231
|
+
const bySupplier = [];
|
|
232
|
+
for (let p = 0; p < nP; p++) {
|
|
233
|
+
const q = Math.round(sol.x[p] ?? 0);
|
|
234
|
+
if (q > 0) {
|
|
235
|
+
const o = items[pairs[p].i];
|
|
236
|
+
bySupplier.push({ supplier: o.supplier, qty: q, unitPrice: pairs[p].price, currency: o.currency, cost: q * pairs[p].price, url: o.url });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const filled = bySupplier.reduce((s, x) => s + x.qty, 0);
|
|
240
|
+
return { bySupplier, totalCost: bySupplier.reduce((s, x) => s + x.cost, 0), filled, status: filled >= qty ? "optimal" : "partial" };
|
|
241
|
+
}
|
|
242
|
+
/** Wrapper direct du dialecte `assignment` (hongrois) : matrice de coûts → affectation optimale. */
|
|
243
|
+
export async function optimizeAssignment(cost, solver) {
|
|
244
|
+
const sol = (await solver.solve({ kind: "assignment", cost }));
|
|
245
|
+
return sol ? { assignment: sol.assignment, total: sol.total } : { assignment: [], total: 0 };
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Affecte N **lignes** de commande à des fournisseurs **distincts** (diversification / 1 fournisseur
|
|
249
|
+
* par produit), au coût total minimal — via le **hongrois** (ro-pla). Colonnes = fournisseurs candidats.
|
|
250
|
+
*/
|
|
251
|
+
export async function assignLinesToSuppliers(lines, solver) {
|
|
252
|
+
if (!lines.length)
|
|
253
|
+
return { assignment: [], totalCost: 0, status: "infeasible" };
|
|
254
|
+
// Colonnes = union des fournisseurs (par url unique).
|
|
255
|
+
const cols = [];
|
|
256
|
+
const colIdx = new Map();
|
|
257
|
+
for (const ln of lines)
|
|
258
|
+
for (const o of ln.offers)
|
|
259
|
+
if (!colIdx.has(o.url)) {
|
|
260
|
+
colIdx.set(o.url, cols.length);
|
|
261
|
+
cols.push({ url: o.url, supplier: o.supplier });
|
|
262
|
+
}
|
|
263
|
+
if (cols.length < lines.length)
|
|
264
|
+
return { assignment: [], totalCost: 0, status: "infeasible" }; // pas assez de fournisseurs distincts
|
|
265
|
+
const BIG = 1e9;
|
|
266
|
+
const cost = lines.map((ln) => cols.map((c) => {
|
|
267
|
+
const o = ln.offers.find((x) => x.url === c.url);
|
|
268
|
+
return o ? ln.qty * o.unitPrice : BIG; // fournisseur sans offre pour cette ligne → coût prohibitif
|
|
269
|
+
}));
|
|
270
|
+
const { assignment } = await optimizeAssignment(cost, solver);
|
|
271
|
+
const out = [];
|
|
272
|
+
let total = 0;
|
|
273
|
+
lines.forEach((ln, i) => {
|
|
274
|
+
const j = assignment[i];
|
|
275
|
+
const col = cols[j];
|
|
276
|
+
const o = col && ln.offers.find((x) => x.url === col.url);
|
|
277
|
+
if (!o)
|
|
278
|
+
return;
|
|
279
|
+
const cst = ln.qty * o.unitPrice;
|
|
280
|
+
total += cst;
|
|
281
|
+
out.push({ line: ln.key, supplier: o.supplier, unitPrice: o.unitPrice, qty: ln.qty, cost: cst, url: o.url });
|
|
282
|
+
});
|
|
283
|
+
return { assignment: out, totalCost: total, status: out.length === lines.length ? "optimal" : "infeasible" };
|
|
284
|
+
}
|
|
285
|
+
// ── Simplex/LP : allocation CONTINUE (divisible) avec contraintes linéaires (part max/fournisseur) ──
|
|
286
|
+
/**
|
|
287
|
+
* Allocation **continue** (biens divisibles : vrac/poids/volume) au coût min, avec contraintes
|
|
288
|
+
* linéaires — notamment une **part maximale par fournisseur** que le flot n'exprime pas simplement.
|
|
289
|
+
* Résolu en **LP (simplexe)** par ro-pla.
|
|
290
|
+
*/
|
|
291
|
+
export async function optimizeContinuous(offers, qty, solver, c) {
|
|
292
|
+
const items = eligible(offers, c);
|
|
293
|
+
if (!items.length || qty <= 0)
|
|
294
|
+
return { bySupplier: [], totalCost: 0, filled: 0, status: "infeasible" };
|
|
295
|
+
const n = items.length;
|
|
296
|
+
const cObj = items.map((o) => o.unitPrice);
|
|
297
|
+
const A = [];
|
|
298
|
+
const b = [];
|
|
299
|
+
const rel = [];
|
|
300
|
+
{
|
|
301
|
+
const r = new Array(n).fill(1);
|
|
302
|
+
A.push(r);
|
|
303
|
+
b.push(qty);
|
|
304
|
+
rel.push("=");
|
|
305
|
+
} // Σx = qty
|
|
306
|
+
items.forEach((o, i) => {
|
|
307
|
+
const cap = Number.isFinite(o.stock) && o.stock > 0 ? o.stock : qty;
|
|
308
|
+
const lim = c?.maxSharePerSupplier ? Math.min(cap, c.maxSharePerSupplier * qty) : cap;
|
|
309
|
+
const r = new Array(n).fill(0);
|
|
310
|
+
r[i] = 1;
|
|
311
|
+
A.push(r);
|
|
312
|
+
b.push(lim);
|
|
313
|
+
rel.push("<="); // x_i ≤ limite
|
|
314
|
+
});
|
|
315
|
+
const sol = (await solver.solve({ kind: "lp", c: cObj, A, b, relations: rel, objective: "min" }));
|
|
316
|
+
if (!sol || sol.status !== "optimal")
|
|
317
|
+
return { bySupplier: [], totalCost: 0, filled: 0, status: "infeasible" };
|
|
318
|
+
const bySupplier = [];
|
|
319
|
+
items.forEach((o, i) => { const q = sol.x[i] ?? 0; if (q > 1e-9)
|
|
320
|
+
bySupplier.push({ supplier: o.supplier, qty: q, unitPrice: o.unitPrice, currency: o.currency, cost: q * o.unitPrice, url: o.url }); });
|
|
321
|
+
const filled = bySupplier.reduce((s, x) => s + x.qty, 0);
|
|
322
|
+
return { bySupplier, totalCost: sol.objective, filled, status: filled >= qty - 1e-6 ? "optimal" : "partial" };
|
|
323
|
+
}
|
|
324
|
+
/** Optimise : paliers (B2B) → MILP par paliers ; sinon contraintes → MILP ; sinon achat fractionné (flot). */
|
|
325
|
+
export async function optimizeOffers(offers, qty, solver, c) {
|
|
326
|
+
const elig = eligible(offers, c);
|
|
327
|
+
if (elig.some((o) => (o.priceTiers?.length ?? 0) > 0))
|
|
328
|
+
return optimizeTiered(offers, qty, solver, c);
|
|
329
|
+
if (c && (c.budget != null || c.maxSuppliers != null))
|
|
330
|
+
return optimizeConstrained(offers, qty, c, solver);
|
|
331
|
+
return optimizeSplitBuy(offers, qty, solver, c);
|
|
332
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mostajs/agora-sourcing — registre de scrapers (forme « dialectes » façon ORM/ro-pla/llm).
|
|
3
|
+
* Un scraper = un dialecte ; sélection par clé ; auto-enregistrement des built-ins.
|
|
4
|
+
* @author Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import type { ScraperDialect, DiscoveryDialect } from "./types.js";
|
|
7
|
+
/** Enregistre (ou remplace) un dialecte de scraper. */
|
|
8
|
+
export declare function registerScraper(d: ScraperDialect): void;
|
|
9
|
+
/** Récupère un scraper par clé (null si absent). */
|
|
10
|
+
export declare function getScraper(key: string): ScraperDialect | null;
|
|
11
|
+
/** Liste les scrapers enregistrés. */
|
|
12
|
+
export declare function listScrapers(): ScraperDialect[];
|
|
13
|
+
/** Vide le registre (tests). */
|
|
14
|
+
export declare function clearScrapers(): void;
|
|
15
|
+
/**
|
|
16
|
+
* Scrapers exposés à l'app, filtrés par `AGORA_SCRAPERS` (CSV) façon `CHATBOT_PROVIDERS`.
|
|
17
|
+
* Vide ⇒ tous.
|
|
18
|
+
*/
|
|
19
|
+
export declare function activeScrapers(env?: Record<string, string | undefined>): ScraperDialect[];
|
|
20
|
+
/** Route une URL vers le scraper dont un `hosts[]` correspond, sinon le scraper générique. */
|
|
21
|
+
export declare function pickScraper(url: string): ScraperDialect | null;
|
|
22
|
+
export declare function registerDiscovery(d: DiscoveryDialect): void;
|
|
23
|
+
export declare function getDiscovery(key: string): DiscoveryDialect | null;
|
|
24
|
+
export declare function listDiscovery(): DiscoveryDialect[];
|
|
25
|
+
export declare function clearDiscovery(): void;
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const SCRAPERS = new Map();
|
|
2
|
+
const DISCOVERY = new Map();
|
|
3
|
+
/** Enregistre (ou remplace) un dialecte de scraper. */
|
|
4
|
+
export function registerScraper(d) {
|
|
5
|
+
SCRAPERS.set(d.key, d);
|
|
6
|
+
}
|
|
7
|
+
/** Récupère un scraper par clé (null si absent). */
|
|
8
|
+
export function getScraper(key) {
|
|
9
|
+
return SCRAPERS.get(key) ?? null;
|
|
10
|
+
}
|
|
11
|
+
/** Liste les scrapers enregistrés. */
|
|
12
|
+
export function listScrapers() {
|
|
13
|
+
return [...SCRAPERS.values()];
|
|
14
|
+
}
|
|
15
|
+
/** Vide le registre (tests). */
|
|
16
|
+
export function clearScrapers() {
|
|
17
|
+
SCRAPERS.clear();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Scrapers exposés à l'app, filtrés par `AGORA_SCRAPERS` (CSV) façon `CHATBOT_PROVIDERS`.
|
|
21
|
+
* Vide ⇒ tous.
|
|
22
|
+
*/
|
|
23
|
+
export function activeScrapers(env = process.env) {
|
|
24
|
+
const csv = (env.AGORA_SCRAPERS ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
25
|
+
const all = listScrapers();
|
|
26
|
+
return csv.length ? all.filter((s) => csv.includes(s.key)) : all;
|
|
27
|
+
}
|
|
28
|
+
/** Route une URL vers le scraper dont un `hosts[]` correspond, sinon le scraper générique. */
|
|
29
|
+
export function pickScraper(url) {
|
|
30
|
+
let host = "";
|
|
31
|
+
try {
|
|
32
|
+
host = new URL(url).hostname.replace(/^www\./, "");
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return getScraper("generic");
|
|
36
|
+
}
|
|
37
|
+
for (const s of listScrapers()) {
|
|
38
|
+
if (s.hosts?.some((h) => host === h || host.endsWith("." + h) || host.endsWith(h)))
|
|
39
|
+
return s;
|
|
40
|
+
}
|
|
41
|
+
return getScraper("generic");
|
|
42
|
+
}
|
|
43
|
+
// ── Registre de découverte (moteur de recherche : requête → URLs) ──
|
|
44
|
+
export function registerDiscovery(d) { DISCOVERY.set(d.key, d); }
|
|
45
|
+
export function getDiscovery(key) { return DISCOVERY.get(key) ?? null; }
|
|
46
|
+
export function listDiscovery() { return [...DISCOVERY.values()]; }
|
|
47
|
+
export function clearDiscovery() { DISCOVERY.clear(); }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mostajs/agora-sourcing — filtre de PERTINENCE (titre/url ↔ requête).
|
|
3
|
+
* Évite que le scraper générique ramasse un prix hors-sujet (ex. « lames de broyeur » $15 pris pour
|
|
4
|
+
* le broyeur). Pur, testable. Appliqué entre le scrape et l'optimisation.
|
|
5
|
+
* @author Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
6
|
+
*/
|
|
7
|
+
import type { Offer } from "./types.js";
|
|
8
|
+
/** Tokens significatifs (minuscule, sans accents, ≥2 car., hors stopwords). */
|
|
9
|
+
export declare function tokens(s: string): string[];
|
|
10
|
+
/** Score 0..1 = part des tokens de la requête retrouvés dans le texte (avec sous-chaîne tolérée). */
|
|
11
|
+
export declare function relevanceScore(text: string, query: string): number;
|
|
12
|
+
/** Garde les offres dont le titre/url/fournisseur matche assez la requête (défaut ≥ 0.34). */
|
|
13
|
+
export declare function filterRelevant(offers: Offer[], query: string, min?: number): Offer[];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const STOP = new Set(["the", "and", "for", "with", "de", "la", "le", "les", "des", "du", "un", "une", "of", "to", "in", "series", "model", "type", "new", "and"]);
|
|
2
|
+
/** Tokens significatifs (minuscule, sans accents, ≥2 car., hors stopwords). */
|
|
3
|
+
export function tokens(s) {
|
|
4
|
+
return ((s || "").toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").match(/[a-z0-9./-]{2,}/g) ?? []).filter((t) => !STOP.has(t));
|
|
5
|
+
}
|
|
6
|
+
/** Score 0..1 = part des tokens de la requête retrouvés dans le texte (avec sous-chaîne tolérée). */
|
|
7
|
+
export function relevanceScore(text, query) {
|
|
8
|
+
const q = tokens(query);
|
|
9
|
+
if (!q.length)
|
|
10
|
+
return 1;
|
|
11
|
+
const t = tokens(text);
|
|
12
|
+
const set = new Set(t);
|
|
13
|
+
let hit = 0;
|
|
14
|
+
for (const w of q)
|
|
15
|
+
if (set.has(w) || t.some((x) => x.includes(w) || w.includes(x)))
|
|
16
|
+
hit++;
|
|
17
|
+
return hit / q.length;
|
|
18
|
+
}
|
|
19
|
+
/** Garde les offres dont le titre/url/fournisseur matche assez la requête (défaut ≥ 0.34). */
|
|
20
|
+
export function filterRelevant(offers, query, min = 0.34) {
|
|
21
|
+
return offers.filter((o) => {
|
|
22
|
+
const title = o.raw?.title ?? "";
|
|
23
|
+
return relevanceScore(`${title} ${o.url} ${o.supplier}`, query) >= min;
|
|
24
|
+
});
|
|
25
|
+
}
|
package/dist/rfq.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mostajs/agora-sourcing — pont RFQ : compose **@mostajs/sourcing** (`sendRfqs`), pas de réécriture.
|
|
3
|
+
* Mappe les offres scrappées (Offer agora) vers le modèle Offer de sourcing, puis délègue l'envoi
|
|
4
|
+
* (dispatchers injectés ; `dryRun` par défaut = brouillon, human-in-the-loop DEVRULES §11).
|
|
5
|
+
* @author Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
6
|
+
*/
|
|
7
|
+
import type { Offer } from "./types.js";
|
|
8
|
+
/** Offre au format attendu par @mostajs/sourcing (sous-ensemble suffisant pour la RFQ). */
|
|
9
|
+
export interface SourcingOffer {
|
|
10
|
+
supplier: string;
|
|
11
|
+
productMatch: string;
|
|
12
|
+
unitPrice?: number;
|
|
13
|
+
currency?: string;
|
|
14
|
+
moq?: number;
|
|
15
|
+
leadTimeDays?: number;
|
|
16
|
+
country?: string;
|
|
17
|
+
sourceUrl?: string;
|
|
18
|
+
contactEmail?: string;
|
|
19
|
+
contacts?: Record<string, string | undefined>;
|
|
20
|
+
connector?: string;
|
|
21
|
+
confidence: number;
|
|
22
|
+
}
|
|
23
|
+
/** Convertit des offres scrappées (faits) en offres sourcing. `confidence=1` (donnée vérifiée). */
|
|
24
|
+
export declare function toSourcingOffers(offers: Offer[], query: string): SourcingOffer[];
|
|
25
|
+
/** Signature minimale du `sendRfqs` de @mostajs/sourcing (injectable). */
|
|
26
|
+
export type SendRfqs = (offers: SourcingOffer[], query: string, opts?: Record<string, unknown>) => Promise<unknown>;
|
|
27
|
+
export interface RfqDeps {
|
|
28
|
+
/** `sendRfqs` injecté (tests) ; défaut = `@mostajs/sourcing` (peer, chargé au runtime). */
|
|
29
|
+
sendRfqs?: SendRfqs;
|
|
30
|
+
dispatchers?: unknown[];
|
|
31
|
+
from?: string;
|
|
32
|
+
ref?: string;
|
|
33
|
+
/** Brouillon par défaut (humain-in-the-loop) ; passer `false` pour acheminer réellement. */
|
|
34
|
+
dryRun?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/** Émet des RFQ pour les offres retenues, en composant @mostajs/sourcing. */
|
|
37
|
+
export declare function sendAgoraRfqs(offers: Offer[], query: string, deps?: RfqDeps): Promise<unknown>;
|
package/dist/rfq.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Convertit des offres scrappées (faits) en offres sourcing. `confidence=1` (donnée vérifiée). */
|
|
2
|
+
export function toSourcingOffers(offers, query) {
|
|
3
|
+
return offers.map((o) => ({
|
|
4
|
+
supplier: o.supplier,
|
|
5
|
+
productMatch: query,
|
|
6
|
+
unitPrice: o.unitPrice,
|
|
7
|
+
currency: o.currency,
|
|
8
|
+
moq: o.moq,
|
|
9
|
+
country: o.country,
|
|
10
|
+
sourceUrl: o.url,
|
|
11
|
+
connector: "agora",
|
|
12
|
+
confidence: 1,
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
/** Émet des RFQ pour les offres retenues, en composant @mostajs/sourcing. */
|
|
16
|
+
export async function sendAgoraRfqs(offers, query, deps = {}) {
|
|
17
|
+
let send = deps.sendRfqs;
|
|
18
|
+
if (!send) {
|
|
19
|
+
const spec = "@mostajs/sourcing";
|
|
20
|
+
send = (await import(spec)).sendRfqs;
|
|
21
|
+
}
|
|
22
|
+
return send(toSourcingOffers(offers, query), query, {
|
|
23
|
+
from: deps.from,
|
|
24
|
+
ref: deps.ref,
|
|
25
|
+
dryRun: deps.dryRun ?? true,
|
|
26
|
+
dispatchers: deps.dispatchers,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Offer, ScraperDialect } from "../types.js";
|
|
2
|
+
/** Récupère du JSON à une URL (DI ; défaut = fetch global). */
|
|
3
|
+
export type JsonFetch = (url: string) => Promise<unknown>;
|
|
4
|
+
export interface AliexpressConfig {
|
|
5
|
+
appKey?: string;
|
|
6
|
+
appSecret?: string;
|
|
7
|
+
/** Endpoint système (défaut : passerelle internationale). */
|
|
8
|
+
endpoint?: string;
|
|
9
|
+
/** Devise/langue/pays cibles. */
|
|
10
|
+
targetCurrency?: string;
|
|
11
|
+
targetLanguage?: string;
|
|
12
|
+
shipToCountry?: string;
|
|
13
|
+
/** HTTP injectable (tests). */
|
|
14
|
+
httpJson?: JsonFetch;
|
|
15
|
+
/** Horloge injectable (signature déterministe en test). */
|
|
16
|
+
now?: () => number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Signe un jeu de paramètres (schéma TOP) : trie les clés, concatène `clé+valeur`, puis
|
|
20
|
+
* HMAC-SHA256 (clé = app_secret) en hex MAJUSCULE. (`md5` legacy : secret+base+secret.)
|
|
21
|
+
*/
|
|
22
|
+
export declare function signTop(params: Record<string, string>, appSecret: string, method?: "sha256" | "md5"): string;
|
|
23
|
+
/** Construit l'URL signée d'un appel API TOP. */
|
|
24
|
+
export declare function buildSignedUrl(cfg: Required<Pick<AliexpressConfig, "appKey" | "appSecret">> & AliexpressConfig, apiMethod: string, business: Record<string, string>): string;
|
|
25
|
+
/** Extrait les produits d'une réponse `aliexpress.affiliate.product.query` → Offer[]. */
|
|
26
|
+
export declare function mapProducts(json: unknown): Offer[];
|
|
27
|
+
/**
|
|
28
|
+
* Dialecte scraper AliExpress (clé `aliexpress`). Sans `appKey`/`appSecret` → renvoie vide
|
|
29
|
+
* (le sourcing se rabat sur les autres connecteurs / le cache).
|
|
30
|
+
*/
|
|
31
|
+
export declare function aliexpressConnector(cfg?: AliexpressConfig): ScraperDialect;
|