departements

This commit is contained in:
el 2025-07-01 18:25:10 +02:00
parent 6b53a419c9
commit 4c180fe1f8
19 changed files with 21999 additions and 431 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,674 @@
{
"openapi": "3.0.1",
"info": {
"version": "1",
"title": "ROME V4.0 - Fiches ROME",
"description": "L'API « ROME 4.0 - Fiches métiers » permet de structurer les données au format de la fiche ROME.<br/> Cette API vous permet d'identifier, pour chaque fiche :<br/> <ul> <li>Les groupes de compétences mobilisées : les compétences sont classées et ordonnées par enjeu,</li> <li>Les groupes de savoirs : les savoirs sont classés et ordonnés par catégorie de savoir.</li> </ul>\r\n\r\n**Scopes** : `api_rome-fiches-metiersv1`,`nomenclatureRome`\r\n\r\n**Royaume** : `/partenaire`"
},
"servers": [
{
"url": "https://api.francetravail.io/partenaire/rome-fiches-metiers"
}
],
"paths": {
"/v1/fiches-rome/fiche-metier": {
"get": {
"tags": [
"Fiche métier"
],
"summary": "Lister les fiches métier",
"description": "Permet de lister les fiches métier.\r\n\r\nSi vous renseignez une ou plusieurs valeurs dans \"champs\", alors vous obtiendrez le résultat pour ces champs.\r\n\r\nSi vous ne renseignez aucune valeur dans \"champs\", alors vous obtiendrez le résultat par défaut. (Sélectionner l'exemple \"***Par défaut***\" pour voir les données retournées)",
"operationId": "listerFichesMetier",
"parameters": [
{
"name": "champs",
"in": "query",
"description": "Sélecteur de champs : il est possible de paramétrer le retour de la requête en définissant les champs souhaités (liste exhaustive des champs possibles : `Allowed values`). <br/> Vous pouvez vous aider des exemples de réponse pour identifier les champs que vous voulez récupérer.",
"required": false,
"schema": {
"type": "string",
"enum": [
"code",
"groupescompetencesmobilisees(competences(libelle,code),enjeu(libelle,code))",
"groupessavoirs(savoirs(libelle,code),categoriesavoirs(libelle,code))",
"metier(libelle,code)"
],
"example": "code,metier(libelle,code)"
}
},
{
"name": "Authorization",
"in": "header",
"required": true,
"schema": {
"type": "string",
"default": "Bearer lPjmA7mM6zOTJFjbJndC1sf3WJg"
}
}
],
"responses": {
"200": {
"description": "Retourne la liste des fiches métier.",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/FicheMetier"
}
},
"examples": {
"Retour complet": {
"value": [
{
"code": "A1201",
"groupesCompetencesMobilisees": [
{
"competences": [
{
"type": "string",
"libelle": "Techniques de soudage",
"code": "100007"
}
],
"enjeu": {
"libelle": "Animation",
"code": "3"
}
}
],
"groupesSavoirs": [
{
"savoirs": [
{
"type": "SAVOIR",
"libelle": "Techniques de soudage",
"code": "100007"
}
],
"categorieSavoirs": {
"libelle": "Véhicules",
"code": "30"
}
}
],
"metier": {
"libelle": "Bûcheronnage et élagage",
"code": "A1201"
}
}
]
},
"Retour par défaut": {
"value": [
{
"code": "A1201",
"metier": {
"libelle": "Bûcheronnage et élagage",
"code": "A1201"
}
}
]
}
}
}
}
},
"400": {
"description": "Bad request : peut-être causé par un sélecteur de champs incorrect.",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"message": "L'attribut 'monAttribut' n'est pas selectionnable",
"codeMetier": "SELECTOR",
"service": "/v1/mon_uri",
"code": 400
}
}
}
}
}
}
},
"security": [],
"servers": [
{
"url": "https://api.francetravail.io/partenaire/rome-fiches-metiers",
"description": "PROD"
}
]
}
},
"/v1/fiches-rome/fiche-metier/{code}": {
"get": {
"tags": [
"Fiche métier"
],
"summary": "Lire une fiche métier",
"description": "Permet de lire une fiche métier depuis son code.\r\n\r\nSi vous renseignez une ou plusieurs valeurs dans \"champs\", alors vous obtiendrez le résultat pour ces champs.\r\n\r\nSi vous ne renseignez aucune valeur dans \"champs\", alors vous obtiendrez le résultat par défaut. (Sélectionner l'exemple \"***Par défaut***\" pour voir les données retournées)",
"operationId": "lireFicheMetier",
"parameters": [
{
"name": "champs",
"in": "query",
"description": "Sélecteur de champs : il est possible de paramétrer le retour de la requête en définissant les champs souhaités (liste exhaustive des champs possibles : `Allowed values`). <br/> Vous pouvez vous aider des exemples de réponse pour identifier les champs que vous voulez récupérer.",
"required": false,
"schema": {
"type": "string",
"enum": [
"code",
"datefin",
"groupescompetencesmobilisees(competences(libelle,code),enjeu(libelle,code))",
"groupessavoirs(savoirs(libelle,code),categoriesavoirs(libelle,code))",
"metier(libelle,code)",
"obsolete"
],
"example": "obsolete,code,groupescompetencesmobilisees(competences(libelle,code),enjeu(libelle,code)),groupessavoirs(savoirs(libelle,code),categoriesavoirs(libelle,code)),datefin,metier(libelle,code)"
}
},
{
"name": "date",
"in": "query",
"description": "Date de la recherche au format yyyy-MM-dd ou yyyyMMdd.",
"required": false,
"schema": {
"type": "string",
"format": "date",
"example": "2022-05-28"
}
},
{
"name": "code",
"in": "path",
"description": "Code de la fiche métier.",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "Authorization",
"in": "header",
"required": true,
"schema": {
"type": "string",
"default": "Bearer lPjmA7mM6zOTJFjbJndC1sf3WJg"
}
}
],
"responses": {
"200": {
"description": "Retourne les données pour une fiche métier.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FicheMetier"
},
"examples": {
"Retour complet": {
"value": {
"obsolete": false,
"code": "A1201",
"groupesCompetencesMobilisees": [
{
"competences": [
{
"type": "string",
"libelle": "Techniques de soudage",
"code": "100007"
}
],
"enjeu": {
"libelle": "Animation",
"code": "3"
}
}
],
"groupesSavoirs": [
{
"savoirs": [
{
"type": "SAVOIR",
"libelle": "Techniques de soudage",
"code": "100007"
}
],
"categorieSavoirs": {
"libelle": "Véhicules",
"code": "30"
}
}
],
"dateFin": "2024-02-23T14:40:14.848Z",
"metier": {
"libelle": "Bûcheronnage et élagage",
"code": "A1201"
}
}
},
"Retour par défaut": {
"value": {
"obsolete": false,
"code": "A1201",
"groupesCompetencesMobilisees": [
{
"competences": [
{
"type": "string",
"libelle": "Techniques de soudage",
"code": "100007"
}
],
"enjeu": {
"libelle": "Animation",
"code": "3"
}
}
],
"groupesSavoirs": [
{
"savoirs": [
{
"type": "SAVOIR",
"libelle": "Techniques de soudage",
"code": "100007"
}
],
"categorieSavoirs": {
"libelle": "Véhicules",
"code": "30"
}
}
],
"dateFin": "2024-02-23T14:40:14.848Z",
"metier": {
"libelle": "Bûcheronnage et élagage",
"code": "A1201"
}
}
}
}
}
}
},
"400": {
"description": "Bad request : peut-être causé par un sélecteur de champs incorrect.",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"message": "L'attribut 'monAttribut' n'est pas selectionnable",
"codeMetier": "SELECTOR",
"service": "/v1/mon_uri",
"code": 400
}
}
}
}
}
},
"404": {
"description": "Not found.",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"service": "/v1/mon_uri",
"message": "Code inexistant: monCode",
"code": 404
}
}
}
}
}
}
},
"security": [],
"servers": [
{
"url": "https://api.francetravail.io/partenaire/rome-fiches-metiers",
"description": "PROD"
}
]
}
},
"/v1/fiches-rome/version": {
"get": {
"tags": [
"Versionning du ROME"
],
"summary": "Récupérer la version actuelle du ROME",
"description": "Récupère la version actuelle du ROME.",
"operationId": "lireVersion",
"parameters": [
{
"name": "champs",
"in": "query",
"description": "Sélecteur de champs : il est possible de paramétrer le retour de la requête en définissant les champs souhaités (liste exhaustive des champs possibles : `Allowed values`). <br/> Vous pouvez vous aider des exemples de réponse pour identifier les champs que vous voulez récupérer.",
"required": false,
"schema": {
"type": "string",
"enum": [
"version",
"lastModifiedDate",
"version,lastModifiedDate"
],
"example": "version,lastModifiedDate"
}
},
{
"name": "Authorization",
"in": "header",
"required": true,
"schema": {
"type": "string",
"default": "Bearer lPjmA7mM6zOTJFjbJndC1sf3WJg"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VersionRome"
},
"examples": {
"Retour complet": {
"value": {
"version": "19",
"lastModifiedDate": "2025-05-22T12:04:43.281Z"
}
},
"Retour par défaut": {
"value": {
"version": "19",
"lastModifiedDate": "2025-05-22T12:04:43.281Z"
}
}
}
}
}
},
"400": {
"description": "Bad request : peut-être causé par un sélecteur de champs incorrect",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"message": "L'attribut 'monAttribut' n'est pas selectionnable",
"codeMetier": "SELECTOR",
"service": "/v1/mon_uri",
"code": 400
}
}
}
}
}
}
},
"security": [],
"servers": [
{
"url": "https://api.pole-emploi.io/partenaire/rome-fiches-metiers",
"description": "PROD"
}
]
}
}
},
"components": {
"schemas": {
"FicheMetier": {
"type": "object",
"description": "Fiche Métier",
"properties": {
"code": {
"type": "string",
"description": "Code unique de la fiche métier",
"example": "A1201"
},
"dateFin": {
"type": "string",
"description": "date de fin de validité (renseignée si l'entité est obsolete)",
"format": "date-time"
},
"groupesCompetencesMobilisees": {
"type": "array",
"description": "Groupes de compétences mobilisées",
"items": {
"$ref": "#/components/schemas/GroupeCompetences"
}
},
"groupesSavoirs": {
"type": "array",
"description": "Groupes de savoirs",
"items": {
"$ref": "#/components/schemas/GroupeSavoirs"
}
},
"metier": {
"$ref": "#/components/schemas/Metier"
},
"obsolete": {
"type": "boolean",
"description": "boolean définissant si l'entité est obsolete"
}
}
},
"GroupeCompetences": {
"type": "object",
"description": "Groupe de compétences",
"properties": {
"competences": {
"type": "array",
"description": "Compétences rattachées. Il peut s'agir, de façon pêle-mêle, de macro-savoir-faire, de macro-savoir-êtres professionnels ou bien de compétence détaillée. Il ne peut pas y avoir de savoirs.",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/CompetenceDetaillee"
},
{
"$ref": "#/components/schemas/MacroSavoirEtreProfessionnel"
},
{
"$ref": "#/components/schemas/MacroSavoirFaire"
},
{
"$ref": "#/components/schemas/Savoir"
}
]
}
},
"enjeu": {
"$ref": "#/components/schemas/Enjeu"
}
}
},
"CompetenceDetaillee": {
"type": "object",
"description": "Compétence détaillée (en provenance du référentiel 'Compétences').",
"allOf": [
{
"$ref": "#/components/schemas/Competence"
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "Cette propriété est forcément à la valeur 'COMPETENCE-DETAILLEE'."
}
}
}
]
},
"Competence": {
"type": "object",
"description": "Compétence (en provenance du référentiel 'Compétences'). Cette entité se décompose en quatre catégories : les macro-savoir-faires, les macro-savoir-êtres professionnels, les compétences détaillées et enfin les savoirs.",
"properties": {
"code": {
"type": "string",
"description": "Code unique de la compétence.",
"example": "100007"
},
"libelle": {
"type": "string",
"description": "Libellé de la compétence.",
"example": "Techniques de soudage"
},
"type": {
"type": "string",
"description": "Le type de l'objet. Il y a quatre valeurs possibles :\n<ul>\n<li>'MACRO-SAVOIR-FAIRE' pour un macro-savoir-faire</li>\n<li>'MACRO-SAVOIR-ETRE-PROFESSIONNEL' pour un macro-savoir-être professionnel</li>\n<li>'COMPETENCE-DETAILLEE' pour un compétence détaillée</li>\n<li>'SAVOIR' pour un savoir</li>\n</ul>\n"
}
},
"discriminator": {
"propertyName": "type"
}
},
"MacroSavoirEtreProfessionnel": {
"type": "object",
"description": "Macro savoir-être professionnel (en provenance du référentiel 'Compétences').",
"allOf": [
{
"$ref": "#/components/schemas/Competence"
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "Cette propriété est forcément à la valeur 'MACRO-SAVOIR-ETRE-PROFESSIONNEL'."
}
}
}
]
},
"MacroSavoirFaire": {
"type": "object",
"description": "Macro savoir-faire (en provenance du référentiel 'Compétences').",
"allOf": [
{
"$ref": "#/components/schemas/Competence"
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "Cette propriété est forcément à la valeur 'MACRO-SAVOIR-FAIRE'."
}
}
}
]
},
"Savoir": {
"type": "object",
"description": "Savoir (en provenance du référentiel 'Compétences').",
"properties": {
"code": {
"type": "string",
"description": "Code unique de la compétence.",
"example": "100007"
},
"libelle": {
"type": "string",
"description": "Libellé de la compétence.",
"example": "Techniques de soudage"
},
"type": {
"type": "string",
"description": "Cette propriété est forcément à la valeur 'SAVOIR'."
}
}
},
"Enjeu": {
"type": "object",
"description": "Enjeu (en provenance du référentiel 'Compétences')",
"properties": {
"code": {
"type": "string",
"description": "Code unique de l'enjeu.",
"example": "3"
},
"libelle": {
"type": "string",
"description": "Libellé de l'enjeu.",
"example": "Animation"
}
}
},
"GroupeSavoirs": {
"type": "object",
"description": "Groupe de savoirs",
"properties": {
"categorieSavoirs": {
"$ref": "#/components/schemas/CategorieSavoirs"
},
"savoirs": {
"type": "array",
"description": "Savoirs rattachés",
"items": {
"$ref": "#/components/schemas/Savoir"
}
}
}
},
"CategorieSavoirs": {
"type": "object",
"description": "Catégorie de savoir (en provenance du référentiel 'Compétences')",
"properties": {
"code": {
"type": "string",
"description": "Code unique de la catégorie de savoirs.",
"example": "30"
},
"libelle": {
"type": "string",
"description": "Libellé de la catégorie de savoirs.",
"example": "Véhicules"
}
}
},
"Metier": {
"type": "object",
"description": "Métier (depuis le référentiel 'Métiers')",
"properties": {
"code": {
"type": "string",
"description": "Code unique du métier.",
"example": "A1201"
},
"libelle": {
"type": "string",
"description": "Libellé unique du métier.",
"example": "Bûcheronnage et élagage"
}
}
},
"VersionRome": {
"title": "VersionRome",
"type": "object",
"properties": {
"version": {
"type": "string",
"description": "La version actuelle.",
"example": "19"
},
"lastModifiedDate": {
"type": "string",
"description": "La date de dernière modification de la version.",
"format": "date-time"
}
},
"description": "Version du rome"
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,358 @@
{
"openapi": "3.0.1",
"info": {
"version": "1",
"title": "ROME V4.0 - Situations de travail",
"description": " Référentiel des environnements dans lequel une personne exerce son travail.<br/> Le contexte de travail englobe les différents éléments physiques, organisationnels et sociaux qui influencent les tâches, les activités et les interactions professionnelles. Il permet de mieux caractériser lemploi à pourvoir (avantages et contraintes du poste proposé) et réduire les « échecs » de recrutement liés aux conditions de travail.<br/> Le référentiel des contextes de travail se décline en 6 rubriques : Conditions de travail et risques professionnels, Horaires et durées de travail, Lieux et déplacements, Statut d'emploi, Publics spécifiques et Types de structure daccueil. \r\n\r\n**Scopes** : `api_rome-contextes-travailv1`,`nomenclatureRome`\r\n\r\n**Royaume** : `/partenaire`"
},
"servers": [
{
"url": "https://api.francetravail.io/partenaire/rome-contextes-travail"
}
],
"paths": {
"/v1/situations-travail/contexte-travail": {
"get": {
"tags": [
"Contexte de travail"
],
"summary": "Lister les contextes de travail",
"description": "Permet de lister les contextes de travail.\r\n\r\nSi vous renseignez une ou plusieurs valeurs dans \"champs\", alors vous obtiendrez le résultat pour ces champs.\r\n\r\nSi vous ne renseignez aucune valeur dans \"champs\", alors vous obtiendrez le résultat par défaut. (Sélectionner l'exemple \"***Par défaut***\" pour voir les données retournées)",
"operationId": "listerContextesTravail",
"parameters": [
{
"name": "champs",
"in": "query",
"description": "Sélecteur de champs : il est possible de paramétrer le retour de la requête en définissant les champs souhaités (liste exhaustive des champs possibles : `Allowed values`). <br/> Vous pouvez vous aider des exemples de réponse pour identifier les champs que vous voulez récupérer.",
"required": false,
"schema": {
"type": "string",
"enum": [
"categorie",
"code",
"libelle"
],
"example": "libelle,code,categorie"
}
},
{
"name": "Authorization",
"in": "header",
"required": true,
"schema": {
"type": "string",
"default": "Bearer 72xlz9Ri8sUL-EK2Bkbr5Uqk498"
}
}
],
"responses": {
"200": {
"description": "Retourne la liste des contextes de travail.",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ContexteTravail"
}
},
"examples": {
"Retour complet": {
"value": [
{
"libelle": "En club sportif",
"code": "403091",
"categorie": "CONDITIONS_TRAVAIL"
}
]
},
"Retour par défaut": {
"value": [
{
"libelle": "En club sportif",
"code": "403091",
"categorie": "CONDITIONS_TRAVAIL"
}
]
}
}
}
}
},
"400": {
"description": "Bad request : peut-être causé par un sélecteur de champs incorrect.",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"message": "L'attribut 'monAttribut' n'est pas selectionnable",
"codeMetier": "SELECTOR",
"service": "/v1/mon_uri",
"code": 400
}
}
}
}
}
}
},
"security": [],
"servers": [
{
"url": "https://api.francetravail.io/partenaire/rome-contextes-travail",
"description": "PROD"
}
]
}
},
"/v1/situations-travail/contexte-travail/{code}": {
"get": {
"tags": [
"Contexte de travail"
],
"summary": "Lire un contexte de travail",
"description": "Permet de lire un contexte de travail depuis son code.\r\n\r\nSi vous renseignez une ou plusieurs valeurs dans \"champs\", alors vous obtiendrez le résultat pour ces champs.\r\n\r\nSi vous ne renseignez aucune valeur dans \"champs\", alors vous obtiendrez le résultat par défaut. (Sélectionner l'exemple \"***Par défaut***\" pour voir les données retournées)",
"operationId": "lireContexteTravail",
"parameters": [
{
"name": "champs",
"in": "query",
"description": "Sélecteur de champs : il est possible de paramétrer le retour de la requête en définissant les champs souhaités (liste exhaustive des champs possibles : `Allowed values`). <br/> Vous pouvez vous aider des exemples de réponse pour identifier les champs que vous voulez récupérer.",
"required": false,
"schema": {
"type": "string",
"enum": [
"categorie",
"code",
"libelle"
],
"example": "libelle,code,categorie"
}
},
{
"name": "code",
"in": "path",
"description": "Code du contexte de travail.",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "Authorization",
"in": "header",
"required": true,
"schema": {
"type": "string",
"default": "Bearer 72xlz9Ri8sUL-EK2Bkbr5Uqk498"
}
}
],
"responses": {
"200": {
"description": "Retourne les données pour un contexte de travail renseigné.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ContexteTravail"
},
"examples": {
"Retour complet": {
"value": {
"libelle": "En club sportif",
"code": "403091",
"categorie": "CONDITIONS_TRAVAIL"
}
},
"Retour par défaut": {
"value": {
"libelle": "En club sportif",
"code": "403091",
"categorie": "CONDITIONS_TRAVAIL"
}
}
}
}
}
},
"400": {
"description": "Bad request : peut-être causé par un sélecteur de champs incorrect.",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"message": "L'attribut 'monAttribut' n'est pas selectionnable",
"codeMetier": "SELECTOR",
"service": "/v1/mon_uri",
"code": 400
}
}
}
}
}
},
"404": {
"description": "Not found.",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"service": "/v1/mon_uri",
"message": "Code inexistant: monCode",
"code": 404
}
}
}
}
}
}
},
"security": [],
"servers": [
{
"url": "https://api.francetravail.io/partenaire/rome-contextes-travail",
"description": "PROD"
}
]
}
},
"/v1/situations-travail/version": {
"get": {
"tags": [
"Versionning du ROME"
],
"summary": "Récupérer la version actuelle du ROME",
"description": "Récupère la version actuelle du ROME.",
"operationId": "lireVersion",
"parameters": [
{
"name": "champs",
"in": "query",
"description": "Sélecteur de champs : il est possible de paramétrer le retour de la requête en définissant les champs souhaités (liste exhaustive des champs possibles : `Allowed values`). <br/> Vous pouvez vous aider des exemples de réponse pour identifier les champs que vous voulez récupérer.",
"required": false,
"schema": {
"type": "string",
"enum": [
"version",
"lastModifiedDate",
"version,lastModifiedDate"
],
"example": "version,lastModifiedDate"
}
},
{
"name": "Authorization",
"in": "header",
"required": true,
"schema": {
"type": "string",
"default": "Bearer 72xlz9Ri8sUL-EK2Bkbr5Uqk498"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VersionRome"
},
"examples": {
"Retour complet": {
"value": {
"version": "19",
"lastModifiedDate": "2025-05-22T12:04:43.281Z"
}
},
"Retour par défaut": {
"value": {
"version": "19",
"lastModifiedDate": "2025-05-22T12:04:43.281Z"
}
}
}
}
}
},
"400": {
"description": "Bad request : peut-être causé par un sélecteur de champs incorrect",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"message": "L'attribut 'monAttribut' n'est pas selectionnable",
"codeMetier": "SELECTOR",
"service": "/v1/mon_uri",
"code": 400
}
}
}
}
}
}
},
"servers": [
{
"url": "https://api.francetravail.io/partenaire/rome-contextes-travail",
"description": "PROD"
}
]
}
}
},
"components": {
"schemas": {
"ContexteTravail": {
"type": "object",
"description": "Contexte de travail",
"properties": {
"categorie": {
"type": "string",
"enum": [
"CONDITIONS_TRAVAIL",
"TYPE_BENEFICIAIRE",
"LIEU_ET_DEPLACEMENT",
"HORAIRE_ET_DUREE_TRAVAIL",
"TYPE_STRUCTURE_ACCUEIL",
"STATUT_EMPLOI"
],
"description": "Catégorie de contexte de travail.",
"example": "CONDITIONS_TRAVAIL"
},
"code": {
"type": "string",
"description": "Code unique du contexte de travail.",
"example": "403091"
},
"libelle": {
"type": "string",
"description": "Libellé du contexte de travail.",
"example": "En club sportif"
}
}
},
"VersionRome": {
"title": "VersionRome",
"type": "object",
"properties": {
"version": {
"type": "string",
"description": "La version actuelle.",
"example": "19"
},
"lastModifiedDate": {
"type": "string",
"description": "La date de dernière modification de la version.",
"format": "date-time"
}
},
"description": "Version du rome"
}
}
}
}

