diff --git a/backend/src/ai/providers/anthropic.js b/backend/src/ai/providers/anthropic.js index 9fa0bd0..0ca0903 100644 --- a/backend/src/ai/providers/anthropic.js +++ b/backend/src/ai/providers/anthropic.js @@ -161,3 +161,65 @@ export async function testConnection({ apiKey, model }) { .trim() || '(réponse vide)'; return { reply, durationMs: Date.now() - started, usage: message.usage }; } + +/** + * lookupInstitution() — recherche web automatique (outil natif Anthropic + * "web_search", exécuté côté serveur Anthropic) pour pré-remplir une fiche + * institution depuis l'admin (icône "baguette magique", InstitutionModal — + * demande Olivier 27/09/26) : au minimum une description succincte et le + * pays de domiciliation (code ISO 3166-1 alpha-2, cf. CountrySelect.jsx + * côté frontend). + * + * Contrairement à testConnection() (ping minimal, sans outil), cet appel + * n'est pas en streaming : pour un outil serveur, Anthropic exécute la/les + * recherche(s) et le tour de lecture des résultats en interne, la réponse + * finale (texte, éventuellement précédée de blocs server_tool_use / + * web_search_tool_result) arrivant en un seul message — donc pas de boucle + * côté client comme dans streamChat(), mais un appel qui peut prendre + * plusieurs secondes. + * + * @param {object} opts + * @param {string} opts.apiKey + * @param {string} opts.model + * @param {string} opts.nom Nom de l'institution à rechercher + * @param {string} [opts.url] URL du site officiel, si déjà connue (aide la recherche) + * @returns {Promise<{description:string, domiciliation:string|null, durationMs:number, sourcesCount:number}>} + */ +export async function lookupInstitution({ apiKey, model, nom, url }) { + const anthropic = new Anthropic({ apiKey }); + const started = Date.now(); + + const prompt = [ + `Recherche sur le web des informations fiables et à jour sur l'institution financière suivante : "${nom}"${url ? ` (site officiel présumé : ${url})` : ''}.`, + `C'est potentiellement une banque, néobanque, courtier, assureur, société de gestion ou établissement de paiement.`, + `Réponds UNIQUEMENT avec un objet JSON valide, sans aucun texte autour, au format exact :`, + `{"description": "", "domiciliation": ""}`, + ].join('\n'); + + const message = await anthropic.messages.create({ + model, + max_tokens: 1024, + tools: [{ type: 'web_search_20260318', name: 'web_search', max_uses: 4 }], + messages: [{ role: 'user', content: prompt }], + }); + + const textBlocks = (message.content || []).filter((b) => b.type === 'text').map((b) => b.text); + const fullText = textBlocks.join('\n').trim(); + const sourcesCount = (message.content || []).filter((b) => b.type === 'web_search_tool_result').length; + + let parsed; + try { + const jsonMatch = fullText.match(/\{[\s\S]*\}/); + parsed = JSON.parse(jsonMatch ? jsonMatch[0] : fullText); + } catch (e) { + throw new Error(`Réponse du modèle non exploitable (JSON attendu) : ${fullText.slice(0, 200) || '(vide)'}`); + } + + const description = typeof parsed.description === 'string' ? parsed.description.trim() : ''; + let domiciliation = typeof parsed.domiciliation === 'string' ? parsed.domiciliation.trim().toUpperCase() : null; + if (domiciliation && !/^[A-Z]{2}$/.test(domiciliation)) domiciliation = null; + + if (!description) throw new Error("Le modèle n'a renvoyé aucune description exploitable."); + + return { description, domiciliation, durationMs: Date.now() - started, sourcesCount }; +} diff --git a/backend/src/routes/institutionsReferentiel.js b/backend/src/routes/institutionsReferentiel.js index 03086a4..9f54da1 100644 --- a/backend/src/routes/institutionsReferentiel.js +++ b/backend/src/routes/institutionsReferentiel.js @@ -7,6 +7,10 @@ import { fileURLToPath } from 'url'; import sharp from 'sharp'; import db from '../db/index.js'; import { HttpError } from '../middleware/errorHandler.js'; +import { getDefaultActiveProvider } from '../ai/providerConfig.js'; +import { decryptSecret } from '../ai/crypto.js'; +import { lookupInstitution } from '../ai/providers/anthropic.js'; +import { audit } from '../utils/audit.js'; /** * routes/institutionsReferentiel.js — Référentiel admin des institutions financières @@ -281,4 +285,68 @@ router.delete('/:id/icone', (req, res, next) => { } catch (e) { next(e); } }); + +// ── POST /api/institutions-referentiel/ai-lookup ──────────────────────────── +// Icône "baguette magique" d'InstitutionModal (demande Olivier 27/09/26) : recherche +// web automatique via le connecteur IA (Admin > Intelligence artificielle), pour +// pré-remplir au minimum description + domiciliation depuis le nom (et l'URL si +// déjà saisie) tapés dans le formulaire — fonctionne aussi bien en création (avant +// tout enregistrement) qu'en édition. Réponse toujours 200 : { ok:false, error } +// n'est pas une erreur HTTP mais un échec applicatif (IA désactivée, aucun +// fournisseur actif, type de fournisseur non pris en charge...) — même précédent +// que POST /providers/:id/test dans adminAi.js. +const AiLookupSchema = z.object({ + nom: z.string().min(1).max(200), + url: z.string().url().optional().or(z.literal('')).nullable(), +}); + +router.post('/ai-lookup', async (req, res, next) => { + try { + const { nom, url } = AiLookupSchema.parse(req.body); + + const config = db.prepare('SELECT actif FROM ai_config WHERE id = 1').get(); + if (!config?.actif) { + return res.json({ ok: false, error: "L'Assistant IA n'est pas activé (Admin > Intelligence artificielle)." }); + } + + const provider = getDefaultActiveProvider(); + if (!provider) { + return res.json({ ok: false, error: "Aucun fournisseur IA actif n'est configuré." }); + } + // Seul le fournisseur "anthropic" (natif) expose l'outil de recherche web + // serveur utilisé par lookupInstitution() — un fournisseur openai_compatible + // n'a aucune garantie d'accès web réel selon le modèle/service derrière + // base_url, on préfère donc refuser explicitement plutôt que de renvoyer une + // réponse non vérifiée présentée comme une recherche internet. + if (provider.type !== 'anthropic') { + return res.json({ + ok: false, + error: `La recherche web automatique n'est prise en charge que pour un fournisseur Anthropic (natif) — le fournisseur actif ("${provider.nom}") est de type "${provider.type}".`, + }); + } + + let apiKey; + try { + apiKey = decryptSecret(provider.api_key_chiffree); + } catch (e) { + throw new HttpError(500, `Impossible de déchiffrer la clé API : ${e.message}`); + } + + try { + const result = await lookupInstitution({ apiKey, model: provider.modele, nom, url: url || undefined }); + audit(req, { + action: 'institution_ai_lookup', category: 'ai', actorId: req.user.id, + details: { nom, ok: true, dureeMs: result.durationMs, sourcesCount: result.sourcesCount }, + }); + res.json({ ok: true, description: result.description, domiciliation: result.domiciliation, dureeMs: result.durationMs }); + } catch (e) { + audit(req, { + action: 'institution_ai_lookup', category: 'ai', actorId: req.user.id, + details: { nom, ok: false, error: e.message }, + }); + res.json({ ok: false, error: e.message }); + } + } catch (e) { next(e); } +}); + export default router; diff --git a/design/logos institution/Abaille assurances -Logo.png b/design/logos institution/Abaille assurances -Logo.png new file mode 100644 index 0000000..4293df8 Binary files /dev/null and b/design/logos institution/Abaille assurances -Logo.png differ diff --git a/design/logos institution/Amundi_logo.svg b/design/logos institution/Amundi_logo.svg new file mode 100644 index 0000000..a33607e --- /dev/null +++ b/design/logos institution/Amundi_logo.svg @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/design/logos institution/Axa France - Logo.jpg b/design/logos institution/Axa France - Logo.jpg new file mode 100644 index 0000000..df9e6c3 Binary files /dev/null and b/design/logos institution/Axa France - Logo.jpg differ diff --git a/design/logos institution/BNP Asset Managment - Logo.png b/design/logos institution/BNP Asset Managment - Logo.png new file mode 100644 index 0000000..2eaf2f0 Binary files /dev/null and b/design/logos institution/BNP Asset Managment - Logo.png differ diff --git a/design/logos institution/BNP Cardif.webp b/design/logos institution/BNP Cardif.webp new file mode 100644 index 0000000..c452580 Binary files /dev/null and b/design/logos institution/BNP Cardif.webp differ diff --git a/design/logos institution/Bourse direct - Logo.webp b/design/logos institution/Bourse direct - Logo.webp new file mode 100644 index 0000000..a06cac2 Binary files /dev/null and b/design/logos institution/Bourse direct - Logo.webp differ diff --git a/design/logos institution/Boursorama_logo.png b/design/logos institution/Boursorama_logo.png new file mode 100644 index 0000000..6789cf3 Binary files /dev/null and b/design/logos institution/Boursorama_logo.png differ diff --git a/design/logos institution/Carmingac_Logo.png b/design/logos institution/Carmingac_Logo.png new file mode 100644 index 0000000..929ea05 Binary files /dev/null and b/design/logos institution/Carmingac_Logo.png differ diff --git a/design/logos institution/Credit-Agricole Assurance-Logo.png b/design/logos institution/Credit-Agricole Assurance-Logo.png new file mode 100644 index 0000000..f2cf0a4 Binary files /dev/null and b/design/logos institution/Credit-Agricole Assurance-Logo.png differ diff --git a/design/logos institution/Credit-Agricole-Logo.png b/design/logos institution/Credit-Agricole-Logo.png new file mode 100644 index 0000000..7b6f52b Binary files /dev/null and b/design/logos institution/Credit-Agricole-Logo.png differ diff --git a/design/logos institution/Credit-Mutuel-Logo.png b/design/logos institution/Credit-Mutuel-Logo.png new file mode 100644 index 0000000..e0f86cf Binary files /dev/null and b/design/logos institution/Credit-Mutuel-Logo.png differ diff --git a/design/logos institution/Crédit_Commercial_de_France_Logo.png b/design/logos institution/Crédit_Commercial_de_France_Logo.png new file mode 100644 index 0000000..698636c Binary files /dev/null and b/design/logos institution/Crédit_Commercial_de_France_Logo.png differ diff --git a/design/logos institution/Degiro - logo.webp b/design/logos institution/Degiro - logo.webp new file mode 100644 index 0000000..db354cc Binary files /dev/null and b/design/logos institution/Degiro - logo.webp differ diff --git a/design/logos institution/Easybourse.webp b/design/logos institution/Easybourse.webp new file mode 100644 index 0000000..4f09365 Binary files /dev/null and b/design/logos institution/Easybourse.webp differ diff --git a/design/logos institution/Generali Logo.png b/design/logos institution/Generali Logo.png new file mode 100644 index 0000000..116c809 Binary files /dev/null and b/design/logos institution/Generali Logo.png differ diff --git a/design/logos institution/Groupama Logo.png b/design/logos institution/Groupama Logo.png new file mode 100644 index 0000000..32e8713 Binary files /dev/null and b/design/logos institution/Groupama Logo.png differ diff --git a/design/logos institution/HSBC-Logo.png b/design/logos institution/HSBC-Logo.png new file mode 100644 index 0000000..d1f83bb Binary files /dev/null and b/design/logos institution/HSBC-Logo.png differ diff --git a/design/logos institution/LOGO-EIFFEL INVESTISSEMENT.jpg b/design/logos institution/LOGO-EIFFEL INVESTISSEMENT.jpg new file mode 100644 index 0000000..929c170 Binary files /dev/null and b/design/logos institution/LOGO-EIFFEL INVESTISSEMENT.jpg differ diff --git a/design/logos institution/La Financière de l'echiquier.png b/design/logos institution/La Financière de l'echiquier.png new file mode 100644 index 0000000..77e01be Binary files /dev/null and b/design/logos institution/La Financière de l'echiquier.png differ diff --git a/design/logos institution/Lazard Frères gestion Logo.jpg b/design/logos institution/Lazard Frères gestion Logo.jpg new file mode 100644 index 0000000..bbc9e1c Binary files /dev/null and b/design/logos institution/Lazard Frères gestion Logo.jpg differ diff --git a/design/logos institution/Linxea Logo.png b/design/logos institution/Linxea Logo.png new file mode 100644 index 0000000..d91913a Binary files /dev/null and b/design/logos institution/Linxea Logo.png differ diff --git a/design/logos institution/Logo Spirica.webp b/design/logos institution/Logo Spirica.webp new file mode 100644 index 0000000..11fc052 Binary files /dev/null and b/design/logos institution/Logo Spirica.webp differ diff --git a/design/logos institution/Logo-MAIF.png b/design/logos institution/Logo-MAIF.png new file mode 100644 index 0000000..2849204 Binary files /dev/null and b/design/logos institution/Logo-MAIF.png differ diff --git a/design/logos institution/Lydia Logo.webp b/design/logos institution/Lydia Logo.webp new file mode 100644 index 0000000..ba91c27 Binary files /dev/null and b/design/logos institution/Lydia Logo.webp differ diff --git a/design/logos institution/N26 - Logo.webp b/design/logos institution/N26 - Logo.webp new file mode 100644 index 0000000..228f110 Binary files /dev/null and b/design/logos institution/N26 - Logo.webp differ diff --git a/design/logos institution/Natixis Asset Managment Logo.png b/design/logos institution/Natixis Asset Managment Logo.png new file mode 100644 index 0000000..47b5147 Binary files /dev/null and b/design/logos institution/Natixis Asset Managment Logo.png differ diff --git a/design/logos institution/Nickel Logo.webp b/design/logos institution/Nickel Logo.webp new file mode 100644 index 0000000..6294b29 Binary files /dev/null and b/design/logos institution/Nickel Logo.webp differ diff --git a/design/logos institution/Paypal Logo.png b/design/logos institution/Paypal Logo.png new file mode 100644 index 0000000..0c0f498 Binary files /dev/null and b/design/logos institution/Paypal Logo.png differ diff --git a/design/logos institution/Qonto Logo.svg b/design/logos institution/Qonto Logo.svg new file mode 100644 index 0000000..9150099 --- /dev/null +++ b/design/logos institution/Qonto Logo.svg @@ -0,0 +1,90 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/design/logos institution/Qonto-logo-nouveau.png b/design/logos institution/Qonto-logo-nouveau.png new file mode 100644 index 0000000..467355c Binary files /dev/null and b/design/logos institution/Qonto-logo-nouveau.png differ diff --git a/design/logos institution/Sogecap - Logo.jpg b/design/logos institution/Sogecap - Logo.jpg new file mode 100644 index 0000000..3fd80e4 Binary files /dev/null and b/design/logos institution/Sogecap - Logo.jpg differ diff --git a/design/logos institution/allianz logo.png b/design/logos institution/allianz logo.png new file mode 100644 index 0000000..af85630 Binary files /dev/null and b/design/logos institution/allianz logo.png differ diff --git a/design/logos institution/cnp-assurances - logo.png b/design/logos institution/cnp-assurances - logo.png new file mode 100644 index 0000000..c6d5703 Binary files /dev/null and b/design/logos institution/cnp-assurances - logo.png differ diff --git a/design/logos institution/interactive-brokers-vector-logo.png b/design/logos institution/interactive-brokers-vector-logo.png new file mode 100644 index 0000000..87798da Binary files /dev/null and b/design/logos institution/interactive-brokers-vector-logo.png differ diff --git a/design/logos institution/la-banque-postale-logo.jpg b/design/logos institution/la-banque-postale-logo.jpg new file mode 100644 index 0000000..3de1e55 Binary files /dev/null and b/design/logos institution/la-banque-postale-logo.jpg differ diff --git a/design/logos institution/logo tikehau-capital.webp b/design/logos institution/logo tikehau-capital.webp new file mode 100644 index 0000000..deb33dd Binary files /dev/null and b/design/logos institution/logo tikehau-capital.webp differ diff --git a/design/logos institution/logo-macif.png b/design/logos institution/logo-macif.png new file mode 100644 index 0000000..6a099d6 Binary files /dev/null and b/design/logos institution/logo-macif.png differ diff --git a/design/logos institution/logo_-_suravenir.png b/design/logos institution/logo_-_suravenir.png new file mode 100644 index 0000000..63d1466 Binary files /dev/null and b/design/logos institution/logo_-_suravenir.png differ diff --git a/design/logos institution/logo_grisbee.webp b/design/logos institution/logo_grisbee.webp new file mode 100644 index 0000000..80c6a31 Binary files /dev/null and b/design/logos institution/logo_grisbee.webp differ diff --git a/design/logos institution/saxo-bank-logo.png b/design/logos institution/saxo-bank-logo.png new file mode 100644 index 0000000..1cf06d2 Binary files /dev/null and b/design/logos institution/saxo-bank-logo.png differ diff --git a/design/logos institution/swisslife Logo.jpg b/design/logos institution/swisslife Logo.jpg new file mode 100644 index 0000000..90d8906 Binary files /dev/null and b/design/logos institution/swisslife Logo.jpg differ diff --git a/design/logos institution/trade-republic-logo-png.png b/design/logos institution/trade-republic-logo-png.png new file mode 100644 index 0000000..2a0be3c Binary files /dev/null and b/design/logos institution/trade-republic-logo-png.png differ diff --git a/design/logos institution/wise-logo.png b/design/logos institution/wise-logo.png new file mode 100644 index 0000000..484d030 Binary files /dev/null and b/design/logos institution/wise-logo.png differ diff --git a/frontend/src/pages/AdminInstitutions.jsx b/frontend/src/pages/AdminInstitutions.jsx index 8fc76d2..b14948e 100644 --- a/frontend/src/pages/AdminInstitutions.jsx +++ b/frontend/src/pages/AdminInstitutions.jsx @@ -46,6 +46,10 @@ function IconChevronLeft() { return ; } +function IconWand() { + return ; +} + /* ── Modale de création/édition d'une institution ──────────────── Formulaire simple à un seul écran (contrairement au RefModal des plateformes, à 5 étapes) : une institution financière n'a qu'une poignée de champs, un wizard serait disproportionné. */ @@ -54,6 +58,9 @@ function InstitutionModal({ row, onClose, onSaved }) { const [categories, setCategories] = useState([]); const [err, setErr] = useState(null); const [saving, setSaving] = useState(false); + const [aiAvailable, setAiAvailable] = useState(false); + const [aiLoading, setAiLoading] = useState(false); + const [aiMsg, setAiMsg] = useState(null); // { ok:boolean, text:string } const [form, setForm] = useState({ nom: row?.nom || '', @@ -67,8 +74,40 @@ function InstitutionModal({ row, onClose, onSaved }) { api.get('/ref-categories-institutions').then(setCategories).catch(() => {}); }, []); + // Icône « baguette magique » (demande Olivier 27/09/26) : visible seulement si le + // connecteur IA est activé (Admin > Intelligence artificielle) — la disponibilité d'un + // fournisseur compatible (Anthropic natif) est vérifiée côté serveur lors de l'appel. + useEffect(() => { + api.get('/admin/ai/config').then(cfg => setAiAvailable(!!cfg?.actif)).catch(() => setAiAvailable(false)); + }, []); + const set = (k, v) => setForm(f => ({ ...f, [k]: v })); + const handleAiLookup = async () => { + if (!form.nom.trim()) { setAiMsg({ ok: false, text: 'Renseignez le nom avant de lancer la recherche.' }); return; } + setAiLoading(true); setAiMsg(null); + try { + const r = await api.post('/institutions-referentiel/ai-lookup', { + nom: form.nom.trim(), + url: form.url.trim() || undefined, + }); + if (r.ok) { + setForm(f => ({ + ...f, + description: r.description || f.description, + domiciliation: r.domiciliation || f.domiciliation, + })); + setAiMsg({ ok: true, text: "Description et domiciliation renseignées automatiquement — vérifiez avant d'enregistrer." }); + } else { + setAiMsg({ ok: false, text: r.error || 'Recherche IA indisponible.' }); + } + } catch (ex) { + setAiMsg({ ok: false, text: ex.message }); + } finally { + setAiLoading(false); + } + }; + const handleSubmit = async (e) => { e.preventDefault(); if (!form.nom.trim()) { setErr('Le nom est requis.'); return; } @@ -113,6 +152,21 @@ function InstitutionModal({ row, onClose, onSaved }) { set('url', e.target.value)} placeholder="https://…" /> + {aiAvailable && ( +
+ + {aiMsg && ( + {aiMsg.text} + )} +
+ )}
set('domiciliation', code)} showCode />