/// 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). */ });