811
backend/api/romeo_v2.json Normal file
View file

@ -0,0 +1,811 @@
{
"openapi": "3.0.3",
"info": {
"version": "2.0",
"title": "Romeo",
"description": "Utilisez lIA afin de vous aider à rapprocher un texte libre à une appellation ou à un savoir / savoir-faire du Répertoire Opérationnel des Métiers et des Emplois (ROME).\r\n\r\n***Scope :*** `api_romeov2`\r\n\r\n***Royaume :*** `/partenaire`"
},
"servers": [
{
"url": "https://api.francetravail.io/partenaire/romeo/v2",
"description": "PROD"
}
],
"paths": {
"/performancePredictionCompetences": {
"post": {
"tags": [
"Competence"
],
"summary": "Renvoyez-nous votre feedback pour permettre l'amélioration du modèle IA",
"description": "Cette ressource vous permet de nous envoyer votre feedback par rapport à la prédiction faite par l'IA. Cette information est importante pour mesurer la performance du modèle IA, pour améliorer la qualité des prédictions et délivrer un service de qualité.",
"operationId": "Renvoyez-nous votre feedback pour permettre l'amélioration du modèle IA",
"requestBody": {
"content": {
"application/json; charset=utf-8": {
"schema": {
"$ref": "#/components/schemas/PerformanceCompetenceDTO"
},
"examples": {
"Example 1": {
"value": {
"performanceCompetence": [
{
"uuidInference": "b42110bb-3d55-4e92-9930-133c5d92a555",
"bonnePrediction": true,
"codeCompetence": "101846"
}
],
"optionsSupervision": {
"nomAppelant": "francetravail"
}
}
}
}
}
}
},
"responses": {
"200": {
"description": "En cas de succès à l'appel API, cette dernière renvoie un code http : 200 OK ",
"content": {
"application/json; charset=utf-8": {
"schema": {
"$ref": "#/components/schemas/ExceptionPerformanceCompetence"
},
"examples": {
"Example 1": {
"value": {
"uuidInference": "b42110bb-3d55-4e92-9930-133c5d92a555"
}
}
}
}
}
},
"400": {
"description": "En cas de réponse 400, veuillez vous référer à l'erreur mentionnée dans l'objet message.",
"content": {
"application/json": {
"schema": {
"type": "object"
},
"examples": {
"Example 1": {
"value": {
"classeOrigine": "fr.pe.empl.service.so072.exceptions.RomeoBadRequestException",
"codeErreur": "J072000G",
"codeHttp": 400,
"message": "Le champ optionsSupervision.nomAppelant est obligatoire"
}
}
}
}
}
},
"500": {
"description": "Erreur serveur."
}
},
"servers": [
{
"url": "https://api.francetravail.io/partenaire/romeo/v2",
"description": ""
}
],
"parameters": [
{
"name": "Authorization",
"in": "header",
"required": true,
"schema": {
"type": "string",
"default": "Bearer SR-QkBNyazIvwyry39FxlZ7BMw4"
}
}
]
}
},
"/performancePredictionMetiers": {
"post": {
"tags": [
"Appellation"
],
"summary": "Renvoyez-nous votre feedback pour permettre l'amélioration du modèle IA",
"description": "Cette ressource vous permet de nous envoyer votre feedback par rapport à la prédiction faite par l'IA. Cette information est importante pour mesurer la performance du modèle IA, pour améliorer la qualité des prédictions et délivrer un service de qualité.",
"operationId": "Renvoyez-nous votre feedback pour permettre l amélioration du modèle IA",
"requestBody": {
"content": {
"application/json; charset=utf-8": {
"schema": {
"$ref": "#/components/schemas/PerformanceAppellationDTO"
},
"examples": {
"Example 1": {
"value": {
"performanceAppellation": [
{
"uuidInference": "93f7f573-da71-4382-94c7-e21db9c9a30f",
"bonnePrediction": true,
"codeAppellation": "11564"
}
],
"optionsSupervision": {
"nomAppelant": "francetravail"
}
}
}
}
}
}
},
"responses": {
"200": {
"description": "OK",
"content": {
"application/json; charset=utf-8": {
"schema": {
"$ref": "#/components/schemas/ExceptionPerformanceAppellation"
},
"examples": {
"Example 1": {
"value": {
"uuidInference": "b42110bb-3d55-4e92-9930-133c5d92a555"
}
}
}
}
}
},
"400": {
"description": "En cas de réponse 400, veuillez vous référer à l'erreur mentionnée dans l'objet message.",
"content": {
"application/json": {
"schema": {
"type": "object"
},
"examples": {
"Example 1": {
"value": {
"classeOrigine": "fr.pe.empl.service.so072.exceptions.RomeoBadRequestException",
"codeErreur": "J072000G",
"codeHttp": 400,
"message": "Le champ optionsSupervision.nomAppelant est obligatoire"
}
}
}
}
}
},
"500": {
"description": "Erreur serveur."
}
},
"servers": [
{
"url": "https://api.francetravail.io/partenaire/romeo/v2",
"description": ""
}
],
"parameters": [
{
"name": "Authorization",
"in": "header",
"required": true,
"schema": {
"type": "string",
"default": "Bearer SR-QkBNyazIvwyry39FxlZ7BMw4"
}
}
]
}
},
"/predictionCompetences": {
"post": {
"tags": [
"Competence"
],
"summary": "Prédiction des savoirs / savoir-faire du ROME",
"description": "Obtenez le savoir / savoir-faire du ROME le plus proche à partir dun texte libre renseigné.\r\n\r\n\r\n\r\nCas d'usage possibles :\r\n\r\n- Rapprocher une compétence dun CV à des savoirs / savoir-faire du ROME 4.0\r\n\r\n- Rapprocher une compétence dun référentiel extérieure à des savoirs / savoir-faire du ROME 4.0\r\n\r\n\r\n\r\nLes bonnes pratiques pour utiliser /PredictionCompetences\r\n\r\nSi votre texte contient plusieurs compétences libres, /PredictionCompetences ne pourra pas les séparer pour ensuite les associer au ROME de façon individuelle. Il est préférable de fournir un texte contenant une seule compétence libre.\r\n\r\nUtiliser /performancePredictionCompetences pour nous aider à améliorer notre modèle IA (cf. ressource ci-dessous : /performancePredictionCompetences - Renvoyez-nous votre feedback pour permettre l'amélioration du modèle IA).",
"operationId": "Prédiction des savoirs / savoir-faire du ROME",
"requestBody": {
"content": {
"application/json; charset=utf-8": {
"schema": {
"$ref": "#/components/schemas/CompetenceDTO"
},
"examples": {
"Example 1": {
"value": {
"competences": [
{
"intitule": "faire du pain",
"identifiant": "123456"
}
],
"options": {
"nomAppelant": "francetravail",
"nbResultats": 2,
"seuilScorePrediction": 0.7
}
}
}
}
}
}
},
"responses": {
"200": {
"description": "OK",
"content": {
"application/json; charset=utf-8": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PredictionCompetence"
}
},
"examples": {
"Example 1": {
"value": [
{
"uuidInference": "93f7f573-da71-4382-94c7-e21db9c9a30f",
"identifiant": "123456",
"intitule": "faire du pain",
"competencesRome": [
{
"libelleCompetence": "Fabriquer des produits de boulangerie",
"codeCompetence": "119357",
"typeCompetence": "COMPETENCE-DETAILLEE",
"scorePrediction": 0.8
}
]
}
]
}
}
}
}
},
"400": {
"description": "En cas de réponse 400, veuillez vous référer à l'erreur mentionnée dans l'objet message.",
"content": {
"application/json": {
"schema": {
"type": "object"
},
"examples": {
"Example 1": {
"value": {
"classeOrigine": "fr.pe.empl.service.so072.exceptions.RomeoBadRequestException",
"codeErreur": "J072000G",
"codeHttp": 400,
"message": "Le champ optionsSupervision.nomAppelant est obligatoire"
}
}
}
}
}
},
"500": {
"description": "Erreur serveur."
}
},
"servers": [
{
"url": "https://api.francetravail.io/partenaire/romeo/v2",
"description": ""
}
],
"parameters": [
{
"name": "Authorization",
"in": "header",
"required": true,
"schema": {
"type": "string",
"default": "Bearer SR-QkBNyazIvwyry39FxlZ7BMw4"
}
}
]
}
},
"/predictionMetiers": {
"post": {
"tags": [
"Appellation"
],
"summary": "Prédiction des appellations métier du ROME",
"description": "Obtenez lappellation métier du ROME la plus proche à partir dun texte libre renseigné.\r\n\r\n\r\n\r\nCas d'usage possibles\r\n- Rapprocher un intitulé doffre demploi à des appellations métier du ROME 4.0\r\n- Rapprocher un intitulé dexpérience dun CV à des appellations métier du ROME 4.0\r\n\r\nLes bonnes pratiques pour utiliser /predictionMetiers\r\n- Un texte court augmente la pertinence des prédictions (évitez les textes longs).\r\n- Si le texte à rapprocher du ROME est peu explicite, précisez le champ \"contexte\" pour faciliter la prédiction de /PredictionMetiers (par exemple, lintitulé \"conseiller\" peut se décliner en plusieurs appellations métier telles que \"conseiller en immobilier\" ou \"conseiller à l'emploi« , en précisant le champ contexte vous vous assurez dune prédiction plus cohérente).\r\n- Il n'est pas possible de prédire plus de 20 intitulés par appel.\r\nUtiliser /performancePredictionMetiers pour nous aider à améliorer notre modèle IA.",
"operationId": "Prédiction des appellations métier du ROME",
"requestBody": {
"content": {
"application/json; charset=utf-8": {
"schema": {
"$ref": "#/components/schemas/AppellationDTO"
},
"examples": {
"Example 1": {
"value": {
"appellations": [
{
"intitule": "Boucher",
"identifiant": "123456",
"contexte": "Commerce de détail de viandes et de produits à base de viande en magasin spécialisé"
}
],
"options": {
"nomAppelant": "francetravail",
"nbResultats": 2,
"seuilScorePrediction": 0.7
}
}
}
}
}
}
},
"responses": {
"200": {
"description": "En cas de succès à l'appel API, cette dernière renvoie un code http : 200 OK ",
"content": {
"application/json; charset=utf-8": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PredictionAppellation"
}
},
"examples": {
"Example 1": {
"value": [
{
"metiersRome": [
{
"libelleAppellation": "Boucher / Bouchère",
"codeAppellation": "11564",
"libelleRome": "Boucherie",
"codeRome": "D1101",
"scorePrediction": 0.75
}
],
"uuidInference": "bca57776-9e3f-4c72-b939-8a48cd87ff6e",
"identifiant": "123456",
"intitule": "boucher",
"contexte": "Commerce de détail de viandes et de produits à base de viande en magasin spécialisé"
}
]
}
}
}
}
},
"400": {
"description": "En cas de réponse 400, veuillez vous référer à l'erreur mentionnée dans l'objet message.",
"content": {
"application/json": {
"schema": {
"type": "object"
},
"examples": {
"Example 1": {
"value": {
"classeOrigine": "fr.pe.empl.service.so072.exceptions.RomeoBadRequestException",
"codeErreur": "J072000G",
"codeHttp": 400,
"message": "Le champ optionsSupervision.nomAppelant est obligatoire"
}
}
}
}
}
},
"500": {
"description": "Erreur serveur."
}
},
"servers": [
{
"url": "https://api.francetravail.io/partenaire/romeo/v2",
"description": ""
}
],
"parameters": [
{
"name": "Authorization",
"in": "header",
"required": true,
"schema": {
"type": "string",
"default": "Bearer SR-QkBNyazIvwyry39FxlZ7BMw4"
}
}
]
}
}
},
"tags": [
{
"name": "Appellation",
"description": "Prédictions sur les appellations"
},
{
"name": "Competence",
"description": "Prédictions sur les compétences"
}
],
"components": {
"schemas": {
"PerformanceCompetenceDTO": {
"type": "object",
"properties": {
"performanceCompetence": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PerformanceCompetence"
}
},
"optionsSupervision": {
"$ref": "#/components/schemas/OptionsSupervision"
}
}
},
"PerformanceCompetence": {
"type": "object",
"properties": {
"uuidInference": {
"type": "string",
"description": "Identifiant unique de l'inférence renvoyé par l'API.",
"example": "b42110bb-3d55-4e92-9930-133c5d92a555"
},
"bonnePrediction": {
"type": "boolean",
"enum": [
true,
false
],
"description": "- True : la prédiction est correcte\r\n- False : la prédiction est fausse "
},
"codeCompetence": {
"type": "string",
"description": "- Si \"bonneprediction\" est true : veuillez renseigner le code OGR du savoir / savoir-faire prédit par l'IA qui a été sélectionné\r\n- Si \"bonneprediction\" est false : veuillez renseigner le code OGR du savoir / savoir-faire que vous auriez souhaité avoir",
"example": "101846"
}
},
"required": [
"uuidInference",
"bonnePrediction"
]
},
"OptionsSupervision": {
"type": "object",
"properties": {
"nomAppelant": {
"type": "string",
"description": "Nom de l'application partenaire ou du partenaire appelant la ressource",
"example": "francetravail"
}
},
"required": [
"nomAppelant"
]
},
"ExceptionPerformanceCompetence": {
"type": "object",
"properties": {
"uuidInference": {
"type": "string",
"description": "identifiant unique de l'inférence, utile pour l'appel à la ressource performancePredicitonCompetences",
"example": "b42110bb-3d55-4e92-9930-133c5d92a555"
}
},
"required": [
"uuidInference"
]
},
"PerformanceAppellationDTO": {
"type": "object",
"properties": {
"performanceAppellation": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PerformanceAppellation"
}
},
"optionsSupervision": {
"$ref": "#/components/schemas/OptionsSupervision"
}
}
},
"PerformanceAppellation": {
"type": "object",
"properties": {
"uuidInference": {
"type": "string",
"description": "Identifiant unique de l'inférence renvoyé par l'API.",
"example": "93f7f573-da71-4382-94c7-e21db9c9a30f"
},
"bonnePrediction": {
"type": "boolean",
"enum": [
true,
false
],
"description": "- True : la prédiction est correcte\r\n- False : la prédiction est fausse "
},
"codeAppellation": {
"type": "string",
"description": "- Si \"bonneprediction\" est true : veuillez renseigner le code OGR de l'appellation métier prédite par l'IA qui a été sélectionnée\r\n- Si \"bonneprediction\" est false : veuillez renseigner le code OGR de l'appellation métier que vous auriez souhaité avoir",
"example": "11564"
}
},
"required": [
"uuidInference",
"bonnePrediction"
],
"x-examples": {
"Example 1": {
"uuidInference": "string",
"bonnePrediction": true,
"codeAppellation": "string"
}
}
},
"ExceptionPerformanceAppellation": {
"type": "object",
"properties": {
"uuidInference": {
"type": "string",
"example": "b42110bb-3d55-4e92-9930-133c5d92a555"
}
}
},
"CompetenceDTO": {
"type": "object",
"properties": {
"competences": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Competence"
}
},
"options": {
"$ref": "#/components/schemas/Options"
}
}
},
"Competence": {
"type": "object",
"properties": {
"intitule": {
"type": "string",
"description": "Texte libre pour lequel on souhaite prédire un savoir / savoir-faire du ROME (par exemple une compétence d'un CV)\r\n",
"example": "faire du pain"
},
"identifiant": {
"type": "string",
"description": "Identifiant fonctionnel unique, clé permettant d'identifier l'intitulé à prédire dans la liste fournie en entrée du service.\r\n\r\nCet identifiant est à la main du service appelant, il permet de rapprocher l'intitulé envoyé avec la prédiction retournée.",
"example": "123456"
}
},
"required": [
"intitule",
"identifiant"
]
},
"Options": {
"type": "object",
"properties": {
"nomAppelant": {
"type": "string",
"description": "Nom de l'application partenaire ou du partenaire appelant la ressource",
"example": "francetravail"
},
"nbResultats": {
"type": "integer",
"description": "nombre de prédictions à retourner\r\n\r\n- entier compris entre 1 et 25\r\n\r\n- par défaut, retourne 5 résultats",
"format": "int32",
"example": 2
},
"seuilScorePrediction": {
"type": "number",
"description": "Niveau de confiance de l'IA accompagnant chaque prédiction. Plus le score est proche de 1, plus lIA est confiante dans sa prédiction. \r\n\r\nSi un seuil est défini, il prévaut sur le nombre de résultats qui a été renseigné",
"format": "float",
"example": 0.7
}
},
"required": [
"nomAppelant"
]
},
"PredictionCompetence": {
"type": "object",
"properties": {
"uuidInference": {
"type": "string",
"description": "identifiant unique de l'inférence, utile pour l'appel à la ressource performancePredicitonCompetences",
"example": "93f7f573-da71-4382-94c7-e21db9c9a30f"
},
"identifiant": {
"type": "string",
"description": "Identifiant fonctionnel unique, clé permettant d'identifier l'intitulé à prédire dans la liste fournie en entrée du service.\r\n\r\nCet identifiant est à la main du service appelant, il permet de rapprocher l'intitulé envoyé avec la prédiction retournée.",
"example": "123456"
},
"intitule": {
"type": "string",
"description": "Texte libre renseigné lors de l'appel à l'API et pour lequel on souhaite prédire un savoir / savoir-faire du ROME",
"example": "faire du pain"
},
"competencesRome": {
"type": "array",
"items": {
"$ref": "#/components/schemas/CompetenceRome"
}
}
},
"required": [
"uuidInference",
"identifiant",
"intitule"
],
"x-examples": {}
},
"CompetenceRome": {
"type": "object",
"properties": {
"libelleCompetence": {
"type": "string",
"description": "Libellé du savoir / savoir-faire du ROME prédit",
"example": "Fabriquer des produits de boulangerie"
},
"codeCompetence": {
"type": "string",
"description": "Code du savoir / savoir-faire du ROME prédit",
"example": "119357"
},
"typeCompetence": {
"type": "string",
"description": "Type du savoir / savoir-faire du ROME prédit (savoir, compétence, macro-compétence)",
"example": "COMPETENCE-DETAILLEE"
},
"scorePrediction": {
"type": "number",
"description": "Score de confiance de l'IA suite à sa prédiction (plus on est proche de 1 plus l'IA est confiante)",
"format": "float",
"example": 0.8
}
},
"required": [
"libelleCompetence",
"codeCompetence",
"typeCompetence",
"scorePrediction"
],
"x-examples": {
"Example 1": {
"libelleCompetence": "Utiliser les réseaux sociaux",
"codeCompetence": "300425",
"typeCompetence": "MACRO-SAVOIR-FAIRE",
"scorePrediction": 0.8682828
}
}
},
"AppellationDTO": {
"type": "object",
"properties": {
"appellations": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Appellation"
}
},
"options": {
"$ref": "#/components/schemas/Options"
}
}
},
"Appellation": {
"type": "object",
"properties": {
"intitule": {
"type": "string",
"description": "Texte libre pour lequel on souhaite prédire un métier (par exemple l'intitulé d'une offre d'emploi). Pour rappel, il n'est pas possible de prédire plus de 20 intitulés par appel.",
"example": "Boucher"
},
"identifiant": {
"type": "string",
"description": "Identifiant fonctionnel unique, clé permettant d'identifier l'intitulé à prédire dans la liste fournie en entrée du service.\r\n\r\nCet identifiant est à la main du service appelant, il permet de rapprocher l'intitulé envoyé avec la prédiction retournée.",
"example": "123456"
},
"contexte": {
"type": "string",
"description": "aide l'IA à trouver la prédiction la plus pertinente en précisant le secteur dactivité. Ce contexte peut être décliné sous 3 formes :\r\n\r\n- un libellé NAF (INSEE)\r\n- un code SIRET (nous récupérons le libellé NAF associé à ce code)\r\n- un texte libre\r\n\r\nSi le texte à rapprocher du ROME est peu explicite, précisez le champ \"contexte\" pour faciliter la prédiction de Text2App (par exemple, lintitulé \"conseiller\" peut se décliner en plusieurs appellations métier telles que \"conseiller en immobilier\" ou \"conseiller à l'emploi« , en précisant le champ contexte vous vous assurez dune prédiction plus cohérente).",
"example": "Commerce de détail de viandes et de produits à base de viande en magasin spécialisé"
}
},
"required": [
"intitule",
"identifiant"
],
"x-examples": {
"Example 1": "Body\n{    \"appellations\": [        {            \"intitule\": \"boucher\",            \"contexte\": \"grande surface\",            \"identifiant\": \"123456\"        }    ],    \"options\": {        \"nomAppelant\": \"Application n°1\",        \"nbResultats\": 1,        \"seuilScorePrediction\": \"\"    } }"
}
},
"PredictionAppellation": {
"type": "object",
"properties": {
"metiersRome": {
"type": "array",
"items": {
"$ref": "#/components/schemas/AppellationRome"
}
},
"uuidInference": {
"type": "string",
"description": "Identifiant unique de l'inférence, utile pour l'appel à la ressource performancePredicitonMetiers",
"example": "bca57776-9e3f-4c72-b939-8a48cd87ff6e"
},
"identifiant": {
"type": "string",
"description": "Identifiant fonctionnel unique, clé permettant d'identifier l'intitulé à prédire dans la liste fournie en entrée du service. Cet identifiant est à la main du service appelant, il permet de rapprocher l'intitulé envoyé avec la prédiction retournée.",
"example": "123456"
},
"intitule": {
"type": "string",
"description": "Texte libre renseigné lors de l'appel à l'API et pour lequel on souhaite prédire un métier du ROME (par exemple l'intitulé d'une offre d'emploi).\nPour rappel, il n'est pas possible de prédire plus de 20 intitulés par appel.",
"example": "boucher"
},
"contexte": {
"type": "string",
"description": "Contexte renseigné lors de l'appel à l'API afin de prédire un métier du ROME",
"example": "Commerce de détail de viandes et de produits à base de viande en magasin spécialisé"
}
},
"required": [
"uuidInference",
"identifiant",
"intitule",
"contexte"
]
},
"AppellationRome": {
"type": "object",
"properties": {
"libelleAppellation": {
"type": "string",
"description": "Libellé de l'appellation métier du ROME prédite",
"example": "Boucher / Bouchère"
},
"codeAppellation": {
"type": "string",
"description": "Code de l'appellation métier ROME prédite",
"example": "11564"
},
"libelleRome": {
"type": "string",
"description": "Libellé de la fiche ROME parente",
"example": "Boucherie"
},
"codeRome": {
"type": "string",
"description": "Code ROME de la fiche parente",
"example": "D1101"
},
"scorePrediction": {
"type": "number",
"description": "Score de confiance de l'IA suite à sa prédiction (plus on est proche de 1 plus l'IA est confiante)",
"format": "float",
"example": 0.75
}
},
"required": [
"libelleAppellation",
"codeAppellation",
"libelleRome",
"codeRome",
"scorePrediction"
]
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,7 @@ class Settings(BaseSettings):
MISTRAL_API_KEY: Optional[str] = None MISTRAL_API_KEY: Optional[str] = None
GEMINI_API_KEY: Optional[str] = None GEMINI_API_KEY: Optional[str] = None
LLM_PROVIDER: str = "gemini" # Votre choix par défaut LLM_PROVIDER: str = "gemini" # Votre choix par défaut
UPLOAD_DIRECTORY: str = "./uploads" # Exemple : un dossier 'uploads' à la racine de votre backend
GEMINI_MODEL_NAME: Optional[str] = "gemini-1.5-flash" # Ou le nom de modèle Gemini que vous utilisez GEMINI_MODEL_NAME: Optional[str] = "gemini-1.5-flash" # Ou le nom de modèle Gemini que vous utilisez
MISTRAL_MODEL_NAME: Optional[str] = "mistral-tiny" # Ou le nom de modèle Mistral par défaut si vous l'utilisez MISTRAL_MODEL_NAME: Optional[str] = "mistral-tiny" # Ou le nom de modèle Mistral par défaut si vous l'utilisez
@ -23,11 +23,10 @@ class Settings(BaseSettings):
FRANCE_TRAVAIL_TOKEN_URL: str = "https://francetravail.io/connexion/oauth2/access_token?realm=%2Fpartenaire" FRANCE_TRAVAIL_TOKEN_URL: str = "https://francetravail.io/connexion/oauth2/access_token?realm=%2Fpartenaire"
FRANCE_TRAVAIL_API_BASE_URL: str = "https://api.francetravail.io/partenaire/offresdemploi" FRANCE_TRAVAIL_API_BASE_URL: str = "https://api.francetravail.io/partenaire/offresdemploi"
FRANCE_TRAVAIL_ROMEO_API_URL: str = "https://api.francetravail.io/partenaire/romeo/v2" FRANCE_TRAVAIL_ROMEO_API_URL: str = "https://api.francetravail.io/partenaire/romeo/v2"
# Si vous avez un scope API par défaut pour les offres d'emploi, vous pouvez le spécifier ici, par exemple : # AJOUT DE LA VARIABLE POUR LE SCOPE DE L'API OFFRES D'EMPLOI
# FRANCE_TRAVAIL_OFFER_API_SCOPE: str = "api_offresdemploiv1" FRANCE_TRAVAIL_OFFER_API_SCOPE: str = "api_offresdemploiv1" # C'est le scope commun pour l'API Offres d'emploi
model_config = SettingsConfigDict(env_file=".env", extra="ignore") model_config = SettingsConfigDict(env_file=".env", extra="ignore")
# --- AJOUT CRUCIAL : Instanciation de l'objet settings --- # --- INSTANCIATION DE L'OBJET settings ---
settings = Settings() settings = Settings()

View file

@ -107,9 +107,14 @@ async def analyze_job_offer_and_cv_route(
if not cv_document: if not cv_document:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="CV non trouvé ou non accessible par cet utilisateur.") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="CV non trouvé ou non accessible par cet utilisateur.")
try: try:
# Utilise la nouvelle fonction d'extraction de texte logger.info(f"Tentative d'extraction du texte du CV à partir de : {cv_document.filepath}") # AJOUTEZ CETTE
# --- AJOUTEZ CES LIGNES DE DEBUG ---
logger.info(f"Texte extrait (début) du CV: '{cv_text_to_analyze[:100]}...'") # Affiche les 100 premiers caractères
logger.info(f"Longueur du texte extrait du CV (avant strip): {len(cv_text_to_analyze)}")
logger.info(f"Longueur du texte extrait du CV (après strip): {len(cv_text_to_analyze.strip())}")
# --- FIN DES LIGNES DE DEBUG ---
cv_text_to_analyze = extract_text_from_file(cv_document.filepath) cv_text_to_analyze = extract_text_from_file(cv_document.filepath)
if not cv_text_to_analyze.strip(): # Vérifier après extraction si le contenu est vide if not cv_text_to_analyze.strip(): # Vérifier après extraction si le contenu est vide
raise ValueError("Le fichier CV est vide ou l'extraction de texte a échoué.") raise ValueError("Le fichier CV est vide ou l'extraction de texte a échoué.")

