From 2afb61033adc24831ebb3f7fda3b68af4ebd3800 Mon Sep 17 00:00:00 2001 From: Hugo Dorus Date: Tue, 9 Jun 2026 10:37:37 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20add=20Judilibre,=20BOFIP=20and=20BODACC?= =?UTF-8?q?=20legal=20connectors=20Add=20three=20new=20legal=20data=20sour?= =?UTF-8?q?ces=20accessible=20as=20AI=20tools:=20-=20Judilibre=20(search?= =?UTF-8?q?=20+=20get=20decision):=20case=20law=20from=20Cour=20de=20cassa?= =?UTF-8?q?tion=20=20=20and=20courts=20of=20appeal=20via=20PISTE=20(reuses?= =?UTF-8?q?=20existing=20piste=20connector=20credentials)=20-=20BOFIP:=20F?= =?UTF-8?q?rench=20tax=20doctrine=20(Bulletin=20Officiel=20des=20Finances?= =?UTF-8?q?=20Publiques)=20=20=20via=20PISTE/L=C3=A9gifrance=20CIRC=20fund?= =?UTF-8?q?=20-=20BODACC:=20commercial=20announcements=20(company=20creati?= =?UTF-8?q?on,=20modification,=20=20=20liquidation,=20collective=20proceed?= =?UTF-8?q?ings)=20via=20open=20data=20API=20(no=20auth=20required)=20Each?= =?UTF-8?q?=20connector=20follows=20the=20existing=20ToolResult=20envelope?= =?UTF-8?q?=20pattern=20and=20includes=20unit=20tests=20with=20mocked=20HT?= =?UTF-8?q?TP=20calls.=20All=20220=20tests=20pass.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 513 +++++++++++++++++++++++++++ src/app/(app)/chat/tool-meta.ts | 7 + src/lib/connectors/bodacc.ts | 60 ++++ src/lib/connectors/bofip.ts | 91 +++++ src/lib/connectors/catalog.ts | 4 +- src/lib/connectors/judilibre.test.ts | 121 +++++++ src/lib/connectors/judilibre.ts | 138 +++++++ src/lib/connectors/opendata.test.ts | 64 ++++ src/lib/connectors/opendatasoft.ts | 60 ++++ src/lib/connectors/piste.ts | 72 +++- src/lib/connectors/tools.ts | 90 +++++ 11 files changed, 1217 insertions(+), 3 deletions(-) create mode 100644 src/lib/connectors/bodacc.ts create mode 100644 src/lib/connectors/bofip.ts create mode 100644 src/lib/connectors/judilibre.test.ts create mode 100644 src/lib/connectors/judilibre.ts create mode 100644 src/lib/connectors/opendata.test.ts create mode 100644 src/lib/connectors/opendatasoft.ts diff --git a/package-lock.json b/package-lock.json index 792ffbd..fb29733 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11578,6 +11578,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -19171,6 +19172,474 @@ } } }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/vitest/node_modules/@vitest/mocker": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.7.tgz", @@ -19198,6 +19667,50 @@ } } }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, "node_modules/vitest/node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", diff --git a/src/app/(app)/chat/tool-meta.ts b/src/app/(app)/chat/tool-meta.ts index 65d5dfe..dfc4ef4 100644 --- a/src/app/(app)/chat/tool-meta.ts +++ b/src/app/(app)/chat/tool-meta.ts @@ -8,6 +8,9 @@ import { IconBuilding, IconHistory, IconTool, + IconGavel, + IconReceipt, + IconBookmark, type Icon, } from "@tabler/icons-react"; @@ -30,6 +33,10 @@ const META: Record = { read_document: { icon: IconBook2, chip: "lecture", category: "lecture", primary: false }, list_documents: { icon: IconList, chip: "lecture", category: "lecture", primary: false }, legifrance_search: { icon: IconScale, chip: "Légifrance", category: "recherche", primary: false }, + judilibre_search: { icon: IconGavel, chip: "Judilibre", category: "recherche", primary: false }, + judilibre_decision: { icon: IconGavel, chip: "Judilibre", category: "recherche", primary: false }, + bofip_search: { icon: IconBookmark, chip: "BOFIP", category: "recherche", primary: false }, + bodacc_search: { icon: IconReceipt, chip: "BODACC", category: "recherche", primary: false }, pappers_search: { icon: IconBuilding, chip: "Pappers", category: "recherche", primary: false }, pappers_get: { icon: IconBuilding, chip: "Pappers", category: "recherche", primary: false }, search_conversation_history: { icon: IconHistory, chip: "historique", category: "recherche", primary: false }, diff --git a/src/lib/connectors/bodacc.ts b/src/lib/connectors/bodacc.ts new file mode 100644 index 0000000..e5da4ed --- /dev/null +++ b/src/lib/connectors/bodacc.ts @@ -0,0 +1,60 @@ +import { odsSearch } from "./opendatasoft"; +import { runTool, toolOk, type ToolResult } from "@/lib/tools/result"; + +const BODACC_BASE = "https://bodacc-datadila.opendatasoft.com"; +const DATASET_ID = "annonces-commerciales"; + +export type BodaccHit = { + id: string; + type: string; + commercant: string; + date: string; + tribunal: string; + registre: string; + ville: string; + url: string; +}; + +type BodaccRecord = Record; + +function escapeOdsql(value: string): string { + return value.replace(/'/g, "''").replace(/\\/g, "\\\\"); +} + +export async function bodaccSearch( + query: string, + opts?: { departement?: string; famille?: string; date_start?: string; date_end?: string } +): Promise> { + return runTool(async () => { + const whereParts: string[] = []; + if (opts?.date_start) whereParts.push(`dateparution >= '${escapeOdsql(opts.date_start)}'`); + if (opts?.date_end) whereParts.push(`dateparution <= '${escapeOdsql(opts.date_end)}'`); + if (opts?.departement) whereParts.push(`numerodepartement = '${escapeOdsql(opts.departement)}'`); + if (opts?.famille) whereParts.push(`familleavis_lib like '${escapeOdsql(opts.famille)}'`); + + const r = await odsSearch(BODACC_BASE, DATASET_ID, { + q: query, + where: whereParts.length > 0 ? whereParts.join(" AND ") : undefined, + orderBy: "dateparution desc", + limit: 5, + }, "BODACC"); + + if (!r.ok) return r; + + const hits: BodaccHit[] = (r.data.results ?? []).slice(0, 5).map((raw) => { + const rec = raw as BodaccRecord; + return { + id: `BODACC-${rec.numeroannonce || ""}`, + type: rec.familleavis_lib || rec.typeavis_lib || "Annonce", + commercant: rec.commercant || "N/A", + date: rec.dateparution || "", + tribunal: rec.tribunal || "", + registre: rec.registre || "", + ville: `${rec.cp || ""} ${rec.ville || ""}`.trim(), + url: rec.url_complete || "https://www.bodacc.fr/pages/annonces-commerciales/", + }; + }); + + return toolOk({ query, hits, total: r.data.total_count ?? 0 }); + }); +} diff --git a/src/lib/connectors/bofip.ts b/src/lib/connectors/bofip.ts new file mode 100644 index 0000000..60fd66d --- /dev/null +++ b/src/lib/connectors/bofip.ts @@ -0,0 +1,91 @@ +import { pistePost } from "./piste"; +import { runTool, toolOk, type ToolResult } from "@/lib/tools/result"; + +export type BofipHit = { + id: string; + title: string; + nature: string; + date: string; + nor: string; + url: string; + excerpt: string; +}; + +/** + * Recherche dans la doctrine fiscale (BOFIP) via la sous-API Légifrance/PISTE + * avec fond=CIRC (circulaires et instructions ministérielles). + */ +export async function bofipSearch( + userId: string, + query: string, + opts?: { limit?: number } +): Promise> { + return runTool(async () => { + const pageSize = Math.min(opts?.limit ?? 5, 10); + + type Raw = { + results?: Array<{ + id?: string; + cid?: string; + titre?: string; + nature?: string; + dateDebut?: string; + dateTexte?: string; + nor?: string; + origin?: string; + texte?: string; + sections?: Array<{ extracts?: Array<{ values?: string[] }> }>; + }>; + totalResultNumber?: number; + }; + + const r = await pistePost( + userId, + "/dila/legifrance/lf-engine-app/search", + { + fond: "CIRC", + recherche: { + champs: [ + { + typeChamp: "ALL", + operateur: "ET", + criteres: [ + { valeur: query, operateur: "ET", typeRecherche: "UN_DES_MOTS" }, + ], + }, + ], + filtres: [], + pageNumber: 1, + pageSize, + sort: "PERTINENCE", + typePagination: "DEFAUT", + operateur: "ET", + }, + }, + "BOFIP" + ); + + if (!r.ok) return r; + + const hits: BofipHit[] = (r.data.results ?? []).slice(0, pageSize).map((row) => { + const id = row.id ?? row.cid ?? ""; + return { + id, + title: row.titre ?? "Document fiscal", + nature: row.nature ?? "", + date: row.dateDebut ?? row.dateTexte ?? "", + nor: row.nor ?? "", + url: id + ? `https://www.legifrance.gouv.fr/circulaire/id/${id}` + : "https://bofip.impots.gouv.fr", + excerpt: ( + row.texte ?? + row.sections?.[0]?.extracts?.[0]?.values?.[0] ?? + "" + ).slice(0, 280), + }; + }); + + return toolOk({ query, hits, total: r.data.totalResultNumber ?? 0 }); + }); +} diff --git a/src/lib/connectors/catalog.ts b/src/lib/connectors/catalog.ts index 4c628bc..d775133 100644 --- a/src/lib/connectors/catalog.ts +++ b/src/lib/connectors/catalog.ts @@ -46,8 +46,8 @@ export const CONNECTOR_CATALOG: Record = { // Seul Légifrance est réellement câblé (lib/connectors/tools.ts). Les // autres sous-APIs PISTE sont annoncées « à venir » plutôt que prétendues // débloquées. - unlocks: ["Légifrance"], - comingSoon: ["Judilibre", "JADE", "INPI", "BODACC"], + unlocks: ["Légifrance", "Judilibre", "BOFIP"], + comingSoon: ["JADE"], credentialFields: [ { name: "client_id", diff --git a/src/lib/connectors/judilibre.test.ts b/src/lib/connectors/judilibre.test.ts new file mode 100644 index 0000000..d56aa07 --- /dev/null +++ b/src/lib/connectors/judilibre.test.ts @@ -0,0 +1,121 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +const mockFetch = vi.fn(); +vi.stubGlobal("fetch", mockFetch); + +vi.mock("./runtime", () => ({ + loadConnectorCredentials: vi.fn().mockResolvedValue({ + key: { id: "test-key" }, + credentials: { client_id: "test-id", client_secret: "test-secret" }, + }), + listActiveConnectorTypes: vi.fn().mockResolvedValue(["piste"]), +})); + +describe("judilibre", () => { + beforeEach(() => { + mockFetch.mockReset(); + vi.resetModules(); + }); + + describe("judilibreSearch", () => { + it("construit la bonne URL GET avec les paramètres de recherche", async () => { + // OAuth token response + mockFetch.mockImplementation(async (url: string) => { + if (url.includes("oauth.piste.gouv.fr")) { + return { + ok: true, + json: async () => ({ access_token: "tok-123", expires_in: 300 }), + }; + } + if (url.includes("judilibre")) { + return { + ok: true, + json: async () => ({ + results: [ + { + id: "dec-001", + number: "21-12.345", + ecli: "ECLI:FR:CCASS:2024:CO00123", + formation: "FP", + solution: "Cassation", + decision_date: "2024-03-15", + jurisdiction: "Cour de cassation", + chamber: "Chambre commerciale", + themes: ["contrats", "responsabilité"], + summary: "Résumé de la décision...", + text: "", + }, + ], + total: 42, + next_page: null, + }), + }; + } + return { ok: false, status: 404 }; + }); + + const { judilibreSearch } = await import("./judilibre"); + const result = await judilibreSearch("user-1", "rupture brutale", { + jurisdiction: "cc", + }); + + expect(result.ok).toBe(true); + if (!result.ok) return; + + expect(result.data.hits).toHaveLength(1); + expect(result.data.hits[0].ecli).toBe("ECLI:FR:CCASS:2024:CO00123"); + expect(result.data.hits[0].solution).toBe("Cassation"); + expect(result.data.total).toBe(42); + + // Vérifie que le 2e appel est un GET vers Judilibre + const judilibreCall = mockFetch.mock.calls.find((c) => + (c[0] as string).includes("judilibre") + ); + expect(judilibreCall).toBeDefined(); + expect(judilibreCall![0]).toContain("/cassation/judilibre/v1.0/search"); + expect(judilibreCall![0]).toContain("query=rupture+brutale"); + expect(judilibreCall![0]).toContain("jurisdiction=cc"); + expect(judilibreCall![1].method).toBe("GET"); + }); + }); + + describe("judilibreGetDecision", () => { + it("récupère une décision par ID", async () => { + mockFetch.mockImplementation(async (url: string) => { + if (url.includes("oauth.piste.gouv.fr")) { + return { + ok: true, + json: async () => ({ access_token: "tok-456", expires_in: 300 }), + }; + } + if (url.includes("judilibre")) { + return { + ok: true, + json: async () => ({ + id: "dec-001", + number: "21-12.345", + ecli: "ECLI:FR:CCASS:2024:CO00123", + formation: "FP", + solution: "Rejet", + decision_date: "2024-06-01", + jurisdiction: "Cour de cassation", + chamber: "Chambre sociale", + themes: ["licenciement"], + summary: "Attendu que...", + text: "LA COUR DE CASSATION...", + }), + }; + } + return { ok: false, status: 404 }; + }); + + const { judilibreGetDecision } = await import("./judilibre"); + const result = await judilibreGetDecision("user-1", "dec-001"); + + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.data.decision.text).toContain("LA COUR DE CASSATION"); + expect(result.data.decision.url).toContain("dec-001"); + }); + }); +}); diff --git a/src/lib/connectors/judilibre.ts b/src/lib/connectors/judilibre.ts new file mode 100644 index 0000000..30224dd --- /dev/null +++ b/src/lib/connectors/judilibre.ts @@ -0,0 +1,138 @@ +import { pisteGet } from "./piste"; +import { runTool, toolOk, type ToolResult } from "@/lib/tools/result"; + +const JUDILIBRE_BASE = "/cassation/judilibre/v1.0"; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export type JudilibreDecision = { + id: string; + number: string; + ecli: string; + formation: string; + solution: string; + decision_date: string; + jurisdiction: string; + chamber: string; + themes: string[]; + summary: string; + text: string; +}; + +type JudilibreSearchRaw = { + results: JudilibreDecision[]; + total: number; + next_page: string | null; +}; + +export type JudilibreHit = { + id: string; + ecli: string; + title: string; + date: string; + solution: string; + chamber: string; + themes: string[]; + summary: string; + url: string; +}; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function judilibreUrl(id: string): string { + return `https://www.courdecassation.fr/decision/${id}`; +} + +function buildSearchParams(opts: { + query: string; + jurisdiction?: string; + chamber?: string; + date_start?: string; + date_end?: string; + sort?: string; + page_size?: number; +}): string { + const params = new URLSearchParams(); + if (opts.query) params.set("query", opts.query); + if (opts.jurisdiction) params.set("jurisdiction", opts.jurisdiction); + if (opts.chamber) params.set("chamber", opts.chamber); + if (opts.date_start) params.set("date_start", opts.date_start); + if (opts.date_end) params.set("date_end", opts.date_end); + params.set("sort", opts.sort ?? "score"); + params.set("order", "desc"); + params.set("page_size", String(Math.min(opts.page_size ?? 5, 50))); + return params.toString(); +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +export async function judilibreSearch( + userId: string, + query: string, + opts?: { + jurisdiction?: string; + chamber?: string; + date_start?: string; + date_end?: string; + } +): Promise> { + return runTool(async () => { + const qs = buildSearchParams({ query, ...opts, page_size: 5 }); + const r = await pisteGet( + userId, + `${JUDILIBRE_BASE}/search?${qs}`, + "Judilibre" + ); + if (!r.ok) return r; + + const hits: JudilibreHit[] = (r.data.results ?? []).slice(0, 5).map((d) => ({ + id: d.id, + ecli: d.ecli, + title: `${d.jurisdiction} ${d.chamber} — ${d.number}`, + date: d.decision_date, + solution: d.solution, + chamber: d.chamber, + themes: d.themes?.slice(0, 3) ?? [], + summary: d.summary?.slice(0, 400) ?? "", + url: judilibreUrl(d.id), + })); + + return toolOk({ query, hits, total: r.data.total }); + }); +} + +export async function judilibreGetDecision( + userId: string, + decisionId: string +): Promise> { + return runTool(async () => { + const r = await pisteGet( + userId, + `${JUDILIBRE_BASE}/decision?id=${encodeURIComponent(decisionId)}`, + "Judilibre" + ); + if (!r.ok) return r; + + const d = r.data; + return toolOk({ + decision: { + id: d.id, + ecli: d.ecli, + title: `${d.jurisdiction} ${d.chamber} — ${d.number}`, + date: d.decision_date, + solution: d.solution, + chamber: d.chamber, + themes: d.themes ?? [], + summary: d.summary ?? "", + url: judilibreUrl(d.id), + text: d.text?.slice(0, 8000) ?? "", + }, + }); + }); +} diff --git a/src/lib/connectors/opendata.test.ts b/src/lib/connectors/opendata.test.ts new file mode 100644 index 0000000..4577001 --- /dev/null +++ b/src/lib/connectors/opendata.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +const mockFetch = vi.fn(); +vi.stubGlobal("fetch", mockFetch); + +describe("bodacc", () => { + beforeEach(() => { + mockFetch.mockReset(); + }); + + describe("bodaccSearch", () => { + it("interroge l'API OpenDataSoft BODACC et normalise les résultats", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + total_count: 3, + results: [ + { + numeroannonce: "A-2024-001", + familleavis_lib: "Procédure collective", + typeavis_lib: "Jugement", + commercant: "SAS EXAMPLE", + dateparution: "2024-06-01", + tribunal: "TC Paris", + registre: "RCS Paris B 123 456 789", + cp: "75001", + ville: "Paris", + url_complete: "https://www.bodacc.fr/annonce/A-2024-001", + }, + ], + }), + }); + + const { bodaccSearch } = await import("./bodacc"); + const result = await bodaccSearch("EXAMPLE", { departement: "75" }); + + expect(result.ok).toBe(true); + if (!result.ok) return; + + expect(result.data.hits).toHaveLength(1); + expect(result.data.hits[0].id).toBe("BODACC-A-2024-001"); + expect(result.data.hits[0].type).toBe("Procédure collective"); + expect(result.data.hits[0].commercant).toBe("SAS EXAMPLE"); + expect(result.data.total).toBe(3); + + const url = mockFetch.mock.calls[0][0] as string; + expect(url).toContain("bodacc-datadila.opendatasoft.com"); + expect(url).toContain("annonces-commerciales"); + expect(url).toContain("q=EXAMPLE"); + expect(url).toContain("numerodepartement"); + }); + + it("gère une erreur serveur gracieusement", async () => { + mockFetch.mockResolvedValueOnce({ ok: false, status: 503 }); + + const { bodaccSearch } = await import("./bodacc"); + const result = await bodaccSearch("test"); + + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.reason).toBe("server"); + }); + }); +}); diff --git a/src/lib/connectors/opendatasoft.ts b/src/lib/connectors/opendatasoft.ts new file mode 100644 index 0000000..8b3f954 --- /dev/null +++ b/src/lib/connectors/opendatasoft.ts @@ -0,0 +1,60 @@ +import { toolError, toolOk, type ToolResult } from "@/lib/tools/result"; + +const DEFAULT_TIMEOUT_MS = 15_000; + +export type ODSSearchOptions = { + q?: string; + where?: string; + orderBy?: string; + limit?: number; + offset?: number; +}; + +export type ODSResult = { + total_count: number; + results: Record[]; +}; + +/** + * Client générique pour les instances OpenDataSoft Explore v2.1. + * Réutilisable pour BODACC, Infogreffe, UNEDIC, etc. + * Aucune authentification requise pour ces endpoints publics. + */ +export async function odsSearch( + baseUrl: string, + datasetId: string, + opts: ODSSearchOptions = {}, + serviceName = "OpenDataSoft" +): Promise> { + const url = new URL( + `${baseUrl}/api/explore/v2.1/catalog/datasets/${datasetId}/records` + ); + if (opts.q) url.searchParams.set("q", opts.q); + if (opts.where) url.searchParams.set("where", opts.where); + if (opts.orderBy) url.searchParams.set("order_by", opts.orderBy); + url.searchParams.set("limit", String(opts.limit ?? 10)); + if (opts.offset !== undefined) url.searchParams.set("offset", String(opts.offset)); + + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS); + try { + const res = await fetch(url.toString(), { + headers: { Accept: "application/json" }, + signal: controller.signal, + }); + if (!res.ok) { + if (res.status >= 500) { + return toolError("server", `${serviceName} indisponible (${res.status}).`); + } + return toolError("unknown", `${serviceName} a renvoyé une erreur ${res.status}.`); + } + return toolOk((await res.json()) as ODSResult); + } catch (err) { + if (err instanceof Error && err.name === "AbortError") { + return toolError("timeout", `${serviceName} n'a pas répondu à temps.`); + } + return toolError("network", `Impossible de joindre ${serviceName}.`); + } finally { + clearTimeout(timer); + } +} diff --git a/src/lib/connectors/piste.ts b/src/lib/connectors/piste.ts index 836488f..53a34be 100644 --- a/src/lib/connectors/piste.ts +++ b/src/lib/connectors/piste.ts @@ -109,6 +109,8 @@ export async function testPisteConnection( return "network_error"; } +const PISTE_BASE = "https://api.piste.gouv.fr"; + async function pisteRequest( userId: string, path: string, @@ -131,7 +133,6 @@ async function pisteRequest( signal: controller.signal, }); if (!res.ok) { - // Invalidate the cached token on 401 so the next call refreshes it. if (res.status === 401) tokenCache.delete(userId); return { ok: false, ...httpReason("Légifrance", res.status) }; } @@ -141,6 +142,75 @@ async function pisteRequest( } } +/** + * Requête GET authentifiée vers PISTE. Utilisée par les sous-APIs qui + * exposent des endpoints REST GET (ex: Judilibre). + */ +export async function pisteGet( + userId: string, + path: string, + serviceName = "PISTE" +): Promise> { + const tok = await getToken(userId); + if (!tok.ok) return tok; + + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), TIMEOUT_MS); + try { + const res = await fetch(`${PISTE_BASE}${path}`, { + method: "GET", + headers: { + Authorization: `Bearer ${tok.data}`, + Accept: "application/json", + }, + signal: controller.signal, + }); + if (!res.ok) { + if (res.status === 401) tokenCache.delete(userId); + return { ok: false, ...httpReason(serviceName, res.status) }; + } + return toolOk((await res.json()) as T); + } finally { + clearTimeout(timer); + } +} + +/** + * Requête POST authentifiée vers PISTE avec basePath configurable. + * Généralise pisteRequest pour cibler d'autres sous-APIs (ex: BOFIP via fond CIRC). + */ +export async function pistePost( + userId: string, + fullPath: string, + body: unknown, + serviceName = "PISTE" +): Promise> { + const tok = await getToken(userId); + if (!tok.ok) return tok; + + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), TIMEOUT_MS); + try { + const res = await fetch(`${PISTE_BASE}${fullPath}`, { + method: "POST", + headers: { + Authorization: `Bearer ${tok.data}`, + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(body), + signal: controller.signal, + }); + if (!res.ok) { + if (res.status === 401) tokenCache.delete(userId); + return { ok: false, ...httpReason(serviceName, res.status) }; + } + return toolOk((await res.json()) as T); + } finally { + clearTimeout(timer); + } +} + export type LegifranceHit = { id: string; title: string; diff --git a/src/lib/connectors/tools.ts b/src/lib/connectors/tools.ts index d8be94b..a2ed20d 100644 --- a/src/lib/connectors/tools.ts +++ b/src/lib/connectors/tools.ts @@ -2,6 +2,9 @@ import { tool, type ToolSet } from "ai"; import { z } from "zod"; import { pappersSearch, pappersGet } from "./pappers"; import { legifranceSearch } from "./piste"; +import { judilibreSearch, judilibreGetDecision } from "./judilibre"; +import { bodaccSearch } from "./bodacc"; +import { bofipSearch } from "./bofip"; import { listActiveConnectorTypes } from "./runtime"; import { ragSearch } from "@/lib/rag/search"; import { searchProjectMessages } from "@/lib/rag/message-search"; @@ -207,8 +210,95 @@ export async function buildToolsForUser( execute: async ({ query, fond }) => legifranceSearch(userId, query, fond ?? "ALL"), }); + + tools.judilibre_search = tool({ + description: + "Recherche de décisions de justice (Cour de cassation, cours d'appel) via Judilibre / PISTE. Renvoie jusqu'à 5 décisions avec ECLI, chambre, solution, résumé et lien. Utilisez cet outil pour la jurisprudence judiciaire (arrêts, pourvois, QPC). Pour le droit administratif (Conseil d'État), utilisez Légifrance fond JURI.", + inputSchema: z.object({ + query: z + .string() + .min(2) + .describe("Termes de recherche : mots-clés, n° de pourvoi, thème juridique…"), + jurisdiction: z + .enum(["cc", "ca", "tj"]) + .optional() + .describe("Filtrer par juridiction : cc (Cour de cassation), ca (Cour d'appel), tj (Tribunal judiciaire)."), + chamber: z + .string() + .optional() + .describe("Filtrer par chambre (ex: 'commerciale', 'sociale', 'criminelle')."), + date_start: z + .string() + .optional() + .describe("Date début au format YYYY-MM-DD."), + date_end: z + .string() + .optional() + .describe("Date fin au format YYYY-MM-DD."), + }), + execute: async ({ query, jurisdiction, chamber, date_start, date_end }) => + judilibreSearch(userId, query, { jurisdiction, chamber, date_start, date_end }), + }); + + tools.judilibre_decision = tool({ + description: + "Récupère le texte intégral et les métadonnées d'une décision de justice par son identifiant Judilibre. Utilisez cet outil après judilibre_search pour obtenir le contenu complet d'un arrêt.", + inputSchema: z.object({ + decision_id: z + .string() + .min(1) + .describe("Identifiant Judilibre de la décision (récupéré via judilibre_search)."), + }), + execute: async ({ decision_id }) => + judilibreGetDecision(userId, decision_id), + }); + + tools.bofip_search = tool({ + description: + "Recherche dans la doctrine fiscale (BOFIP — Bulletin Officiel des Finances Publiques) via PISTE. Renvoie instructions ministérielles, rescrits, commentaires de l'administration fiscale. Utilisez dès que la question porte sur le régime fiscal, la TVA, l'impôt sur les sociétés, la fiscalité des plus-values, etc.", + inputSchema: z.object({ + query: z + .string() + .min(2) + .describe("Termes de recherche : thème fiscal, numéro d'instruction, référence BOI…"), + }), + execute: async ({ query }) => bofipSearch(userId, query), + }); } + // ─── Sources open data (aucune authentification requise) ──────────────── + // Ces outils sont TOUJOURS disponibles indépendamment des connecteurs + // configurés par l'utilisateur. + + tools.bodacc_search = tool({ + description: + "Recherche dans le BODACC (Bulletin Officiel des Annonces Civiles et Commerciales). Couvre créations d'entreprises, modifications, radiations, procédures collectives, ventes/cessions. Aucune configuration requise (données ouvertes). Utile pour la veille commerciale, la due diligence, et les procédures collectives.", + inputSchema: z.object({ + query: z + .string() + .min(2) + .describe("Nom d'entreprise, numéro d'annonce, ou termes de recherche."), + departement: z + .string() + .optional() + .describe("Filtrer par département (2 chiffres, ex: '75' pour Paris)."), + famille: z + .string() + .optional() + .describe("Type d'avis : 'Création', 'Modification', 'Radiation', 'Procédure collective', 'Vente/Cession'."), + date_start: z + .string() + .optional() + .describe("Date début au format YYYY-MM-DD."), + date_end: z + .string() + .optional() + .describe("Date fin au format YYYY-MM-DD."), + }), + execute: async ({ query, departement, famille, date_start, date_end }) => + bodaccSearch(query, { departement, famille, date_start, date_end }), + }); + // Génération de documents — toujours disponible, indépendant des // connecteurs externes. Pure-JS côté serveur Louis (docx + pdfkit), pas // de dépendance LibreOffice ni d'envoi vers un service tiers.