Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions packages/ai-proxy/src/integrations/infogreffe/tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import RemoteTool from '../../remote-tool';
import createAdvancedCompanySearchTool from './tools/advanced-company-search';
import createAssessCompanyRiskTool from './tools/assess-company-risk';
import createCheckCompanyRadiationTool from './tools/check-company-radiation';
import createFindRelatedCompaniesTool from './tools/find-related-companies';
import createGetCompanyDetailsTool from './tools/get-company-details';
import createGetFinancialIndicatorsTool from './tools/get-financial-indicators';
import createSearchNewCompaniesTool from './tools/search-new-companies';
import createSearchRadiatedCompaniesTool from './tools/search-radiated-companies';

export interface InfogreffeConfig {
apiKey: string;
}

export default function getInfogreffeTools(config: InfogreffeConfig): RemoteTool[] {
const headers: Record<string, string> = {
Authorization: `Apikey ${config.apiKey}`,
};

return [
createCheckCompanyRadiationTool(headers),
createSearchRadiatedCompaniesTool(headers),
createSearchNewCompaniesTool(headers),
createGetCompanyDetailsTool(headers),
createAssessCompanyRiskTool(headers),
createGetFinancialIndicatorsTool(headers),
createFindRelatedCompaniesTool(headers),
createAdvancedCompanySearchTool(headers),
].map(
tool =>
new RemoteTool({
sourceId: 'infogreffe',
sourceType: 'server',
tool,
}),
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';

import { buildQuery, searchV1Api } from '../utils';

export default function createAdvancedCompanySearchTool(
headers: Record<string, string>,
): DynamicStructuredTool {
return new DynamicStructuredTool({
name: 'infogreffe_advanced_company_search',
description:
'Advanced multi-criteria search for French companies with combined filters (year, city, sector, legal form, etc.)',
schema: z.object({
ville: z.string().optional().describe('City (e.g., PARIS)'),
code_postal: z.string().optional().describe('Postal code (e.g., 75001)'),
secteur_d_activite: z.string().optional().describe('Business sector'),
code_ape: z.string().optional().describe('APE/NAF code (e.g., 6420Z)'),
forme_juridique: z.string().optional().describe('Legal form'),
status: z
.enum(['active', 'closed'])
.default('active')
.describe('Status: active (registered) or closed (struck-off)'),
year: z.enum(['2022', '2023', '2024', '2025']).default('2024').describe('Year'),
limit: z.number().int().positive().default(20).describe('Maximum results (default: 20)'),
}),
func: async ({
ville,
code_postal,
secteur_d_activite,
code_ape,
forme_juridique,
status,
year,
limit,
}) => {
const dataset =
status === 'closed'
? `entreprises-radiees-en-${year}`
: `entreprises-immatriculees-en-${year}`;

const query = buildQuery([
ville && `ville:${ville}`,
code_postal && `code_postal:${code_postal}`,
secteur_d_activite && `secteur_d_activite:"${secteur_d_activite}"`,
code_ape && `code_ape:${code_ape}`,
forme_juridique && `forme_juridique:"${forme_juridique}"`,
]);

const result = await searchV1Api(headers, dataset, query, limit);

return JSON.stringify({
criteres: {
ville,
code_postal,
secteur_d_activite,
code_ape,
forme_juridique,
status,
year,
},
total_resultats: result.nhits,
resultats_affiches: result.records.length,
entreprises: result.records.map(r => {
const fields = r.fields as Record<string, unknown>;

return {
siren: fields.siren,
denomination: fields.denomination,
adresse: fields.adresse,
code_postal: fields.code_postal,
ville: fields.ville,
forme_juridique: fields.forme_juridique,
secteur_d_activite: fields.secteur_d_activite,
code_ape: fields.code_ape,
date_immatriculation: fields.date_immatriculation,
date_radiation: fields.date_radiation,
};
}),
});
},
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';

import { searchV1Api } from '../utils';

type RiskLevel = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';

interface RiskFactor {
type: string;
severity: RiskLevel;
description: string;
date?: string;
date_immatriculation?: string;
}

const RECOMMENDATIONS: Record<RiskLevel, string> = {
CRITICAL: 'DO NOT ONBOARD - Company struck off or major risk',
HIGH: 'CAUTION - Manual verification required',
MEDIUM: 'VIGILANCE - Monitor closely',
LOW: 'Acceptable - Low risk',
};

function calculateRiskLevel(score: number): RiskLevel {
if (score >= 80) return 'CRITICAL';
if (score >= 50) return 'HIGH';
if (score >= 25) return 'MEDIUM';

return 'LOW';
}

export default function createAssessCompanyRiskTool(
headers: Record<string, string>,
): DynamicStructuredTool {
return new DynamicStructuredTool({
name: 'infogreffe_assess_company_risk',
description:
'Assess risk for a French company for KYC/KYB. Checks radiations, company age, etc. Returns a risk score',
schema: z
.object({
siren: z.string().length(9).optional().describe('SIREN number (9 digits)'),
denomination: z.string().optional().describe('Company name'),
})
.refine(data => data.siren || data.denomination, {
message: 'Either siren or denomination is required',
}),
func: async ({ siren, denomination }) => {
const riskFactors: RiskFactor[] = [];
let riskScore = 0;

const query = siren ? `siren:${siren}` : `denomination:${denomination}`;

const radiationResults = await Promise.all(
[2025, 2024].map(async year => {
const result = await searchV1Api(headers, `entreprises-radiees-en-${year}`, query, 1);

return { year, result };
}),
);

for (const { year, result } of radiationResults) {
if (result.nhits > 0) {
const fields = result.records[0].fields as Record<string, unknown>;
riskScore += 100;
riskFactors.push({
type: 'RADIATION',
severity: 'CRITICAL',
description: `Company struck off in ${year}`,
date: fields.date_radiation as string,
});
break;
}
}

if (siren) {
const result = await searchV1Api(
headers,
'entreprises-immatriculees-en-2024',
`siren:${siren}`,
1,
);

if (result.nhits > 0) {
const fields = result.records[0].fields as Record<string, unknown>;
const dateImmat = fields.date_immatriculation as string;

if (dateImmat) {
const immatDate = new Date(dateImmat);
const ageDays = Math.floor((Date.now() - immatDate.getTime()) / (1000 * 60 * 60 * 24));

if (ageDays < 90) {
riskScore += 30;
riskFactors.push({
type: 'YOUNG_COMPANY',
severity: 'HIGH',
description: `Very recent company (${ageDays} days)`,
date_immatriculation: dateImmat,
});
} else if (ageDays < 180) {
riskScore += 15;
riskFactors.push({
type: 'YOUNG_COMPANY',
severity: 'MEDIUM',
description: `Recent company (${ageDays} days)`,
date_immatriculation: dateImmat,
});
}
}
}
}

const riskLevel = calculateRiskLevel(riskScore);

return JSON.stringify({
siren,
denomination,
risk_score: riskScore,
risk_level: riskLevel,
risk_factors: riskFactors,
recommendation: RECOMMENDATIONS[riskLevel],
});
},
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';

import { searchV1Api } from '../utils';

const AVAILABLE_YEARS = [2025, 2024, 2023, 2022] as const;

export default function createCheckCompanyRadiationTool(
headers: Record<string, string>,
): DynamicStructuredTool {
return new DynamicStructuredTool({
name: 'infogreffe_check_company_radiation',
description:
'Check if a French company (by SIREN or name) is struck off from RCS. If no year is specified, checks all available years (2022-2025)',
schema: z
.object({
siren: z.string().length(9).optional().describe('SIREN number (9 digits)'),
denomination: z.string().optional().describe('Company name'),
year: z
.enum(['2022', '2023', '2024', '2025'])
.optional()
.describe('Specific year to check (optional, checks all years if not provided)'),
})
.refine(data => data.siren || data.denomination, {
message: 'Either siren or denomination is required',
}),
func: async ({ siren, denomination, year }) => {
const query = siren ? `siren:${siren}` : `denomination:${denomination}`;
const yearsToCheck = year ? [parseInt(year, 10)] : [...AVAILABLE_YEARS];

const results = await Promise.all(
yearsToCheck.map(async checkYear => {
const dataset = `entreprises-radiees-en-${checkYear}`;
const result = await searchV1Api(headers, dataset, query, 10);

return { checkYear, result };
}),
);

const allResults: Array<Record<string, unknown>> = [];

for (const { checkYear, result } of results) {
if (result.nhits > 0) {
for (const r of result.records) {
const fields = r.fields as Record<string, unknown>;
allResults.push({
annee: checkYear,
siren: fields.siren,
denomination: fields.denomination,
date_radiation: fields.date_radiation,
date_immatriculation: fields.date_immatriculation,
forme_juridique: fields.forme_juridique,
ville: fields.ville,
code_postal: fields.code_postal,
adresse: fields.adresse,
secteur_d_activite: fields.secteur_d_activite,
greffe: fields.greffe,
});
}
}
}

if (allResults.length === 0) {
return JSON.stringify({
est_radiee: false,
annees_verifiees: yearsToCheck,
critere: { siren, denomination },
message: 'No radiation found for this company',
});
}

return JSON.stringify({
est_radiee: true,
annees_verifiees: yearsToCheck,
nb_resultats: allResults.length,
entreprises: allResults,
});
},
});
}
Loading
Loading