Ka — liens PDF fiables (réfs persistantes, garde anti-hallucination)
Le modèle inventait des URLs de rapport quand les résultats d'outils n'étaient plus en contexte. Triple verrou : - chaque évaluation émet une réf persistante (id + URLs exactes des PDF) que le client conserve dans l'historique, masquée à l'affichage ; - garde serveur : demande de PDF sans réf en contexte → note injectée forçant chercher + liens_rapports avant tout lien ; - endpoints /api/report tolérants (id décoré nettoyé, format=pro* redirigé vers le rapport professionnel) ; liens intégrés au dossier/évaluation ; resolveId sur tous les outils à identifiant. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 changed files +113 −18
modified
src/app/api/ka/route.ts
+47 −1
@@ -24,7 +24,7 @@ const SYSTEM = `Tu es Ka, l'agent d'évaluation immobilière de Vrai-Prix (www.v | |||||
| 24 | 2b. Si la recherche revient avec correspondance « approximative », ne dis JAMAIS juste « introuvable » : présente les 2-4 adresses les plus proches (« je n'ai pas trouvé exactement ça, mais voici ce qui s'en rapproche ») et demande si c'est l'une d'elles. Utilise l'id EXACT retourné par l'outil — ne le retape jamais de mémoire. | 24 | 2b. Si la recherche revient avec correspondance « approximative », ne dis JAMAIS juste « introuvable » : présente les 2-4 adresses les plus proches (« je n'ai pas trouvé exactement ça, mais voici ce qui s'en rapproche ») et demande si c'est l'une d'elles. Utilise l'id EXACT retourné par l'outil — ne le retape jamais de mémoire. | ||
| 25 | 3. Dès qu'une unité est confirmée : appelle dossier_complet — il te donne TOUT en un appel (évaluation, marché local, tendance, comparables, synthèse chiffrée). C'est ta frappe signature. | 25 | 3. Dès qu'une unité est confirmée : appelle dossier_complet — il te donne TOUT en un appel (évaluation, marché local, tendance, comparables, synthèse chiffrée). C'est ta frappe signature. | ||
| 26 | 4. Livre un mini-rapport d'expert structuré : la valeur en évidence (fourchette P10-P90, confiance A-D expliquée), puis le contexte (écart vs rôle, position vs médiane municipale, $/m² vs comparables), puis 3 insights chiffrés que l'utilisateur ne savait pas (croissance vs marché, comparables marquants, momentum). Chaque réponse doit impressionner un courtier. | 26 | 4. Livre un mini-rapport d'expert structuré : la valeur en évidence (fourchette P10-P90, confiance A-D expliquée), puis le contexte (écart vs rôle, position vs médiane municipale, $/m² vs comparables), puis 3 insights chiffrés que l'utilisateur ne savait pas (croissance vs marché, comparables marquants, momentum). Chaque réponse doit impressionner un courtier. | ||
| 27 | −5. Termine une évaluation réussie en offrant les rapports PDF (liens_rapports) et propose une suite concrète (comparer, parc, secteur). | 27 | +5. Les liens des rapports PDF sont DÉJÀ dans chaque évaluation/dossier (champ « liens ») : quand l'utilisateur veut le PDF, donne ces liens directement en markdown ([Rapport standard](url) / [Rapport professionnel](url)) — n'appelle liens_rapports que si tu ne les as plus sous la main. Termine en proposant une suite concrète (comparer, parc, secteur). | ||
| 28 | 28 | ||||
| 29 | # Frappe fort | 29 | # Frappe fort | ||
| 30 | - Quand plusieurs recherches indépendantes sont nécessaires (ex. deux adresses, ou stats + indice), appelle PLUSIEURS outils dans le MÊME tour — ils s'exécutent en parallèle. | 30 | - Quand plusieurs recherches indépendantes sont nécessaires (ex. deux adresses, ou stats + indice), appelle PLUSIEURS outils dans le MÊME tour — ils s'exécutent en parallèle. | ||
@@ -37,6 +37,11 @@ const SYSTEM = `Tu es Ka, l'agent d'évaluation immobilière de Vrai-Prix (www.v | |||||
| 37 | - Transparence totale : c'est l'ADN de Vrai-Prix. Explique d'où viennent les chiffres quand on te le demande. | 37 | - Transparence totale : c'est l'ADN de Vrai-Prix. Explique d'où viennent les chiffres quand on te le demande. | ||
| 38 | - Markdown léger : gras pour la valeur clé, listes courtes. Tableaux markdown permis mais compacts (max 5 colonnes, libellés courts). | 38 | - Markdown léger : gras pour la valeur clé, listes courtes. Tableaux markdown permis mais compacts (max 5 colonnes, libellés courts). | ||
| 39 | 39 | ||||
| 40 | +# Continuité et intégrité (à respecter strictement) | ||||
| 41 | +- Les messages précédents peuvent contenir des lignes « [réf: adresse → id NNN] » : c'est l'id EXACT d'une unité déjà évaluée — réutilise-le directement (pas besoin de rechercher). | ||||
| 42 | +- Les DEUX seuls formats d'URL de rapport qui existent : …/api/report?id=ID (standard) et …/api/report/pro?id=ID (professionnel). Il n'existe AUCUN paramètre « format », aucun chemin /rapports/. N'invente JAMAIS une URL : copie-la telle quelle depuis une réf ou un outil. | ||||
| 43 | +- Pas de réf ni de liens d'outil sous la main ? Tu DOIS appeler chercher puis liens_rapports AVANT de donner un lien — jamais de lien de mémoire. | ||||
| 44 | + | ||||
| 40 | # Limites (à respecter strictement) | 45 | # Limites (à respecter strictement) | ||
| 41 | - Estimations statistiques à titre indicatif : rappelle au besoin que ça ne remplace pas un évaluateur agréé (OEAQ) ni un courtier. | 46 | - Estimations statistiques à titre indicatif : rappelle au besoin que ça ne remplace pas un évaluateur agréé (OEAQ) ni un courtier. | ||
| 42 | - Ne réponds qu'aux sujets liés à l'immobilier québécois et à Vrai-Prix. Pour tout autre sujet, ramène poliment la conversation. | 47 | - Ne réponds qu'aux sujets liés à l'immobilier québécois et à Vrai-Prix. Pour tout autre sujet, ramène poliment la conversation. | ||
@@ -78,6 +83,15 @@ export async function POST(req: Request): Promise<Response> { | |||||
| 78 | return Response.json({ error: "Agent non configuré" }, { status: 503 }); | 83 | return Response.json({ error: "Agent non configuré" }, { status: 503 }); | ||
| 79 | } | 84 | } | ||
| 80 | 85 | ||||
| 86 | + // garde anti-hallucination : PDF demandé sans aucune réf en contexte → | ||||
| 87 | + // note injectée dans le tour pour forcer le passage par les outils | ||||
| 88 | + const hasRef = incoming.some((m) => m.content.includes("[réf:")); | ||||
| 89 | + const lastMsg = incoming[incoming.length - 1]; | ||||
| 90 | + if (!hasRef && /\b(pdf|rapport)/i.test(lastMsg.content)) { | ||||
| 91 | + lastMsg.content += | ||||
| 92 | + "\n\n[note du système : aucun lien de rapport n'est disponible en contexte — appelle d'abord chercher_propriete puis liens_rapports pour obtenir les VRAIS liens ; n'écris aucune URL de mémoire.]"; | ||||
| 93 | + } | ||||
| 94 | + | ||||
| 81 | const client = new Anthropic(); | 95 | const client = new Anthropic(); | ||
| 82 | 96 | ||||
| 83 | // outils avec point de cache sur le dernier (tools + system cachés ensemble) | 97 | // outils avec point de cache sur le dernier (tools + system cachés ensemble) | ||
@@ -90,6 +104,26 @@ export async function POST(req: Request): Promise<Response> { | |||||
| 90 | content: m.content, | 104 | content: m.content, | ||
| 91 | })); | 105 | })); | ||
| 92 | 106 | ||||
| 107 | + // réfs persistantes (id exacts évalués) — renvoyées au client pour l'historique | ||||
| 108 | + const refs = new Map<string, string>(); | ||||
| 109 | + const collectRefs = (out: unknown) => { | ||||
| 110 | + const scan = (o: unknown) => { | ||||
| 111 | + if (!o || typeof o !== "object") return; | ||||
| 112 | + const rec = o as Record<string, unknown>; | ||||
| 113 | + const id = rec.id; | ||||
| 114 | + const adresse = rec.adresse; | ||||
| 115 | + if (typeof id === "string" && id.length >= 10 && typeof adresse === "string") { | ||||
| 116 | + refs.set(id, adresse); | ||||
| 117 | + } | ||||
| 118 | + for (const k of ["unite", "evaluation", "detail"]) { | ||||
| 119 | + const v = rec[k]; | ||||
| 120 | + if (Array.isArray(v)) v.forEach(scan); | ||||
| 121 | + else scan(v); | ||||
| 122 | + } | ||||
| 123 | + }; | ||||
| 124 | + scan(out); | ||||
| 125 | + }; | ||||
| 126 | + | ||||
| 93 | const stream = new ReadableStream({ | 127 | const stream = new ReadableStream({ | ||
| 94 | async start(controller) { | 128 | async start(controller) { | ||
| 95 | try { | 129 | try { | ||
@@ -113,6 +147,17 @@ export async function POST(req: Request): Promise<Response> { | |||||
| 113 | const final = await msgStream.finalMessage(); | 147 | const final = await msgStream.finalMessage(); | ||
| 114 | 148 | ||||
| 115 | if (final.stop_reason !== "tool_use") { | 149 | if (final.stop_reason !== "tool_use") { | ||
| 150 | + if (refs.size) { | ||||
| 151 | + sse(controller, { | ||||
| 152 | + type: "refs", | ||||
| 153 | + refs: [...refs].slice(-4).map(([id, adresse]) => ({ | ||||
| 154 | + id, | ||||
| 155 | + adresse, | ||||
| 156 | + pdf: `https://www.vrai-prix.com/api/report?id=${encodeURIComponent(id)}`, | ||||
| 157 | + pdf_pro: `https://www.vrai-prix.com/api/report/pro?id=${encodeURIComponent(id)}`, | ||||
| 158 | + })), | ||||
| 159 | + }); | ||||
| 160 | + } | ||||
| 116 | sse(controller, { type: "done" }); | 161 | sse(controller, { type: "done" }); | ||
| 117 | break; | 162 | break; | ||
| 118 | } | 163 | } | ||
@@ -129,6 +174,7 @@ export async function POST(req: Request): Promise<Response> { | |||||
| 129 | } catch (e) { | 174 | } catch (e) { | ||
| 130 | out = { erreur: `Échec de l'outil : ${e instanceof Error ? e.message : "inconnu"}` }; | 175 | out = { erreur: `Échec de l'outil : ${e instanceof Error ? e.message : "inconnu"}` }; | ||
| 131 | } | 176 | } | ||
| 177 | + collectRefs(out); | ||||
| 132 | results.push({ | 178 | results.push({ | ||
| 133 | type: "tool_result", | 179 | type: "tool_result", | ||
| 134 | tool_use_id: block.id, | 180 | tool_use_id: block.id, | ||
modified
src/app/api/report/pro/route.ts
+3 −2
@@ -5,8 +5,9 @@ import { getMarketIndex } from "@/lib/db"; | |||||
| 5 | import { buildProReport } from "@/lib/report-pro"; | 5 | import { buildProReport } from "@/lib/report-pro"; | ||
| 6 | 6 | ||||
| 7 | export async function GET(req: NextRequest) { | 7 | export async function GET(req: NextRequest) { | ||
| 8 | − const id = req.nextUrl.searchParams.get("id"); | 8 | + const rawId = req.nextUrl.searchParams.get("id"); | ||
| 9 | − if (!id) return NextResponse.json({ error: "id requis" }, { status: 400 }); | 9 | + if (!rawId) return NextResponse.json({ error: "id requis" }, { status: 400 }); | ||
| 10 | + const id = rawId.replace(/\D/g, "") || rawId; | ||||
| 10 | const data = estimateByUnitId(id); | 11 | const data = estimateByUnitId(id); | ||
| 11 | if (!data || !data.unit) | 12 | if (!data || !data.unit) | ||
| 12 | return NextResponse.json({ error: "unité introuvable" }, { status: 404 }); | 13 | return NextResponse.json({ error: "unité introuvable" }, { status: 404 }); | ||
modified
src/app/api/report/route.ts
+8 −2
@@ -4,8 +4,14 @@ import { estimateByUnitId } from "@/lib/estimator"; | |||||
| 4 | import { buildReport } from "@/lib/report"; | 4 | import { buildReport } from "@/lib/report"; | ||
| 5 | 5 | ||||
| 6 | export async function GET(req: NextRequest) { | 6 | export async function GET(req: NextRequest) { | ||
| 7 | − const id = req.nextUrl.searchParams.get("id"); | 7 | + const rawId = req.nextUrl.searchParams.get("id"); | ||
| 8 | − if (!id) return NextResponse.json({ error: "id requis" }, { status: 400 }); | 8 | + if (!rawId) return NextResponse.json({ error: "id requis" }, { status: 400 }); | ||
| 9 | + const id = rawId.replace(/\D/g, "") || rawId; // tolère un id décoré (espaces, tirets) | ||||
| 10 | + // tolère les liens du type ?format=professionnel générés par des agents | ||||
| 11 | + const format = (req.nextUrl.searchParams.get("format") ?? "").toLowerCase(); | ||||
| 12 | + if (format.startsWith("pro")) { | ||||
| 13 | + return NextResponse.redirect(new URL(`/api/report/pro?id=${encodeURIComponent(id)}`, req.url)); | ||||
| 14 | + } | ||||
| 9 | const data = estimateByUnitId(id); | 15 | const data = estimateByUnitId(id); | ||
| 10 | if (!data || !data.unit) | 16 | if (!data || !data.unit) | ||
| 11 | return NextResponse.json({ error: "unité introuvable" }, { status: 404 }); | 17 | return NextResponse.json({ error: "unité introuvable" }, { status: 404 }); | ||
modified
src/components/KaChat.tsx
+20 −1
@@ -146,6 +146,7 @@ function renderMd(text: string): React.ReactNode[] { | |||||
| 146 | } | 146 | } | ||
| 147 | flushList(); | 147 | flushList(); | ||
| 148 | if (t === "" || t === "---") continue; | 148 | if (t === "" || t === "---") continue; | ||
| 149 | + if (t.startsWith("[réf:")) continue; // réf machine pour la continuité — jamais affichée | ||||
| 149 | if (/^#{1,4}\s+/.test(t)) { | 150 | if (/^#{1,4}\s+/.test(t)) { | ||
| 150 | blocks.push( | 151 | blocks.push( | ||
| 151 | <p key={`h-${k++}`} className="vp-display mt-2 text-[15px] font-bold"> | 152 | <p key={`h-${k++}`} className="vp-display mt-2 text-[15px] font-bold"> | ||
@@ -244,7 +245,13 @@ export default function KaChat() { | |||||
| 244 | for (const ev of events) { | 245 | for (const ev of events) { | ||
| 245 | const line = ev.split("\n").find((l) => l.startsWith("data: ")); | 246 | const line = ev.split("\n").find((l) => l.startsWith("data: ")); | ||
| 246 | if (!line) continue; | 247 | if (!line) continue; | ||
| 247 | − let data: { type: string; text?: string; name?: string; message?: string }; | 248 | + let data: { | ||
| 249 | + type: string; | ||||
| 250 | + text?: string; | ||||
| 251 | + name?: string; | ||||
| 252 | + message?: string; | ||||
| 253 | + refs?: { id: string; adresse: string; pdf?: string; pdf_pro?: string }[]; | ||||
| 254 | + }; | ||||
| 248 | try { | 255 | try { | ||
| 249 | data = JSON.parse(line.slice(6)); | 256 | data = JSON.parse(line.slice(6)); | ||
| 250 | } catch { | 257 | } catch { | ||
@@ -259,6 +266,18 @@ export default function KaChat() { | |||||
| 259 | ...m, | 266 | ...m, | ||
| 260 | tools: m.tools?.includes(data.name!) ? m.tools : [...(m.tools ?? []), data.name!], | 267 | tools: m.tools?.includes(data.name!) ? m.tools : [...(m.tools ?? []), data.name!], | ||
| 261 | })); | 268 | })); | ||
| 269 | + } else if (data.type === "refs" && Array.isArray(data.refs)) { | ||||
| 270 | + const lines = ( | ||||
| 271 | + data.refs as { id: string; adresse: string; pdf?: string; pdf_pro?: string }[] | ||||
| 272 | + ) | ||||
| 273 | + .map( | ||||
| 274 | + (r) => | ||||
| 275 | + `[réf: ${r.adresse} → id ${r.id}` + | ||||
| 276 | + (r.pdf ? ` | rapport standard: ${r.pdf} | rapport pro: ${r.pdf_pro}` : "") + | ||||
| 277 | + "]" | ||||
| 278 | + ) | ||||
| 279 | + .join("\n"); | ||||
| 280 | + patch((m) => ({ ...m, content: m.content + "\n\n" + lines })); | ||||
| 262 | } else if (data.type === "error") { | 281 | } else if (data.type === "error") { | ||
| 263 | patch((m) => ({ | 282 | patch((m) => ({ | ||
| 264 | ...m, | 283 | ...m, | ||
modified
src/lib/ka/tools.ts
+35 −12
@@ -75,6 +75,30 @@ function searchFlexible( | |||||
| 75 | return { rows: [], niveau: "approximative" }; | 75 | return { rows: [], niveau: "approximative" }; | ||
| 76 | } | 76 | } | ||
| 77 | 77 | ||||
| 78 | +/** | ||||
| 79 | + * Résout un id éventuellement mal recopié par le modèle : essai brut, | ||||
| 80 | + * puis version chiffres seulement (les id provinciaux sont numériques). | ||||
| 81 | + */ | ||||
| 82 | +function resolveId(raw: unknown): string { | ||||
| 83 | + const a = String(raw ?? "").trim(); | ||||
| 84 | + if (getUnit(a)) return a; | ||||
| 85 | + const d = a.replace(/\D/g, ""); | ||||
| 86 | + if (d && d !== a && getUnit(d)) return d; | ||||
| 87 | + return a; | ||||
| 88 | +} | ||||
| 89 | + | ||||
| 90 | +const ID_ERR = | ||||
| 91 | + "Unité introuvable — l'id est peut-être mal recopié. Relance chercher_propriete et copie le champ `id` EXACTEMENT tel quel."; | ||||
| 92 | + | ||||
| 93 | +function liens(id: string) { | ||||
| 94 | + const e = encodeURIComponent(id); | ||||
| 95 | + return { | ||||
| 96 | + fiche_complete: `https://www.vrai-prix.com/estimation/${e}`, | ||||
| 97 | + rapport_standard_pdf: `https://www.vrai-prix.com/api/report?id=${e}`, | ||||
| 98 | + rapport_professionnel_pdf: `https://www.vrai-prix.com/api/report/pro?id=${e}`, | ||||
| 99 | + }; | ||||
| 100 | +} | ||||
| 101 | + | ||||
| 78 | function unitCard(u: ReturnType<typeof searchUnits>[number]) { | 102 | function unitCard(u: ReturnType<typeof searchUnits>[number]) { | ||
| 79 | return { | 103 | return { | ||
| 80 | id: u.id_provinc, | 104 | id: u.id_provinc, | ||
@@ -113,6 +137,7 @@ function evalCard(e: UnitEstimate) { | |||||
| 113 | valeur_terrain_role: u.specs.valeurTerrain, | 137 | valeur_terrain_role: u.specs.valeurTerrain, | ||
| 114 | valeur_batiment_role: u.specs.valeurBatiment, | 138 | valeur_batiment_role: u.specs.valeurBatiment, | ||
| 115 | valeur_role_anterieure: u.specs.valeurAnterieure, | 139 | valeur_role_anterieure: u.specs.valeurAnterieure, | ||
| 140 | + liens: liens(u.id), | ||||
| 116 | } | 141 | } | ||
| 117 | : null, | 142 | : null, | ||
| 118 | estimation: { | 143 | estimation: { | ||
@@ -333,14 +358,14 @@ export function runKaTool(name: string, input: J): unknown { | |||||
| 333 | } | 358 | } | ||
| 334 | 359 | ||||
| 335 | case "evaluer_propriete": { | 360 | case "evaluer_propriete": { | ||
| 336 | − const e = estimateByUnitId(String(input.id ?? "")); | 361 | + const e = estimateByUnitId(resolveId(input.id)); | ||
| 337 | − if (!e) return { erreur: "Unité introuvable — vérifier l'id avec chercher_propriete." }; | 362 | + if (!e) return { erreur: ID_ERR }; | ||
| 338 | return evalCard(e); | 363 | return evalCard(e); | ||
| 339 | } | 364 | } | ||
| 340 | 365 | ||||
| 341 | case "comparables_detailles": { | 366 | case "comparables_detailles": { | ||
| 342 | − const e = estimateByUnitId(String(input.id ?? "")); | 367 | + const e = estimateByUnitId(resolveId(input.id)); | ||
| 343 | − if (!e) return { erreur: "Unité introuvable." }; | 368 | + if (!e) return { erreur: ID_ERR }; | ||
| 344 | const max = Math.min(Number(input.max) || 8, 15); | 369 | const max = Math.min(Number(input.max) || 8, 15); | ||
| 345 | return { | 370 | return { | ||
| 346 | n_utilises: e.result.nCompsUsed, | 371 | n_utilises: e.result.nCompsUsed, | ||
@@ -423,7 +448,7 @@ export function runKaTool(name: string, input: J): unknown { | |||||
| 423 | } | 448 | } | ||
| 424 | 449 | ||||
| 425 | case "evaluer_parc": { | 450 | case "evaluer_parc": { | ||
| 426 | − const ids = (input.ids as string[] | undefined)?.slice(0, 40) ?? []; | 451 | + const ids = ((input.ids as string[] | undefined) ?? []).slice(0, 40).map(resolveId); | ||
| 427 | if (ids.length < 1) return { erreur: "Fournir au moins un id." }; | 452 | if (ids.length < 1) return { erreur: "Fournir au moins un id." }; | ||
| 428 | const p = estimatePortfolio(ids); | 453 | const p = estimatePortfolio(ids); | ||
| 429 | if (!p.items.length) return { erreur: "Aucune unité valide trouvée." }; | 454 | if (!p.items.length) return { erreur: "Aucune unité valide trouvée." }; | ||
@@ -450,7 +475,7 @@ export function runKaTool(name: string, input: J): unknown { | |||||
| 450 | } | 475 | } | ||
| 451 | 476 | ||||
| 452 | case "comparer_proprietes": { | 477 | case "comparer_proprietes": { | ||
| 453 | − const ids = (input.ids as string[] | undefined)?.slice(0, 4) ?? []; | 478 | + const ids = ((input.ids as string[] | undefined) ?? []).slice(0, 4).map(resolveId); | ||
| 454 | if (ids.length < 2) return { erreur: "Fournir 2 à 4 ids." }; | 479 | if (ids.length < 2) return { erreur: "Fournir 2 à 4 ids." }; | ||
| 455 | return { | 480 | return { | ||
| 456 | comparaison: ids.map((id) => { | 481 | comparaison: ids.map((id) => { | ||
@@ -521,7 +546,7 @@ export function runKaTool(name: string, input: J): unknown { | |||||
| 521 | } | 546 | } | ||
| 522 | 547 | ||||
| 523 | case "dossier_complet": { | 548 | case "dossier_complet": { | ||
| 524 | − const id = String(input.id ?? ""); | 549 | + const id = resolveId(input.id); | ||
| 525 | const ev = estimateByUnitId(id); | 550 | const ev = estimateByUnitId(id); | ||
| 526 | if (!ev || !ev.unit) return { erreur: "Unité introuvable — vérifier l'id avec chercher_propriete." }; | 551 | if (!ev || !ev.unit) return { erreur: "Unité introuvable — vérifier l'id avec chercher_propriete." }; | ||
| 527 | const u = ev.unit; | 552 | const u = ev.unit; | ||
@@ -575,13 +600,11 @@ export function runKaTool(name: string, input: J): unknown { | |||||
| 575 | } | 600 | } | ||
| 576 | 601 | ||||
| 577 | case "liens_rapports": { | 602 | case "liens_rapports": { | ||
| 578 | − const id = String(input.id ?? ""); | 603 | + const id = resolveId(input.id); | ||
| 579 | const u = getUnit(id); | 604 | const u = getUnit(id); | ||
| 580 | − if (!u) return { erreur: "Unité introuvable." }; | 605 | + if (!u) return { erreur: ID_ERR }; | ||
| 581 | return { | 606 | return { | ||
| 582 | − fiche_complete: `https://www.vrai-prix.com/estimation/${encodeURIComponent(id)}`, | 607 | + ...liens(id), | ||
| 583 | − rapport_standard_pdf: `https://www.vrai-prix.com/api/report?id=${encodeURIComponent(id)}`, | ||||
| 584 | − rapport_professionnel_pdf: `https://www.vrai-prix.com/api/report/pro?id=${encodeURIComponent(id)}`, | ||||
| 585 | note: "Le rapport professionnel (6 pages) est le format à présenter à une banque.", | 608 | note: "Le rapport professionnel (6 pages) est le format à présenter à une banque.", | ||
| 586 | }; | 609 | }; | ||
| 587 | } | 610 | } | ||
| 588 | 611 | ||||