departements
This commit is contained in:
parent
6b53a419c9
commit
4c180fe1f8
19 changed files with 21999 additions and 431 deletions
2833
backend/api/la_bonne_boite_bundled.json
Normal file
2833
backend/api/la_bonne_boite_bundled.json
Normal file
File diff suppressed because it is too large
Load diff
3405
backend/api/marche_du_travail_bundled.json
Normal file
3405
backend/api/marche_du_travail_bundled.json
Normal file
File diff suppressed because it is too large
Load diff
2463
backend/api/offres_emploi_v2_bundled.json
Normal file
2463
backend/api/offres_emploi_v2_bundled.json
Normal file
File diff suppressed because it is too large
Load diff
4017
backend/api/rome_4_competences.json
Normal file
4017
backend/api/rome_4_competences.json
Normal file
File diff suppressed because it is too large
Load diff
674
backend/api/rome_4_fiches_rome.json
Normal file
674
backend/api/rome_4_fiches_rome.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4015
backend/api/rome_4_metiers.json
Normal file
4015
backend/api/rome_4_metiers.json
Normal file
File diff suppressed because it is too large
Load diff
358
backend/api/rome_4_situations_de_travail.json
Normal file
358
backend/api/rome_4_situations_de_travail.json
Normal 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 l’emploi à 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 d’accueil. \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
811
backend/api/romeo_v2.json
Normal file
|
@ -0,0 +1,811 @@
|
||||||
|
{
|
||||||
|
"openapi": "3.0.3",
|
||||||
|
"info": {
|
||||||
|
"version": "2.0",
|
||||||
|
"title": "Romeo",
|
||||||
|
"description": "Utilisez l’IA 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 d’un texte libre renseigné.\r\n\r\n\r\n\r\nCas d'usage possibles :\r\n\r\n- Rapprocher une compétence d’un CV à des savoirs / savoir-faire du ROME 4.0\r\n\r\n- Rapprocher une compétence d’un 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 l‘appellation métier du ROME la plus proche à partir d’un texte libre renseigné.\r\n\r\n\r\n\r\nCas d'usage possibles\r\n- Rapprocher un intitulé d’offre d’emploi à des appellations métier du ROME 4.0\r\n- Rapprocher un intitulé d’expérience d’un 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, l’intitulé \"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 d’une 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 l’IA 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 d’activité. 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, l’intitulé \"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 d’une 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2781
backend/api/sortants_de_formation_et_acces_a_lemploi_bundled.json
Normal file
2781
backend/api/sortants_de_formation_et_acces_a_lemploi_bundled.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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()
|
|
@ -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é.")
|
||||||
|
|
|
@ -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")
|
|
@ -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}"
|
||||||
)
|
)
|
|
@ -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."""
|
||||||
|
|
|
@ -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()
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
data = {
|
token_url_with_realm,
|
||||||
|
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()
|
|
@ -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
71
test/test_extraction.py
Normal 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}")
|
Loading…
Add table
Add a link
Reference in a new issue