334 lines
10 KiB
JavaScript
334 lines
10 KiB
JavaScript
/// <reference path="../pb_data/types.d.ts" />
|
|
|
|
const ANTHROPIC_MODEL = "claude-sonnet-4-20250514";
|
|
|
|
function getAnthropicKey() {
|
|
return $os.getenv("ANTHROPIC_API_KEY");
|
|
}
|
|
|
|
/**
|
|
* @param {string} userText
|
|
* @returns {{ text: string, error?: { status: number, body: unknown } }}
|
|
*/
|
|
function callAnthropic(userText) {
|
|
const key = getAnthropicKey();
|
|
if (!key) {
|
|
return { text: "", error: { status: 500, body: { message: "ANTHROPIC_API_KEY manquante" } } };
|
|
}
|
|
const payload = {
|
|
model: ANTHROPIC_MODEL,
|
|
max_tokens: 2200,
|
|
messages: [{ role: "user", content: userText }],
|
|
};
|
|
const res = $http.send({
|
|
url: "https://api.anthropic.com/v1/messages",
|
|
method: "POST",
|
|
headers: {
|
|
"x-api-key": key,
|
|
"anthropic-version": "2023-06-01",
|
|
"content-type": "application/json",
|
|
},
|
|
body: JSON.stringify(payload),
|
|
timeout: 120,
|
|
});
|
|
if (res.statusCode >= 400) {
|
|
const errText = typeof res.raw === "string" ? res.raw : "";
|
|
return {
|
|
text: "",
|
|
error: {
|
|
status: 502,
|
|
body: { message: "Anthropic", statusCode: res.statusCode, detail: errText.slice(0, 1500) },
|
|
},
|
|
};
|
|
}
|
|
let parsed = res.json;
|
|
if (parsed == null && typeof res.raw === "string" && res.raw.length > 0) {
|
|
try {
|
|
parsed = JSON.parse(res.raw);
|
|
} catch (_) {
|
|
parsed = null;
|
|
}
|
|
}
|
|
const text =
|
|
parsed && Array.isArray(parsed.content) && parsed.content[0] && parsed.content[0].text
|
|
? parsed.content[0].text
|
|
: "";
|
|
return { text };
|
|
}
|
|
|
|
function readJsonBody(e) {
|
|
const b = e.requestInfo().body || {};
|
|
return typeof b === "object" && b != null ? b : {};
|
|
}
|
|
|
|
routerAdd(
|
|
"POST",
|
|
"/api/mdb/agent-immobilier",
|
|
(e) => {
|
|
if (!e.auth) {
|
|
return e.json(401, { message: "Non autorisé" });
|
|
}
|
|
const body = readJsonBody(e);
|
|
const objectif = typeof body.objectif === "string" ? body.objectif : "prospection off-market";
|
|
const contexte = typeof body.contexte === "string" ? body.contexte : "";
|
|
const prompt =
|
|
"Tu es un agent immobilier senior en France. Rédige un plan d'actions concret (puces) puis un brouillon de message court (email ou message) pour : " +
|
|
objectif +
|
|
".\nContexte fourni par l'utilisateur :\n" +
|
|
contexte;
|
|
const { text, error } = callAnthropic(prompt);
|
|
if (error) {
|
|
return e.json(error.status, error.body);
|
|
}
|
|
const save = body.save === true;
|
|
if (save && text) {
|
|
const rec = new Record($app.findCollectionByNameOrId("courriers_immobilier"), {
|
|
user: e.auth.id,
|
|
titre: "Brouillon — " + objectif.slice(0, 80),
|
|
corps: text,
|
|
kind: "prospection",
|
|
etat: "brouillon",
|
|
});
|
|
$app.save(rec);
|
|
return e.json(200, { brouillon: text, courrier_id: rec.id });
|
|
}
|
|
return e.json(200, { brouillon: text });
|
|
},
|
|
$apis.requireAuth(),
|
|
);
|
|
|
|
routerAdd(
|
|
"POST",
|
|
"/api/mdb/agent-marchand",
|
|
(e) => {
|
|
if (!e.auth) {
|
|
return e.json(401, { message: "Non autorisé" });
|
|
}
|
|
const body = readJsonBody(e);
|
|
const titre = typeof body.titre === "string" ? body.titre : "Annonce";
|
|
const prix = typeof body.prix === "number" ? body.prix : null;
|
|
const surface = typeof body.surface === "number" ? body.surface : null;
|
|
const code_postal = typeof body.code_postal === "string" ? body.code_postal : "";
|
|
const ville = typeof body.ville === "string" ? body.ville : "";
|
|
const notes = typeof body.notes === "string" ? body.notes : "";
|
|
const grille = typeof body.grille_json === "string" ? body.grille_json : "";
|
|
const prompt =
|
|
"Tu es un marchand de biens en France. Analyse l'offre suivante : titre=" +
|
|
titre +
|
|
", prix=" +
|
|
String(prix) +
|
|
", surface_m2=" +
|
|
String(surface) +
|
|
", CP=" +
|
|
code_postal +
|
|
", ville=" +
|
|
ville +
|
|
".\nNotes utilisateur : " +
|
|
notes +
|
|
"\nRéférentiel grille perso (JSON optionnel) : " +
|
|
grille +
|
|
"\nRéponds en français : (1) fourchette €/m² si calculable, (2) points de vigilance, (3) verdict rapide opportunité / neutre / risqué.";
|
|
const { text, error } = callAnthropic(prompt);
|
|
if (error) {
|
|
return e.json(error.status, error.body);
|
|
}
|
|
return e.json(200, { analyse: text });
|
|
},
|
|
$apis.requireAuth(),
|
|
);
|
|
|
|
routerAdd(
|
|
"POST",
|
|
"/api/mdb/agent-dvf",
|
|
(e) => {
|
|
if (!e.auth) {
|
|
return e.json(401, { message: "Non autorisé" });
|
|
}
|
|
const body = readJsonBody(e);
|
|
const libelle = typeof body.libelle === "string" ? body.libelle : "";
|
|
if (!libelle.trim()) {
|
|
return e.json(400, { message: "libelle requis" });
|
|
}
|
|
const code_insee = typeof body.code_insee === "string" ? body.code_insee : "";
|
|
const annee = typeof body.annee === "number" ? body.annee : null;
|
|
const prix_m2_median = typeof body.prix_m2_median === "number" ? body.prix_m2_median : null;
|
|
const nb_ventes = typeof body.nb_ventes === "number" ? body.nb_ventes : null;
|
|
const detail_json = typeof body.detail_json === "string" ? body.detail_json : "";
|
|
const col = $app.findCollectionByNameOrId("transactions_secteur");
|
|
const data = {
|
|
user: e.auth.id,
|
|
libelle: libelle.trim(),
|
|
source: "manuel",
|
|
};
|
|
if (code_insee) {
|
|
data.code_insee = code_insee;
|
|
}
|
|
if (annee != null) {
|
|
data.annee = annee;
|
|
}
|
|
if (prix_m2_median != null) {
|
|
data.prix_m2_median = prix_m2_median;
|
|
}
|
|
if (nb_ventes != null) {
|
|
data.nb_ventes = nb_ventes;
|
|
}
|
|
if (detail_json) {
|
|
data.detail_json = detail_json;
|
|
}
|
|
const rec = new Record(col, data);
|
|
$app.save(rec);
|
|
const prompt =
|
|
"Tu es un analyste immobilier. Synthétise en 5 phrases maximum l'intérêt de ces statistiques de marché (secteur, médiane €/m², volume) pour un marchand de biens.\nDonnées : " +
|
|
JSON.stringify({
|
|
libelle: libelle.trim(),
|
|
code_insee,
|
|
annee,
|
|
prix_m2_median,
|
|
nb_ventes,
|
|
});
|
|
const { text, error } = callAnthropic(prompt);
|
|
if (error) {
|
|
return e.json(error.status, error.body);
|
|
}
|
|
return e.json(200, { id: rec.id, synthese: text });
|
|
},
|
|
$apis.requireAuth(),
|
|
);
|
|
|
|
routerAdd(
|
|
"POST",
|
|
"/api/mdb/agent-veille",
|
|
(e) => {
|
|
if (!e.auth) {
|
|
return e.json(401, { message: "Non autorisé" });
|
|
}
|
|
const body = readJsonBody(e);
|
|
const titre = typeof body.titre === "string" ? body.titre.trim() : "";
|
|
if (!titre) {
|
|
return e.json(400, { message: "titre requis" });
|
|
}
|
|
const url = typeof body.url === "string" ? body.url.trim() : "";
|
|
const source = typeof body.source === "string" ? body.source.trim() : "manuel";
|
|
const prix = typeof body.prix === "number" ? body.prix : undefined;
|
|
const surface = typeof body.surface === "number" ? body.surface : undefined;
|
|
const code_postal = typeof body.code_postal === "string" ? body.code_postal : "";
|
|
const ville = typeof body.ville === "string" ? body.ville : "";
|
|
const empreinte = $security.md5((url || "") + "\n" + titre);
|
|
const col = $app.findCollectionByNameOrId("annonces_veille");
|
|
const filt = 'user = "' + e.auth.id + '" && empreinte = "' + empreinte + '"';
|
|
let existing = null;
|
|
try {
|
|
existing = $app.findFirstRecordByFilter("annonces_veille", filt);
|
|
} catch (_) {
|
|
existing = null;
|
|
}
|
|
if (existing != null && existing.id) {
|
|
return e.json(200, { id: existing.id, dedupe: true, message: "Déjà enregistrée" });
|
|
}
|
|
const ann = {
|
|
user: e.auth.id,
|
|
titre,
|
|
url,
|
|
source,
|
|
empreinte,
|
|
statut: "nouveau",
|
|
};
|
|
if (prix !== undefined) {
|
|
ann.prix = prix;
|
|
}
|
|
if (surface !== undefined) {
|
|
ann.surface = surface;
|
|
}
|
|
if (code_postal) {
|
|
ann.code_postal = code_postal;
|
|
}
|
|
if (ville) {
|
|
ann.ville = ville;
|
|
}
|
|
const rec = new Record(col, ann);
|
|
$app.save(rec);
|
|
return e.json(200, { id: rec.id, dedupe: false });
|
|
},
|
|
$apis.requireAuth(),
|
|
);
|
|
|
|
routerAdd(
|
|
"POST",
|
|
"/api/mdb/agent-redaction",
|
|
(e) => {
|
|
if (!e.auth) {
|
|
return e.json(401, { message: "Non autorisé" });
|
|
}
|
|
const body = readJsonBody(e);
|
|
const kind = typeof body.kind === "string" ? body.kind : "annonce_agence";
|
|
const bullets = Array.isArray(body.bullets) ? body.bullets.map(String).join("\n- ") : "";
|
|
const prompt =
|
|
"Tu es rédacteur pour une agence immobilière en France. Rédige un texte court et professionnel (titres + paragraphes) à partir des puces :\n- " +
|
|
bullets +
|
|
"\nType de contenu : " +
|
|
kind +
|
|
".";
|
|
const { text, error } = callAnthropic(prompt);
|
|
if (error) {
|
|
return e.json(error.status, error.body);
|
|
}
|
|
const save = body.save === true;
|
|
if (save && text) {
|
|
const col = $app.findCollectionByNameOrId("courriers_immobilier");
|
|
const rec = new Record(col, {
|
|
user: e.auth.id,
|
|
titre: "Rédaction — " + kind,
|
|
corps: text,
|
|
kind: kind === "relance" || kind === "prospection" ? kind : "annonce_agence",
|
|
etat: "pret",
|
|
});
|
|
$app.save(rec);
|
|
return e.json(200, { texte: text, courrier_id: rec.id });
|
|
}
|
|
return e.json(200, { texte: text });
|
|
},
|
|
$apis.requireAuth(),
|
|
);
|
|
|
|
routerAdd(
|
|
"POST",
|
|
"/api/mdb/agent-alertes-scan",
|
|
(e) => {
|
|
if (!e.auth) {
|
|
return e.json(401, { message: "Non autorisé" });
|
|
}
|
|
const now = new Date().toISOString();
|
|
let updated = 0;
|
|
try {
|
|
const rows = $app.findRecordsByFilter(
|
|
"alertes_recherche",
|
|
'user = "' + e.auth.id + '"',
|
|
"-created",
|
|
50,
|
|
0,
|
|
);
|
|
for (let i = 0; i < rows.length; i++) {
|
|
const r = rows[i];
|
|
if (!r) {
|
|
continue;
|
|
}
|
|
if (r.get("actif") === false) {
|
|
continue;
|
|
}
|
|
r.set("derniere_verification", now);
|
|
r.set("dernier_nb_resultats", 0);
|
|
$app.save(r);
|
|
updated++;
|
|
}
|
|
} catch (_) {
|
|
/* noop */
|
|
}
|
|
return e.json(200, { processed: updated, note: "Stub: branchement annonces agrégées à venir." });
|
|
},
|
|
$apis.requireAuth(),
|
|
);
|
|
|
|
cronAdd("mdb_agents_alertes_tick", "0 * * * *", () => {
|
|
/* Placeholder : futur batch serveur (sans auth utilisateur). */
|
|
});
|