View file

@ -1,17 +1,23 @@
# backend/routers/document.py
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
import os import os
import uuid # Pour générer des noms de fichiers uniques import uuid # For generating unique filenames
import logging
from typing import List # Required for list type hint in get_user_documents
from core.database import get_db from core.database import get_db
from core.security import create_access_token # Non utilisé directement ici mais potentiellement dans d'autres routers # Removed unused 'create_access_token'
from core.config import settings # Pour accéder au chemin d'upload from core.security import get_current_user # Ensure this is the correct import for your get_current_user dependency
from core.config import settings # To access upload directory
from crud import document as crud_document from crud import document as crud_document
from crud import user as crud_user # Pour récupérer l'utilisateur courant # Removed unused 'crud_user' as it's not directly used in this router
from schemas import document as schemas_document from schemas import document as schemas_document
from schemas import user as schemas_user # Pour le modèle UserInDBBase ou UserResponse from schemas import user as schemas_user # For UserResponse schema
from dependencies import get_current_user # Pour la protection des routes
logger = logging.getLogger(__name__)
router = APIRouter( router = APIRouter(
prefix="/documents", prefix="/documents",
@ -19,7 +25,7 @@ router = APIRouter(
responses={404: {"description": "Not found"}}, responses={404: {"description": "Not found"}},
) )
@router.post("/upload-cv", response_model=schemas_document.DocumentResponse, status_code=status.HTTP_201_CREATED) @router.post("/upload-cv", response_model=schemas_document.DocumentResponse, status_code=status.HTTP_201_CREATED, summary="Uploader un CV")
async def upload_cv( async def upload_cv(
file: UploadFile = File(...), file: UploadFile = File(...),
db: Session = Depends(get_db), db: Session = Depends(get_db),
@ -29,39 +35,53 @@ async def upload_cv(
Permet à un utilisateur authentifié d'uploader un CV. Permet à un utilisateur authentifié d'uploader un CV.
Le fichier est stocké sur le serveur et ses métadonnées sont enregistrées en base de données. Le fichier est stocké sur le serveur et ses métadonnées sont enregistrées en base de données.
""" """
if not file.filename.lower().endswith(('.pdf', '.doc', '.docx')): logger.info(f"Tentative d'upload de CV par l'utilisateur {current_user.id} - Nom du fichier: {file.filename}")
if not file.filename:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Le nom du fichier est manquant.")
allowed_extensions = ('.pdf', '.doc', '.docx')
# Use os.path.splitext to safely get the extension
file_extension = os.path.splitext(file.filename)[1].lower()
if file_extension not in allowed_extensions:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="Seuls les fichiers PDF, DOC, DOCX sont autorisés." detail=f"Seuls les fichiers {', '.join(allowed_extensions).upper()} sont autorisés."
) )
# Créer un nom de fichier unique pour éviter les collisions et les problèmes de sécurité upload_dir = settings.UPLOADS_DIR # Utilisez le chemin absolu configuré dans settings
unique_filename = f"{uuid.uuid4()}_{file.filename}" os.makedirs(upload_dir, exist_ok=True)
file_path = os.path.join(settings.UPLOADS_DIR, unique_filename)
# S'assurer que le répertoire d'uploads existe # Generate a unique filename using UUID to prevent collisions and potential path traversal issues
os.makedirs(settings.UPLOADS_DIR, exist_ok=True) unique_filename = f"{uuid.uuid4()}{file_extension}"
file_path = os.path.join(upload_dir, unique_filename)
try: try:
# Write the file in chunks for efficiency with large files
with open(file_path, "wb") as buffer: with open(file_path, "wb") as buffer:
# Écrit le fichier par morceaux pour les gros fichiers while content := await file.read(1024 * 1024): # Read in 1MB chunks
while content := await file.read(1024 * 1024): # Lire par blocs de 1MB
buffer.write(content) buffer.write(content)
logger.info(f"Fichier '{file.filename}' enregistré sous '{file_path}' pour l'utilisateur {current_user.id}")
except Exception as e: except Exception as e:
logger.error(f"Erreur lors de l'enregistrement du fichier {file.filename}: {e}", exc_info=True)
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Erreur lors de l'enregistrement du fichier: {e}" detail=f"Erreur lors de l'enregistrement du fichier: {e}"
) )
finally: finally:
# Ensure the UploadFile is closed even if an error occurs
await file.close() await file.close()
# Enregistrer les métadonnées du document dans la base de données # Save document metadata in the database
# The DocumentCreate schema might not need 'filename' as a field if you pass it directly to crud
# Assuming DocumentCreate schema only takes filename and crud.create_document handles filepath
document_data = schemas_document.DocumentCreate(filename=file.filename) document_data = schemas_document.DocumentCreate(filename=file.filename)
db_document = crud_document.create_document(db, document_data, file_path, current_user.id) db_document = crud_document.create_document(db, document_data, file_path, current_user.id)
logger.info(f"Document ID {db_document.id} créé en base de données pour l'utilisateur {current_user.id}")
return db_document return db_document
@router.get("/", response_model=list[schemas_document.DocumentResponse]) @router.get("/", response_model=List[schemas_document.DocumentResponse], summary="Lister les documents de l'utilisateur")
def get_user_documents( def get_user_documents(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: schemas_user.UserResponse = Depends(get_current_user) current_user: schemas_user.UserResponse = Depends(get_current_user)
@ -69,26 +89,34 @@ def get_user_documents(
""" """
Récupère tous les documents uploadés par l'utilisateur authentifié. Récupère tous les documents uploadés par l'utilisateur authentifié.
""" """
logger.info(f"Tentative de listage des documents pour l'utilisateur {current_user.id}")
documents = crud_document.get_documents_by_owner(db, current_user.id) documents = crud_document.get_documents_by_owner(db, current_user.id)
return documents return documents
@router.get("/{document_id}", response_model=schemas_document.DocumentResponse) @router.get("/{document_id}", response_model=schemas_document.DocumentResponse, summary="Récupérer un document par ID")
def get_document_details( def get_document_details(
document_id: int, document_id: int,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: schemas_user.UserResponse = Depends(get_current_user) current_user: schemas_user.UserResponse = Depends(get_current_user)
): ):
""" """
Récupère les détails d'un document spécifique de l'utilisateur authentifié. Récupère les détails d'un document spécifique appartenant à l'utilisateur courant.
""" """
document = crud_document.get_document_by_id(db, document_id) logger.info(f"Tentative de récupération du document {document_id} pour l'utilisateur {current_user.id}")
# Appel à la fonction CRUD qui filtre déjà par owner_id
document = crud_document.get_document_by_id(db, document_id, current_user.id)
if not document: if not document:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document non trouvé.") # Si le document n'est pas trouvé (soit il n'existe pas, soit il n'appartient pas à cet utilisateur)
if document.owner_id != current_user.id: logger.warning(f"Document {document_id} non trouvé ou non autorisé pour l'utilisateur {current_user.id}")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Vous n'avez pas accès à ce document.") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Document non trouvé ou vous n'avez pas l'autorisation d'y accéder."
)
return document return document
@router.delete("/{document_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/{document_id}", status_code=status.HTTP_204_NO_CONTENT, summary="Supprimer un document par ID")
async def delete_document( async def delete_document(
document_id: int, document_id: int,
db: Session = Depends(get_db), db: Session = Depends(get_db),
@ -98,22 +126,62 @@ async def delete_document(
Supprime un document spécifique de l'utilisateur authentifié, Supprime un document spécifique de l'utilisateur authentifié,
à la fois de la base de données et du système de fichiers. à la fois de la base de données et du système de fichiers.
""" """
db_document = crud_document.get_document_by_id(db, document_id) logger.info(f"Tentative de suppression du document {document_id} pour l'utilisateur {current_user.id}")
if not db_document:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document non trouvé.")
if db_document.owner_id != current_user.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Vous n'avez pas la permission de supprimer ce document.")
# Supprimer le fichier du système de fichiers # Appel à la fonction CRUD qui filtre déjà par owner_id
if os.path.exists(db_document.filepath): db_document = crud_document.get_document_by_id(db, document_id, current_user.id)
if not db_document:
# Si le document n'est pas trouvé (soit il n'existe pas, soit il n'appartient pas à cet utilisateur)
logger.warning(f"Document {document_id} non trouvé ou non autorisé pour la suppression par l'utilisateur {current_user.id}")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Document non trouvé ou vous n'avez pas la permission de le supprimer."
)
# Supprimer le fichier du système de fichiers s'il existe et si un chemin est défini
if db_document.filepath and os.path.exists(db_document.filepath):
try: try:
os.remove(db_document.filepath) os.remove(db_document.filepath)
logger.info(f"Fichier physique '{db_document.filepath}' supprimé pour le document {document_id}.")
except OSError as e: except OSError as e:
logger.error(f"Erreur lors de la suppression du fichier physique '{db_document.filepath}' pour le document {document_id}: {e}", exc_info=True)
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Erreur lors de la suppression du fichier sur le serveur: {e}" detail=f"Erreur lors de la suppression du fichier sur le serveur: {e}"
) )
else:
logger.warning(f"Le document {document_id} n'a pas de chemin de fichier ou le fichier n'existe pas: {db_document.filepath}")
# Supprimer l'entrée de la base de données # Supprimer l'entrée de la base de données
crud_document.delete_document(db, document_id) success = crud_document.delete_document(db, document_id)
return {"message": "Document supprimé avec succès."} if not success:
logger.error(f"Échec de la suppression de l'entrée du document {document_id} de la base de données.")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Échec de la suppression du document de la base de données.")
logger.info(f"Document {document_id} et son fichier physique supprimés avec succès pour l'utilisateur {current_user.id}.")
return {} # 204 No Content typically returns an empty body
# Optional: Add a route to download the actual file if needed
@router.get("/{document_id}/download", summary="Télécharger un document")
async def download_document(
document_id: int,
db: Session = Depends(get_db),
current_user: schemas_user.UserResponse = Depends(get_current_user)
):
"""
Permet à l'utilisateur authentifié de télécharger un de ses documents.
"""
logger.info(f"Tentative de téléchargement du document {document_id} par l'utilisateur {current_user.id}")
db_document = crud_document.get_document_by_id(db, document_id, current_user.id)
if not db_document:
logger.warning(f"Document {document_id} non trouvé ou non autorisé pour le téléchargement par l'utilisateur {current_user.id}")
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document non trouvé ou non autorisé.")
if not os.path.exists(db_document.filepath):
logger.error(f"Fichier physique non trouvé pour le document {document_id} à l'emplacement: {db_document.filepath}")
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Fichier physique non trouvé sur le serveur.")
# Return the file as a FastAPI FileResponse
return FileResponse(path=db_document.filepath, filename=db_document.filename, media_type="application/octet-stream")

View file

@ -1,73 +1,62 @@
# backend/routers/france_travail_offers.py import logging
from typing import List, Optional from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status, Query from fastapi import APIRouter, Depends, HTTPException, status, Query
from services.france_travail_offer_service import france_travail_offer_service from services.france_travail_offer_service import france_travail_offer_service
from core.security import get_current_user # Assuming these imports are still needed for your project context,
from models.user import User # even if not directly used in the current problem scope.
# from core.security import get_current_user
# from models.user import User
from schemas.france_travail import FranceTravailSearchResponse, OffreDetail, Offre from schemas.france_travail import FranceTravailSearchResponse, OffreDetail, Offre
import logging
router = APIRouter() router = APIRouter()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@router.get("/search", response_model=FranceTravailSearchResponse) @router.get("/search", response_model=FranceTravailSearchResponse)
async def search_france_travail_offers( async def search_france_travail_offers(
motsCles: Optional[str] = Query(None, description="Mots-clés de recherche (ex: 'développeur full stack')"), motsCles: Optional[str] = Query(None, description="Mots-clés de recherche (ex: 'développeur full stack')"),
commune_nom_ou_code: Optional[str] = Query(None, alias="commune", description="Nom, code postal ou code INSEE de la commune"), commune_input: Optional[str] = Query(None, alias="commune", description="Nom de la commune (ex: 'Paris', 'Marseille'). Si spécifié, le code départemental sera automatiquement dérivé."),
distance: Optional[int] = Query(10, description="Distance maximale en km autour de la commune"), distance: Optional[int] = Query(10, description="Distance maximale en km autour de la commune ou du code postal. Applicable avec 'commune' ou 'codePostal', 'latitude'/'longitude'."),
codePostal: Optional[str] = Query(None, description="Code postal spécifique (ex: '75001')"),
latitude: Optional[float] = Query(None, description="Latitude du point de recherche (ex: 48.8566)"),
longitude: Optional[float] = Query(None, description="Longitude du point de recherche (ex: 2.3522)"),
# codeDepartement: Optional[str] = Query(None, description="Code départemental sur 2 chiffres (ex: '75' pour Paris). Prioritaire sur les autres paramètres de localisation."), # Ce paramètre est maintenant géré en interne par le service
page: int = Query(0, description="Numéro de la page de résultats (commence à 0)"), page: int = Query(0, description="Numéro de la page de résultats (commence à 0)"),
limit: int = Query(15, description="Nombre d'offres par page (max 100 pour l'API France Travail)"), # Max 100 est une limite courante pour une seule requête à l'API FT limit: int = Query(15, description="Nombre d'offres par page (max 100 pour l'API France Travail)"),
contrat: Optional[str] = Query(None, description="Type de contrat (ex: 'CDI', 'CDD', 'MIS')"), contrat: Optional[str] = Query(None, description="Type de contrat (ex: 'CDI', 'CDD', 'MIS'). Plusieurs séparés par des virgules."),
experience: Optional[str] = Query(None, description="Niveau d'expérience (ex: '1' pour débutant, '2' pour 1-3 ans, '3' pour >3 ans)"), experience: Optional[str] = Query(None, description="Niveau d'expérience (ex: 'E' pour expérimenté, 'D' pour débutant). Plusieurs séparés par des virgules.")
current_user: User = Depends(get_current_user) # current_user: User = Depends(get_current_user) # Décommentez si l'authentification est nécessaire
): ):
""" """
Recherche des offres d'emploi via l'API France Travail. Recherche des offres d'emploi via l'API France Travail.
Convertit le nom de ville en code INSEE si nécessaire et gère la pagination. La localisation peut être spécifiée par commune (le département sera dérivé), code postal, ou latitude/longitude.
Nécessite une authentification.
""" """
if limit > 100: # La limite de l'API France Travail pour 'range' est souvent 150 ou 100 items par requête. logger.info(f"Requête de recherche d'offres reçue: motsCles='{motsCles}', commune_input='{commune_input}', codePostal='{codePostal}', latitude='{latitude}', longitude='{longitude}', distance={distance}, page={page}, limit={limit}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="La limite de résultats par page ne peut pas dépasser 100 pour une seule requête API."
)
commune_param_for_api = None range_start = page * limit
range_end = range_start + limit - 1
logger.info(f"Paramètre 'range' calculé pour l'API France Travail: {range_start}-{range_end}")
if commune_nom_ou_code: # Convertir les chaînes de contrats et expériences en listes
if commune_nom_ou_code.isdigit() and len(commune_nom_ou_code) == 5: contrats_list = contrat.split(',') if contrat else None
commune_param_for_api = commune_nom_ou_code experiences_list = experience.split(',') if experience else None
logger.info(f"Recherche par code postal: {commune_nom_ou_code}")
else:
logger.info(f"Tentative de récupération du code INSEE pour la ville: {commune_nom_ou_code}")
insee_code = await france_travail_offer_service.get_insee_code_for_commune(commune_nom_ou_code)
if not insee_code:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Code INSEE non trouvé pour la ville '{commune_nom_ou_code}'. Veuillez vérifier l'orthographe ou utiliser un code postal."
)
commune_param_for_api = insee_code
logger.info(f"Code INSEE '{insee_code}' trouvé pour '{commune_nom_ou_code}'.")
if (commune_param_for_api is not None) and (distance is None): # Les paramètres de localisation sont passés directement au service,
distance = 10 # qui gérera la dérivation du département et la priorité.
# Calcul du paramètre 'range' pour l'API France Travail
start_index = page * limit
end_index = start_index + limit - 1
api_range_param = f"{start_index}-{end_index}"
logger.info(f"Paramètre 'range' calculé pour l'API France Travail: {api_range_param}")
try: try:
response = await france_travail_offer_service.search_offers( response = await france_travail_offer_service.search_offers(
motsCles=motsCles, motsCles=motsCles,
commune=commune_param_for_api, commune=commune_input, # Passe le nom de la commune directement
codePostal=codePostal,
latitude=latitude,
longitude=longitude,
distance=distance, distance=distance,
range=api_range_param, # On passe le 'range' calculé # codeDepartement n'est plus passé ici, il est dérivé dans le service
typeContrat=contrat, range_start=range_start,
# experience=experience # Vérifiez si ce paramètre est géré par l'API France Travail ou doit être mappé range_end=range_end,
typeContrat=contrats_list,
experience=experiences_list
) )
return response return response
except RuntimeError as e: except RuntimeError as e:
@ -80,7 +69,7 @@ async def search_france_travail_offers(
@router.get("/{offer_id}", response_model=OffreDetail) @router.get("/{offer_id}", response_model=OffreDetail)
async def get_france_travail_offer_details( async def get_france_travail_offer_details(
offer_id: str, offer_id: str,
current_user: User = Depends(get_current_user) # current_user: User = Depends(get_current_user)
): ):
""" """
Récupère les détails d'une offre d'emploi spécifique de l'API France Travail par son ID. Récupère les détails d'une offre d'emploi spécifique de l'API France Travail par son ID.
@ -93,5 +82,5 @@ async def get_france_travail_offer_details(
logger.error(f"Erreur lors de la récupération des détails de l'offre {offer_id} de France Travail: {e}") logger.error(f"Erreur lors de la récupération des détails de l'offre {offer_id} de France Travail: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Impossible de récupérer les détails de l'offre: {e}" detail=f"Impossible de récupérer les détails de l'offre {offer_id}: {e}"
) )

View file

@ -59,7 +59,7 @@ class Offre(BaseModel):
id: str = Field(..., example="1234567") id: str = Field(..., example="1234567")
intitule: str = Field(..., example="Développeur Full Stack") intitule: str = Field(..., example="Développeur Full Stack")
description: Optional[str] = None description: Optional[str] = None
dateCreation: datetime dateActualisation: Optional[datetime] = Field(None, example="2024-01-15T10:00:00.000Z")
dateActualisation: datetime dateActualisation: datetime
lieuTravail: Optional[LieuTravail] = None lieuTravail: Optional[LieuTravail] = None
typeContrat: Optional[Union[TypeContrat, str]] = None typeContrat: Optional[Union[TypeContrat, str]] = None
@ -81,7 +81,6 @@ class Offre(BaseModel):
class Config: class Config:
from_attributes = True from_attributes = True
# AJOUTEZ CETTE PROPRIÉTÉ CALCULÉE
@computed_field @computed_field
def url_francetravail(self) -> str: def url_francetravail(self) -> str:
"""Génère l'URL de l'offre sur le site candidat.francetravail.fr.""" """Génère l'URL de l'offre sur le site candidat.francetravail.fr."""

View file

@ -1,11 +1,12 @@
import json import json
import logging import logging
import sys import sys
from typing import Optional, Dict, Any from typing import Optional, Dict, Any, List
import google.genai as genai
# CORRECTION ICI : Importez explicitement HarmCategory et HarmBlockThreshold
from google.genai import types, HarmCategory, HarmBlockThreshold # Pour accéder à GenerationConfig, HarmCategory, HarmBlockThreshold
# MODIFIÉ ICI: Importations pour google-genai
from google import genai
from google.genai import types # Nécessaire pour configurer les types comme GenerateContentConfig
import mistralai import mistralai
from mistralai.client import MistralClient from mistralai.client import MistralClient
@ -13,6 +14,7 @@ from fastapi import HTTPException, status
import anyio import anyio
from core.config import settings from core.config import settings
from services.romeo_service import romeo_service # Assurez-vous que ce service existe et est configuré
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -29,140 +31,208 @@ try:
except Exception as e: except Exception as e:
logger.error(f"Error during mistralai debug info collection: {e}") logger.error(f"Error during mistralai debug info collection: {e}")
class AIService: class AIService:
def __init__(self): def __init__(self):
self.provider = settings.LLM_PROVIDER self.provider = settings.LLM_PROVIDER
self.model_name = settings.GEMINI_MODEL_NAME if self.provider == "gemini" else settings.MISTRAL_MODEL_NAME self.model_name = settings.GEMINI_MODEL_NAME if self.provider == "gemini" else settings.MISTRAL_MODEL_NAME
if self.provider == "gemini": self.client = None # Initialise le client à None
try: self.model = None # Initialise l'instance du modèle à None
# Initialisation du client genai.Client()
self.client = genai.Client(
api_key=settings.GEMINI_API_KEY
)
logger.info(f"Client Gemini genai.Client() initialisé.")
# Configuration de la génération avec types.GenerateContentConfig # S'assurer que generation_config et safety_settings sont toujours définis
self.gemini_config = types.GenerateContentConfig( self.generation_config = types.GenerationConfig(
candidate_count=1,
max_output_tokens=2048,
temperature=0.7, temperature=0.7,
top_p=1.0, top_k=40,
top_k=1, top_p=0.95
safety_settings=[
types.SafetySetting(category="HARM_CATEGORY_HARASSMENT", threshold="BLOCK_NONE"),
types.SafetySetting(category="HARM_CATEGORY_HATE_SPEECH", threshold="BLOCK_NONE"),
types.SafetySetting(category="HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold="BLOCK_NONE"),
types.SafetySetting(category="HARM_CATEGORY_DANGEROUS_CONTENT", threshold="BLOCK_NONE"),
]
) )
logger.info(f"Configuration Gemini types.GenerateContentConfig créée.") self.safety_settings = [
{"category": HarmCategory.HARM_CATEGORY_HARASSMENT, "threshold": HarmBlockThreshold.BLOCK_NONE},
{"category": HarmCategory.HARM_CATEGORY_HATE_SPEECH, "threshold": HarmBlockThreshold.BLOCK_NONE},
except Exception as e: {"category": HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, "threshold": HarmBlockThreshold.BLOCK_NONE},
logger.error(f"Erreur d'initialisation du client Gemini: {e}") {"category": HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, "threshold": HarmBlockThreshold.BLOCK_NONE},
raise ValueError(f"Impossible d'initialiser le client Gemini. Vérifiez votre GEMINI_API_KEY. Erreur: {e}") ]
if self.provider == "gemini":
if not settings.GEMINI_API_KEY:
raise ValueError("GEMINI_API_KEY n'est pas configurée.")
genai.configure(api_key=settings.GEMINI_API_KEY)
self.client = genai # Ceci est l'API de base
# Créez une instance de modèle spécifique sur laquelle appeler generate_content_async
self.model = genai.GenerativeModel(self.model_name, generation_config=self.generation_config)
elif self.provider == "mistral": elif self.provider == "mistral":
if not settings.MISTRAL_API_KEY: if not settings.MISTRAL_API_KEY:
raise ValueError("MISTRAL_API_KEY n'est pas configurée dans les paramètres.") raise ValueError("MISTRAL_API_KEY n'est pas configurée.")
# Initialize Mistral client
self.client = MistralClient(api_key=settings.MISTRAL_API_KEY) self.client = MistralClient(api_key=settings.MISTRAL_API_KEY)
# Pour Mistral, le client est directement l'objet qui appelle le chat, pas un modèle séparé comme pour Gemini.
# Gardez self.model à None ou à une valeur non utilisée si vous ne l'utilisez pas avec Mistral.
else: else:
raise ValueError(f"Fournisseur LLM non supporté: {self.provider}") raise ValueError(f"Fournisseur LLM inconnu: {self.provider}")
logger.info(f"AI Service initialized with Provider: {self.provider}, Model: {self.model_name}") async def _call_gemini_api(self, prompt: str) -> str:
try:
# CORRECTION ICI: Utilisez self.model pour appeler generate_content_async
if not self.model: # Ajout d'une vérification pour s'assurer que le modèle est initialisé
raise ValueError("Le modèle Gemini n'a pas été correctement initialisé.")
response = await self.model.generate_content_async(
prompt,
generation_config=self.generation_config,
safety_settings=self.safety_settings
)
return response.text
except Exception as e:
logger.error(f"Erreur lors de l'appel à Gemini: {e}")
raise RuntimeError(f"Erreur lors de l'appel à Gemini: {e}")
async def analyze_job_offer_and_cv(self, job_offer_text: str, cv_text: str) -> Dict[str, Any]: async def _call_mistral_api(self, prompt: str) -> str:
try:
# Assurez-vous que self.client est bien un MistralClient
if not isinstance(self.client, MistralClient):
raise TypeError("Le client Mistral n'est pas correctement initialisé.")
response = self.client.chat(
model=self.model_name,
messages=[{"role": "user", "content": prompt}],
temperature=self.generation_config.temperature,
max_tokens=self.generation_config.max_output_tokens,
)
return response.choices[0].message.content
except Exception as e:
logger.error(f"Erreur lors de l'appel à Mistral: {e}")
raise RuntimeError(f"Erreur lors de l'appel à Mistral: {e}")
async def analyze_job_offer_and_cv(
self, job_offer_text: str, cv_text: str
) -> Dict[str, Any]:
prompt = f""" prompt = f"""
En tant qu'assistant spécialisé dans la rédaction de CV et de lettres de motivation, votre tâche est d'analyser une offre d'emploi et un CV fournis, puis de : En tant qu'expert en recrutement, j'ai besoin d'une analyse comparative détaillée entre une offre d'emploi et un CV.
1. Calculer un score de pertinence entre 0 et 100 indiquant à quel point le CV correspond à l'offre. L'analyse doit identifier les correspondances, les lacunes et les suggestions d'amélioration pour le CV, en vue de maximiser les chances d'obtenir le poste.
2. Identifier les 3 à 5 points forts du CV en relation avec l'offre.
3. Suggérer 3 à 5 améliorations clés pour le CV afin de mieux correspondre à l'offre.
4. Proposer une brève phrase d'accroche pour une lettre de motivation, personnalisée pour cette offre et ce CV.
5. Identifier 3 à 5 mots-clés ou phrases importants de l'offre d'emploi que l'on devrait retrouver dans le CV.
L'offre d'emploi est la suivante : Voici l'offre d'emploi :
--- ---
{job_offer_text} {job_offer_text}
--- ---
Le CV est le suivant : Voici le CV :
--- ---
{cv_text} {cv_text}
--- ---
Veuillez retourner votre analyse au format JSON, en respectant la structure suivante : Veuillez fournir l'analyse dans le format JSON suivant, en vous assurant que tous les champs sont présents et remplis :
```json
{{ {{
"score_pertinence": int, "match_score": "Score de correspondance global (sur 100).",
"points_forts": ["string", "string", ...], "correspondances": [
"ameliorations_cv": ["string", "string", ...], {{
"phrase_accroche_lm": "string", "categorie": "Catégorie de correspondance (ex: 'Compétences techniques', 'Expérience', 'Qualités personnelles', 'Mots-clés')",
"mots_cles_offre": ["string", "string", ...] "elements": ["Liste des éléments correspondants trouvés dans l'offre et le CV."]
}} }}
],
"lacunes": [
{{
"categorie": "Catégorie de lacune (ex: 'Compétences manquantes', 'Expérience insuffisante', 'Mots-clés absents')",
"elements": ["Liste des éléments de l'offre d'emploi qui ne sont pas (ou peu) présents dans le CV."]
}}
],
"suggestions_cv": [
"Suggestions spécifiques pour améliorer le CV afin de mieux correspondre à l'offre (ex: 'Ajouter des détails sur...', 'Mettre en avant l'expérience en...', 'Inclure le mot-clé...')."
],
"qualites_perso_identifiees": ["Liste des qualités personnelles déduites du CV."],
"mots_cles_pertinents_offre": ["Liste des mots-clés importants identifiés dans l'offre d'emploi."],
"metiers_rome_suggeres_offre": [],
"competences_rome_suggeres_offre": [],
"analyse_detaillee": "Une analyse narrative plus approfondie des points forts et faibles du CV par rapport à l'offre, et un résumé général."
}}
```
Assurez-vous que la réponse est un JSON valide et complet. Ne pas inclure de texte explicatif avant ou après le bloc JSON.
""" """
logger.info(f"Envoi du prompt au LLM ({self.provider}): \n {prompt[:200]}...") # Log des 200 premiers caractères du prompt
response_content = "" response_content = ""
try:
if self.provider == "gemini": if self.provider == "gemini":
try: response_content = await self._call_gemini_api(prompt)
# MODIFIÉ ICI: Utilisation d'une lambda pour envelopper l'appel à generate_content
# avec tous ses arguments, pour que run_sync reçoive une fonction sans arguments supplémentaires
response = await anyio.to_thread.run_sync(
lambda: self.client.models.generate_content(
model=self.model_name,
contents=[{"role": "user", "parts": [{"text": prompt}]}],
config=self.gemini_config,
)
)
response_content = response.text
# Nettoyage de la réponse pour retirer les blocs de code Markdown
if response_content.startswith("```json") and response_content.endswith("```"):
response_content = response_content[len("```json"): -len("```")].strip()
elif response_content.startswith("```") and response_content.endswith("```"):
response_content = response_content[len("```"): -len("```")].strip()
except Exception as e:
logger.error(f"Erreur lors de l'appel à Gemini: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Erreur lors de l'appel à l'API Gemini: {e}"
)
elif self.provider == "mistral": elif self.provider == "mistral":
if not settings.MISTRAL_API_KEY: response_content = await self._call_mistral_api(prompt)
else:
raise ValueError("Fournisseur LLM non supporté.")
except Exception as e:
logger.error(f"Échec de l'appel au service LLM: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="La clé API Mistral n'est pas configurée." detail=f"Erreur lors de l'appel au service LLM: {e}"
) )
try: try:
response = await self.client.chat_async( # Gemini renvoie parfois du Markdown, donc on extrait le JSON
model=self.model_name, if self.provider == "gemini" and "```json" in response_content:
messages=[{"role": "user", "content": prompt}], json_start = response_content.find("```json") + len("```json")
temperature=0.7, json_end = response_content.find("```", json_start)
max_tokens=1000 if json_end != -1:
json_str = response_content[json_start:json_end].strip()
else:
json_str = response_content[json_start:].strip()
else:
json_str = response_content.strip()
analysis_result = json.loads(json_str)
logger.info("Réponse JSON du LLM parsée avec succès.")
except json.JSONDecodeError as e:
logger.error(f"Erreur de parsing JSON depuis la réponse du LLM: {e}. Réponse brute: {response_content}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Erreur de format de réponse du LLM. Impossible de parser le JSON."
) )
response_content = response.choices[0].message.content except Exception as e:
logger.error(f"Erreur inattendue lors du traitement de la réponse LLM: {e}. Réponse brute: {response_content}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Erreur inattendue lors du traitement de la réponse LLM: {e}"
)
# Intégration du service Romeo
try:
logger.info("Début de l'intégration avec le service Romeo...")
# S'assurer que les textes ne sont pas None avant de les passer à Romeo
job_offer_text_for_romeo = job_offer_text if job_offer_text is not None else ""
cv_text_for_romeo = cv_text if cv_text is not None else ""
# Appels aux services Romeo
# romeo_metiers_predictions = await romeo_service.predict_metiers(job_offer_text_for_romeo)
# romeo_competences_predictions = await romeo_service.predict_competences(job_offer_text_for_romeo)
# NOTE: Les appels Romeo sont mis en commentaire car vous pourriez vouloir les activer sélectivement
# ou les décommenter une fois que la base d'analyse LLM est stable.
# Si vous utilisez romeo_service, assurez-vous qu'il est correctement initialisé et accessible.
# Exemple de comment les utiliser si activé:
# Extraction des codes ROME (par exemple, 'D1101') des prédictions métiers
# et des codes de compétences ROME (par exemple, 'G1601') des prédictions de compétences
# predicted_rome_metiers = [
# m["codeRome"] for m in romeo_metiers_predictions if "codeRome" in m
# ] if romeo_metiers_predictions else []
# predicted_rome_competences = [
# c["codeAppellation"] for c in romeo_competences_predictions if "codeAppellation" in c
# ] if romeo_competences_predictions else []
# Utiliser ces prédictions de Romeo pour mettre à jour le résultat de l'analyse
# analysis_result["metiers_rome_suggeres_offre"] = list(set(predicted_rome_metiers)) # Utilise set pour éviter les doublons
# analysis_result["competences_rome_suggeres_offre"] = list(set(predicted_rome_competences)) # Utilise set pour éviter les doublons
logger.info("Intégration Romeo terminée avec succès (ou ignorée si en commentaire).")
except Exception as e: except Exception as e:
logger.error(f"Erreur lors de l'appel à Mistral: {e}") logger.error(f"Erreur lors de l'intégration avec le service Romeo: {e}")
raise HTTPException( # Ne pas relancer une HTTPException ici si l'intégration Romeo est optionnelle ou en cours de développement,
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, # car cela masquerait l'analyse LLM. Vous pouvez choisir de logguer et continuer, ou de relancer si c'est critique.
detail=f"Erreur lors de l'appel à l'API Mistral: {e}" # Pour l'instant, on se contente de logguer l'erreur.
) pass
else:
raise ValueError(f"Fournisseur LLM non supporté: {self.provider}")
logger.info(f"Réponse brute de l'IA (après nettoyage si nécessaire) ({self.provider}): {response_content}") return analysis_result
try: # Instanciation unique du service AI...
parsed_response = json.loads(response_content)
return parsed_response
except json.JSONDecodeError as e:
logger.error(f"Erreur de décodage JSON de la réponse IA ({self.provider}): {e}")
logger.error(f"Contenu non-JSON reçu (après nettoyage): {response_content}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="La réponse de l'IA n'était pas au format JSON attendu."
)
# Instanciation unique du service AI
ai_service = AIService() ai_service = AIService()

View file

@ -2,7 +2,7 @@
import httpx import httpx
import logging import logging
from core.config import settings from core.config import settings
from datetime import datetime, timedelta
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class FranceTravailAuthService: class FranceTravailAuthService:
@ -16,12 +16,12 @@ class FranceTravailAuthService:
async def get_access_token(self): async def get_access_token(self):
# Vérifiez si le token est encore valide dans le cache # Vérifiez si le token est encore valide dans le cache
if self._token_cache and self._token_cache.get("expires_at", 0) > httpx._compat.current_time(): if self._token_cache and self._token_cache.get("expires_at", 0) > datetime.now():
logger.info("Utilisation du token France Travail depuis le cache.") logger.info("Utilisation du token France Travail depuis le cache.")
return self._token_cache["access_token"] return self._token_cache["access_token"]
logger.info("Obtention d'un nouveau token France Travail...") logger.info("Obtention d'un nouveau token France Travail...")
token_url = settings.FRANCE_TRAVAIL_TOKEN_URL token_url = settings.FRANCE_TRAVAIL_TOKEN_URL # C'est la ligne modifiée
client_id = settings.FRANCE_TRAVAIL_CLIENT_ID client_id = settings.FRANCE_TRAVAIL_CLIENT_ID
client_secret = settings.FRANCE_TRAVAIL_CLIENT_SECRET client_secret = settings.FRANCE_TRAVAIL_CLIENT_SECRET
scope = "o2dsoffre api_offresdemploiv2" # Assurez-vous que ces scopes sont activés pour votre application scope = "o2dsoffre api_offresdemploiv2" # Assurez-vous que ces scopes sont activés pour votre application
@ -34,14 +34,15 @@ class FranceTravailAuthService:
} }
headers = { headers = {
"Content-Type": "application/x-www-form-urlencoded" # C'est très important ! "Content-Type": "application/x-www-form-urlencoded"
} }
try:
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
try:
response = await client.post(token_url, data=data, headers=headers) response = await client.post(token_url, data=data, headers=headers)
response.raise_for_status() # Lève une exception pour les codes d'erreur HTTP response.raise_for_status() # Lève une exception pour les codes d'erreur HTTP
# DÉBUT DE LA CORRECTION : Ces lignes sont maintenant correctement indentées dans le bloc try
token_data = response.json() token_data = response.json()
access_token = token_data.get("access_token") access_token = token_data.get("access_token")
expires_in = token_data.get("expires_in") # Durée de validité en secondes expires_in = token_data.get("expires_in") # Durée de validité en secondes
@ -52,11 +53,11 @@ class FranceTravailAuthService:
# Mettre à jour le cache # Mettre à jour le cache
self._token_cache = { self._token_cache = {
"access_token": access_token, "access_token": access_token,
"expires_at": httpx._compat.current_time() + expires_in - 60 # 60 secondes de marge de sécurité "expires_at": datetime.now() + timedelta(seconds=expires_in - 60) # 60 secondes de marge de sécurité
} }
logger.info("Nouveau token France Travail obtenu et mis en cache.") logger.info("Nouveau token France Travail obtenu et mis en cache.")
return access_token return access_token
# FIN DE LA CORRECTION
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
logger.error(f"Erreur HTTP lors de l'obtention du token France Travail: {e.response.status_code} - {e.response.text}") logger.error(f"Erreur HTTP lors de l'obtention du token France Travail: {e.response.status_code} - {e.response.text}")
# Re-raise une RuntimeError pour que le service appelant puisse la gérer # Re-raise une RuntimeError pour que le service appelant puisse la gérer

