recherche
This commit is contained in:
333
pocketbase/pb_hooks/agents_veille.pb.js
Normal file
333
pocketbase/pb_hooks/agents_veille.pb.js
Normal file
@ -0,0 +1,333 @@
|
||||
/// <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). */
|
||||
});
|
||||
Reference in New Issue
Block a user