View file

@ -1,197 +1,225 @@
# backend/services/france_travail_offer_service.py
import httpx import httpx
import logging import logging
from datetime import datetime, timedelta import time
from typing import List, Optional, Dict, Any, Union from typing import Optional, Dict, Any, List, Tuple
import asyncio
from core.config import settings from core.config import settings
from schemas.france_travail import FranceTravailSearchResponse, OffreDetail, Offre, TypeContrat from schemas.france_travail import FranceTravailSearchResponse, OffreDetail
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class FranceTravailOfferService: class FranceTravailOfferService:
def __init__(self): def __init__(self):
self.client_id = settings.FRANCE_TRAVAIL_CLIENT_ID self.client = httpx.AsyncClient(base_url=settings.FRANCE_TRAVAIL_API_BASE_URL)
self.client_secret = settings.FRANCE_TRAVAIL_CLIENT_SECRET self.auth_client = httpx.AsyncClient(base_url=settings.FRANCE_TRAVAIL_TOKEN_URL.split('?')[0])
self.token_url = settings.FRANCE_TRAVAIL_TOKEN_URL self.token_info = {"token": None, "expires_at": 0}
self.api_base_url = settings.FRANCE_TRAVAIL_API_BASE_URL self.geo_api_client = httpx.AsyncClient() # Client for geo.api.gouv.fr
self.api_scope = settings.FRANCE_TRAVAIL_API_SCOPE
self.access_token = None
self.token_expires_at = None
async def _get_access_token(self): async def _get_access_token(self):
if self.access_token and self.token_expires_at and datetime.now() < self.token_expires_at: if self.token_info["token"] and self.token_info["expires_at"] > time.time() + 60: # Refresh 1 min before expiry
logger.info("Réutilisation du token France Travail existant.") logger.info("Utilisation du token France Travail depuis le cache.")
return self.access_token return self.token_info["token"]
logger.info("Obtention d'un nouveau token d'accès France Travail...") logger.info("Obtention d'un nouveau token France Travail...")
headers = { try:
"Content-Type": "application/x-www-form-urlencoded" token_url_with_realm = settings.FRANCE_TRAVAIL_TOKEN_URL
} response = await self.auth_client.post(
token_url_with_realm,
data={ data={
"grant_type": "client_credentials", "grant_type": "client_credentials",
"client_id": self.client_id, "client_id": settings.FRANCE_TRAVAIL_CLIENT_ID,
"client_secret": self.client_secret, "client_secret": settings.FRANCE_TRAVAIL_CLIENT_SECRET,
"scope": self.api_scope "scope": "o2dsoffre api_offresdemploiv2"
} }
async with httpx.AsyncClient() as client:
try:
response = await client.post(self.token_url, headers=headers, data=data)
response.raise_for_status()
token_data = response.json()
self.access_token = token_data["access_token"]
expires_in = token_data.get("expires_in", 1500)
self.token_expires_at = datetime.now() + timedelta(seconds=expires_in - 60)
logger.info("Token France Travail obtenu avec succès.")
return self.access_token
except httpx.HTTPStatusError as e:
logger.error(f"Erreur HTTP lors de l'obtention du token France Travail: {e.response.status_code} - {e.response.text}")
raise RuntimeError(f"Échec de l'obtention du token France Travail: {e.response.text}")
except Exception as e:
logger.error(f"Erreur inattendue lors de l'obtention du token France Travail: {e}")
raise RuntimeError(f"Échec inattendu lors de l'obtention du token France Travail: {e}")
async def get_insee_code_for_commune(self, commune_name: str) -> Optional[str]:
"""
Récupère le code INSEE d'une commune à partir de son nom.
Recherche une correspondance exacte du libellé, ou un code spécifique pour Paris.
"""
token = await self._get_access_token()
headers = {
"Accept": "application/json",
"Authorization": f"Bearer {token}"
}
params = {
"q": commune_name
}
async with httpx.AsyncClient() as client:
try:
response = await client.get(
f"{self.api_base_url}/v2/referentiel/communes",
headers=headers,
params=params
) )
response.raise_for_status() response.raise_for_status()
communes_data = response.json() token_data = response.json()
self.token_info["token"] = token_data["access_token"]
self.token_info["expires_at"] = time.time() + token_data["expires_in"]
logger.info("Nouveau token France Travail obtenu et mis en cache.")
return self.token_info["token"]
except httpx.HTTPStatusError as e:
logger.error(f"Échec de l'obtention du token France Travail: {e.response.status_code} - {e.response.text}")
raise RuntimeError(f"Échec de l'obtention du token France Travail: {e.response.text}")
except httpx.RequestError as e:
logger.error(f"Erreur réseau lors de l'obtention du token France Travail: {e}")
raise RuntimeError(f"Erreur réseau lors de l'obtention du token France Travail: {e}")
found_code = None async def get_insee_and_postal_code_for_commune(self, commune_name: str) -> Optional[Tuple[str, str, float, float]]:
normalized_input_name = commune_name.upper().strip() logger.info(f"Début de la recherche Geo API pour: '{commune_name}'")
try:
# First, try exact match by name
geo_url_by_name = f"https://geo.api.gouv.fr/communes?nom={commune_name}&fields=codesPostaux,code,nom,centre&format=json&limit=5"
logger.info(f"Recherche par nom via API Geo.gouv.fr: {geo_url_by_name}")
response = await self.geo_api_client.get(geo_url_by_name)
response.raise_for_status()
communes = response.json()
if communes_data and isinstance(communes_data, list): for commune in communes:
for commune_info in communes_data: # Prioritize exact name match
if commune_info and "code" in commune_info and "libelle" in commune_info: if commune['nom'].lower() == commune_name.lower():
normalized_libelle = commune_info["libelle"].upper().strip() insee_code = commune['code']
postal_code = commune['codesPostaux'][0] if commune['codesPostaux'] else None
latitude = commune['centre']['coordinates'][1]
longitude = commune['centre']['coordinates'][0]
logger.info(f"Correspondance exacte par nom trouvée: INSEE='{insee_code}', CP='{postal_code}', Lat='{latitude}', Long='{longitude}' pour '{commune_name}'.")
return insee_code, postal_code, latitude, longitude
# Priorité 1: Recherche spécifique pour "PARIS" avec son code INSEE connu # If no exact name match, try the first result if available
if normalized_input_name == "PARIS" and commune_info["code"] == "75056": if communes:
found_code = commune_info["code"] commune = communes[0]
break insee_code = commune['code']
# Priorité 2: Correspondance exacte du libellé postal_code = commune['codesPostaux'][0] if commune['codesPostaux'] else None
elif normalized_libelle == normalized_input_name: latitude = commune['centre']['coordinates'][1]
found_code = commune_info["code"] longitude = commune['centre']['coordinates'][0]
break logger.info(f"Aucune correspondance exacte par nom, première commune trouvée: INSEE='{insee_code}', CP='{postal_code}', Lat='{latitude}', Long='{longitude}' pour '{commune_name}'.")
# Priorité 3: Si c'est Paris, mais le libellé renvoyé n'est pas "PARIS" exactement, return insee_code, postal_code, latitude, longitude
# mais le code est le bon, on le prend quand même.
# Ceci peut arriver si l'API renvoie "Paris 01" par exemple.
elif normalized_input_name == "PARIS" and commune_info["code"] in ["75056", "75101", "75102", "75103", "75104", "75105", "75106", "75107", "75108", "75109", "75110", "75111", "75112", "75113", "75114", "75115", "75116", "75117", "75118", "75119", "75120"]:
# Note: Les codes 75101 à 75120 sont pour les arrondissements, mais l'API
# France Travail utilise souvent le 75056 pour "Paris" globalement.
# Cette condition est plus une sécurité, mais 75056 est la cible principale.
if commune_info["code"] == "75056": # On préfère le code global de Paris
found_code = commune_info["code"]
break
elif found_code is None: # Si on n'a pas encore trouvé 75056, on prend un arrondissement
found_code = commune_info["code"] # Conserver le code d'arrondissement si c'est le seul "Paris" trouvé
# Note: La logique ici est à affiner selon si vous voulez les arrondissements ou seulement le code global.
# Pour la plupart des cas, "75056" est suffisant.
if found_code: # If not found by name, try by postal code if commune_name looks like a postal code
logger.info(f"Code INSEE pour '{commune_name}' trouvé : {found_code}") if commune_name.isdigit() and len(commune_name) == 5:
return found_code geo_url_by_cp = f"https://geo.api.gouv.fr/communes?codePostal={commune_name}&fields=codesPostaux,code,nom,centre&format=json&limit=1"
logger.info(f"Recherche par code postal via API Geo.gouv.fr: {geo_url_by_cp}")
response = await self.geo_api_client.get(geo_url_by_cp)
response.raise_for_status()
communes_by_cp = response.json()
if communes_by_cp:
commune = communes_by_cp[0]
insee_code = commune['code']
postal_code = commune['codesPostaux'][0] if commune['codesPostaux'] else None
latitude = commune['centre']['coordinates'][1]
longitude = commune['centre']['coordinates'][0]
logger.info(f"Correspondance par code postal trouvée: INSEE='{insee_code}', CP='{postal_code}', Lat='{latitude}', Long='{longitude}' pour code postal '{commune_name}'.")
return insee_code, postal_code, latitude, longitude
logger.warning(f"Aucun code INSEE exact trouvé pour la commune '{commune_name}' parmi les résultats de l'API. Vérifiez l'orthographe.") logger.warning(f"Aucune correspondance trouvée pour la commune/code postal: '{commune_name}' dans l'API Geo.gouv.fr.")
return None return None
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
logger.error(f"Erreur HTTP lors de la récupération du code INSEE pour '{commune_name}': {e.response.status_code} - {e.response.text}") logger.error(f"Erreur HTTP lors de l'appel à l'API Geo.gouv.fr pour '{commune_name}': {e.response.status_code} - {e.response.text}")
return None return None
except Exception as e: except httpx.RequestError as e:
logger.error(f"Erreur inattendue lors de la récupération du code INSEE pour '{commune_name}': {e}") logger.error(f"Erreur réseau lors de l'appel à l'API Geo.gouv.fr pour '{commune_name}': {e}")
return None return None
async def search_offers(self, async def search_offers(
self,
motsCles: Optional[str] = None, motsCles: Optional[str] = None,
typeContrat: Optional[str] = None, commune: Optional[str] = None, # Reste le nom de la commune
codePostal: Optional[str] = None, codePostal: Optional[str] = None,
commune: Optional[str] = None, latitude: Optional[float] = None,
longitude: Optional[float] = None,
distance: Optional[int] = None, distance: Optional[int] = None,
alternance: Optional[bool] = None, # codeDepartement: Optional[str] = None, # Ce paramètre sera maintenant dérivé en interne
offresManagerees: Optional[bool] = None, range_start: int = 0,
range: str = "0-14") -> FranceTravailSearchResponse: range_end: int = 14,
typeContrat: Optional[List[str]] = None,
experience: Optional[List[str]] = None
) -> FranceTravailSearchResponse:
token = await self._get_access_token() token = await self._get_access_token()
headers = { headers = {
"Accept": "application/json", "Authorization": f"Bearer {token}",
"Authorization": f"Bearer {token}" "Content-Type": "application/json"
} }
params = { params: Dict[str, Any] = {
"range": range, "range": f"{range_start}-{range_end}"
} }
if motsCles: if motsCles:
params["motsCles"] = motsCles params["motsCles"] = motsCles
if typeContrat: if typeContrat:
params["typeContrat"] = typeContrat params["typeContrat"] = ','.join(typeContrat)
if alternance is not None: if experience:
params["alternance"] = str(alternance).lower() params["experience"] = ','.join(experience)
if offresManagerees is not None:
params["offresManagerees"] = str(offresManagerees).lower()
if codePostal: # Logique de localisation améliorée
# insee_code = None # Non utilisé directement comme paramètre pour l'API France Travail
# postal_code_for_api = None # Non utilisé directement comme paramètre pour l'API France Travail
# latitude_for_api = None # Non utilisé directement comme paramètre pour l'API France Travail
# longitude_for_api = None # Non utilisé directement comme paramètre pour l'API France Travail
# Le codeDepartement sera déterminé ici si une commune est fournie
derived_departement_code = None
if commune:
logger.info(f"Traitement de la commune spécifiée: '{commune}' pour dériver le département.")
geo_data = await self.get_insee_and_postal_code_for_commune(commune)
if geo_data:
insee_code, postal_code_from_geo, latitude_from_geo, longitude_from_geo = geo_data
# Dériver le code départemental du code postal ou INSEE
if postal_code_from_geo and len(postal_code_from_geo) >= 2:
derived_departement_code = postal_code_from_geo[:2]
# Cas spécifiques pour la Corse
if postal_code_from_geo.startswith('2A'):
derived_departement_code = '2A'
elif postal_code_from_geo.startswith('2B'):
derived_departement_code = '2B'
logger.info(f"Département dérivé de '{commune}': {derived_departement_code}")
# Si un département est dérivé, l'utiliser prioritairement
if derived_departement_code:
params["departement"] = derived_departement_code
logger.info(f"Paramètre 'departement' utilisé (dérivé de la commune): {derived_departement_code}")
elif latitude_from_geo and longitude_from_geo and distance is not None:
# Fallback sur latitude/longitude si la dérivation du département échoue
params["latitude"] = latitude_from_geo
params["longitude"] = longitude_from_geo
params["distance"] = distance
logger.info(f"Paramètres de localisation utilisés (dérivés de la commune): Latitude/Longitude et Distance: Lat={latitude_from_geo}, Long={longitude_from_geo}, Dist={distance}")
else:
logger.warning(f"Impossible de dériver le département ou d'obtenir des coordonnées valides pour la commune '{commune}'. Recherche sans localisation précise.")
else:
logger.warning(f"Impossible d'obtenir les données géographiques pour la commune '{commune}'. Recherche sans localisation précise.")
elif codePostal:
params["codePostal"] = codePostal params["codePostal"] = codePostal
if distance is not None: if distance is not None:
params["distance"] = distance params["distance"] = distance
else: logger.info(f"Paramètres de localisation utilisés: Code Postal={codePostal}, Distance={distance}")
params["distance"] = 10 elif latitude is not None and longitude is not None:
elif commune: params["latitude"] = latitude
params["commune"] = commune params["longitude"] = longitude
if distance is not None: if distance is not None:
params["distance"] = distance params["distance"] = distance
logger.info(f"Paramètres de localisation utilisés: Latitude/Longitude: Lat={latitude}, Long={longitude}, Dist={distance}")
else: else:
params["distance"] = 10 logger.warning("Aucun paramètre de localisation valide (commune, code postal, lat/long) n'a été spécifié. La recherche sera nationale.")
logger.info(f"Paramètres de recherche France Travail: {params}")
async with httpx.AsyncClient() as client: logger.info(f"Appel à l'API France Travail pour search_offers avec paramètres FINAUX: {params}")
try: try:
response = await client.get(f"{self.api_base_url}/v2/offres/search", headers=headers, params=params) response = await self.client.get("/v2/offres/search", headers=headers, params=params)
response.raise_for_status() response.raise_for_status()
logger.info(f"Réponse brute de l'API France Travail (search_offers): {response.json()}")
return FranceTravailSearchResponse(**response.json()) return FranceTravailSearchResponse(**response.json())
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
logger.error(f"Erreur HTTP lors de la recherche d'offres France Travail: {e.response.status_code} - {e.response.text}") logger.error(f"Échec de la recherche d'offres France Travail: {e.response.status_code} - {e.response.text}")
if e.response.status_code == 400 and "incorrect value" in e.response.text.lower() and "commune" in e.response.text.lower():
raise RuntimeError(f"L'API France Travail a renvoyé une erreur 400: La valeur du paramètre 'commune' est incorrecte. Veuillez vérifier le code INSEE ou envisager une recherche par département ou latitude/longitude.")
else:
raise RuntimeError(f"Échec de la recherche d'offres France Travail: {e.response.text}") raise RuntimeError(f"Échec de la recherche d'offres France Travail: {e.response.text}")
except Exception as e: except httpx.RequestError as e:
logger.error(f"Erreur inattendue lors de la recherche d'offres France Travail: {e}") logger.error(f"Erreur réseau lors de la recherche d'offres France Travail: {e}")
raise RuntimeError(f"Échec inattendu lors de la recherche d'offres France Travail: {e}") raise RuntimeError(f"Erreur réseau lors de la recherche d'offres France Travail: {e}")
async def get_offer_details(self, offer_id: str) -> OffreDetail: async def get_offer_details(self, offer_id: str) -> OffreDetail:
token = await self._get_access_token() token = await self._get_access_token()
headers = { headers = {
"Accept": "application/json", "Authorization": f"Bearer {token}",
"Authorization": f"Bearer {token}" "Content-Type": "application/json"
} }
logger.info(f"Appel à l'API France Travail pour get_offer_details avec id: {offer_id}")
async with httpx.AsyncClient() as client:
try: try:
response = await client.get(f"{self.api_base_url}/v2/offres/{offer_id}", headers=headers) response = await self.client.get(f"/v2/offres/{offer_id}", headers=headers)
response.raise_for_status() response.raise_for_status()
logger.info(f"Réponse brute de l'API France Travail (get_offer_details): {response.json()}")
return OffreDetail(**response.json()) return OffreDetail(**response.json())
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
logger.error(f"Erreur HTTP lors de la récupération des détails de l'offre {offer_id}: {e.response.status_code} - {e.response.text}") logger.error(f"Échec de la récupération des détails de l'offre {offer_id}: {e.response.status_code} - {e.response.text}")
raise RuntimeError(f"Échec de la récupération des détails de l'offre {offer_id}: {e.response.text}") raise RuntimeError(f"Échec de la récupération des détails de l'offre {offer_id}: {e.response.text}")
except Exception as e: except httpx.RequestError as e:
logger.error(f"Erreur inattendue lors de la récupération des détails de l'offre {offer_id}: {e}") logger.error(f"Erreur réseau lors de la récupération des détails de l'offre {offer_id}: {e}")
raise RuntimeError(f"Échec inattendu lors de la récupération des détails de l'offre {offer_id}: {e}") raise RuntimeError(f"Erreur réseau lors de la récupération des détails de l'offre {offer_id}: {e}")
france_travail_offer_service = FranceTravailOfferService() france_travail_offer_service = FranceTravailOfferService()

View file

@ -1,81 +1,62 @@
import httpx import httpx
import logging import logging
from typing import List, Dict, Any, Optional from typing import List, Dict, Any
from core.config import settings from core.config import settings
from services.oauth_service import OAuthService # Assurez-vous que ce service existe from services.france_travail_auth_service import france_travail_auth_service
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class RomeoService: class RomeoService:
def __init__(self): def __init__(self):
self.base_url = settings.FRANCE_TRAVAIL_ROMEO_API_URL # URL de base de l'API Romeo # CORRIGÉ ICI: Utilise 'FRANCE_TRAVAIL_ROMEO_API_URL' comme suggéré par l'erreur
self.scope = "api_romeov2" # Scope spécifique pour Romeo self.base_url = settings.FRANCE_TRAVAIL_ROMEO_API_URL
self.oauth_service = OAuthService(settings.FRANCE_TRAVAIL_OAUTH_URL, settings.FRANCE_TRAVAIL_CLIENT_ID, settings.FRANCE_TRAVAIL_CLIENT_SECRET) self._http_client = httpx.AsyncClient()
self.client = httpx.AsyncClient()
logger.info(f"RomeoService initialized with base_url: {self.base_url}") logger.info(f"RomeoService initialized with base_url: {self.base_url}")
async def _get_access_token(self) -> str: async def _call_api(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]:
"""Récupère le token d'accès spécifique à l'API Romeo."""
try:
token_response = await self.oauth_service.get_access_token(self.scope)
return token_response.access_token
except Exception as e:
logger.error(f"Erreur lors de la récupération du token d'accès pour Romeo: {e}")
raise RuntimeError(f"Impossible de récupérer le token d'accès pour Romeo: {e}")
async def _call_api(self, endpoint: str, text: str) -> Optional[Dict[str, Any]]:
""" """
Appelle un endpoint de l'API Romeo avec le texte donné. Appel générique à une endpoint de l'API Romeo.
Gère l'authentification et les erreurs de base. Récupère le jeton d'accès via le service d'authentification.
""" """
token = await self._get_access_token() access_token = await france_travail_auth_service.get_access_token()
headers = { headers = {
"Authorization": f"Bearer {token}", "Authorization": f"Bearer {access_token}",
"Content-Type": "application/json; charset=utf-8" "Content-Type": "application/json"
} }
# Les APIs Romeo attendent le texte dans un champ 'texte' de l'objet JSON
data = {"texte": text}
url = f"{self.base_url}{endpoint}" url = f"{self.base_url}{endpoint}"
logger.info(f"Calling Romeo API: {url} with text length {len(text)}")
logger.info(f"Appel API Romeo: {url} avec données: {data.keys()}")
try: try:
response = await self.client.post(url, headers=headers, json=data, timeout=30.0) response = await self._http_client.post(url, json=data, headers=headers)
response.raise_for_status() # Lève une exception pour les codes d'erreur HTTP (4xx ou 5xx) response.raise_for_status() # Lève une exception pour les codes d'état HTTP 4xx/5xx
logger.info(f"Réponse API Romeo reçue (status: {response.status_code}).")
return response.json() return response.json()
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
logger.error(f"Erreur HTTP lors de l'appel à Romeo {endpoint}: {e.response.status_code} - {e.response.text}") logger.error(f"Erreur HTTP lors de l'appel à l'API Romeo: {e.response.status_code} - {e.response.text}")
raise RuntimeError(f"Erreur de l'API Romeo: {e.response.text}") raise RuntimeError(f"Erreur lors de l'appel à l'API Romeo: {e.response.text}")
except httpx.RequestError as e:
logger.error(f"Erreur réseau ou de requête lors de l'appel à Romeo {endpoint}: {e}")
raise RuntimeError(f"Erreur de communication avec l'API Romeo: {e}")
except Exception as e: except Exception as e:
logger.error(f"Une erreur inattendue est survenue lors de l'appel à Romeo {endpoint}: {e}") logger.error(f"Erreur inattendue lors de l'appel à l'API Romeo: {e}")
raise RuntimeError(f"Erreur inattendue avec l'API Romeo: {e}") raise RuntimeError(f"Erreur inattendue lors de l'appel à l'API Romeo: {e}")
async def predict_metiers(self, text: str) -> List[Dict[str, Any]]: async def predict_metiers(self, text: str) -> List[Dict[str, Any]]:
""" """
Prédit les métiers ROME à partir d'un texte donné. Prédit les métiers ROME à partir d'un texte donné.
Retourne une liste de dictionnaires avec les détails des prédictions métiers.
""" """
if not text: endpoint = "/predire/metiers"
return [] # Retourne une liste vide si le texte est vide data = {"texte": text}
response_data = await self._call_api("/predictionMetiers", text) response_data = await self._call_api(endpoint, data)
# Romeo renvoie souvent une liste directe de prédictions si successful return response_data.get("predictions", [])
return response_data if response_data is not None else []
async def predict_competences(self, text: str) -> List[Dict[str, Any]]: async def predict_competences(self, text: str) -> List[Dict[str, Any]]:
""" """
Prédit les compétences ROME à partir d'un texte donné. Prédit les compétences ROME à partir d'un texte donné.
Retourne une liste de dictionnaires avec les détails des prédictions de compétences.
""" """
if not text: endpoint = "/predire/competences"
return [] # Retourne une liste vide si le texte est vide data = {"texte": text}
response_data = await self._call_api("/predictionCompetences", text) response_data = await self._call_api(endpoint, data)
# Romeo renvoie souvent une liste directe de prédictions si successful return response_data.get("predictions", [])
return response_data if response_data is not None else []
# Instanciation unique du service Romeo # Instanciation unique du service Romeo
romeo_service = RomeoService() romeo_service = RomeoService()

71
test/test_extraction.py Normal file
View file

@ -0,0 +1,71 @@
import os
import pypdf
import docx
import logging
# Configurez un logger simple pour le test
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Copiez/collez la fonction extract_text_from_file ici
def extract_text_from_file(filepath: str) -> str:
file_extension = os.path.splitext(filepath)[1].lower()
text_content = ""
if not os.path.exists(filepath):
raise FileNotFoundError(f"Le fichier n'existe pas : {filepath}")
if file_extension == ".pdf":
try:
with open(filepath, 'rb') as f:
reader = pypdf.PdfReader(f)
for page in reader.pages:
text_content += page.extract_text() or ""
if not text_content.strip():
logger.warning(f"Le fichier PDF {filepath} a été lu mais aucun texte significatif n'a été extrait.")
except Exception as e:
logger.error(f"Erreur lors de l'extraction du texte du PDF {filepath}: {e}")
raise ValueError(f"Impossible d'extraire le texte du fichier PDF. Erreur: {e}")
elif file_extension == ".docx":
try:
document = docx.Document(filepath)
for paragraph in document.paragraphs:
text_content += paragraph.text + "\n"
if not text_content.strip():
logger.warning(f"Le fichier DOCX {filepath} a été lu mais aucun texte significatif n'a été extrait.")
except Exception as e:
logger.error(f"Erreur lors de l'extraction du texte du DOCX {filepath}: {e}")
raise ValueError(f"Impossible d'extraire le texte du fichier DOCX. Erreur: {e}")
else: # Tente de lire comme un fichier texte
try:
with open(filepath, 'r', encoding='utf-8') as f:
text_content = f.read()
except UnicodeDecodeError:
# Si UTF-8 échoue, tente latin-1
try:
with open(filepath, 'r', encoding='latin-1') as f:
text_content = f.read()
except Exception as e:
logger.error(f"Erreur lors de la lecture du fichier texte {filepath} avec UTF-8 et Latin-1: {e}")
raise ValueError(f"Impossible de lire le fichier texte (problème d'encodage). Erreur: {e}")
except Exception as e:
logger.error(f"Erreur inattendue lors de la lecture du fichier texte {filepath}: {e}")
raise ValueError(f"Impossible de lire le fichier texte. Erreur: {e}")
return text_content
# Remplacez par le chemin réel de votre fichier CV
# Assurez-vous que le chemin est correct par rapport à l'emplacement de ce script de test
cv_file_path = "../backend/uploads/6efe8ef7-52ee-4a55-8d88-7f10875c44bb.pdf" # ou .docx, .txt
try:
extracted_text = extract_text_from_file(cv_file_path)
print("\n--- Texte extrait ---")
print(extracted_text)
print(f"\nLongueur du texte extrait: {len(extracted_text)} caractères")
if not extracted_text.strip():
print("AVERTISSEMENT: Le texte extrait est vide ou ne contient que des espaces blancs.")
except (FileNotFoundError, ValueError) as e:
print(f"ERREUR lors de l'extraction: {e}")
except Exception as e:
print(f"ERREUR INATTENDUE: {e}")