commit 6dbfe5cb0701b90029b68cf6859ecb84b4f0a88e Author: Eliyan Date: Mon Oct 13 12:33:20 2025 +0200 [INIT] innitial Poc of the concept for ql creation fron NL diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7780ba3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.env +.vscode/ +package-lock.json \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..be97e8f --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +# ODMDB Natural Language Query PoC + +This is a **Proof of Concept (PoC)** that demonstrates the conversion of natural language queries into ODMDB search queries using OpenAI's structured output API. + +## Current Status + +⚠️ **Partial Implementation**: Currently only the **seekers** object mapping is implemented. This PoC focuses on demonstrating the natural language to DSL query conversion for seeker-related searches. + +## Features + +- Converts natural language requests into ODMDB DSL queries +- Handles temporal queries ("new seekers since last week") +- Maps human-readable field names to schema fields +- Validates output using Zod schema validation +- Uses OpenAI's structured output for reliable JSON generation + +## Prerequisites + +- Node.js (v16 or higher) +- OpenAI API key + +## Installation + +1. Install dependencies: + ```bash + npm install + ``` + +2. Set your OpenAI API key: + ```bash + export OPENAI_API_KEY=sk-your-api-key-here + ``` + +## Usage + +### Running the PoC + +```bash +npm start +``` + +This will process the hardcoded natural language query and output the generated ODMDB query in JSON format. + +### Changing the Query + +To test different natural language queries, edit the `NL_QUERY` constant in `poc.js`: + +```javascript +// Line 16 in poc.js +const NL_QUERY = "your natural language query here"; +``` + +### Example Queries + +- `"give me new seekers since last week with email and experience"` +- `"find recent seekers with job titles and salary expectations"` +- `"show me seekers from yesterday with their skills"` + +## Output Format + +The PoC generates ODMDB queries in this format: + +```json +{ + "object": "seekers", + "condition": [ + "prop.dt_create(>=:2025-10-06)" + ], + "fields": [ + "alias", + "email", + "seekworkingyear" + ] +} +``` + +## ODMDB DSL Support + +The PoC understands and generates these ODMDB DSL patterns: + +- **Property queries**: `prop.(operator:value)` +- **Index queries**: `idx.(value)` +- **Join queries**: `join(remoteObject:localKey:remoteProp:operator:value)` + +## Field Mappings + +Currently supports mapping for seekers object: +- `email` → `email` +- `experience` → `seekworkingyear` +- `job titles` → `seekjobtitleexperience` +- `status` → `seekstatus` + +## Schema Context + +The PoC can optionally load schema files for context: +- `main.json` - Combined schema definitions +- `lg.json` - Localization/language mappings + +## Limitations + +- **Seekers only**: Other ODMDB objects (jobads, recruiters, etc.) are not yet implemented +- **No execution**: Only generates queries, doesn't execute them against ODMDB +- **Hardcoded query**: Single query per run (no interactive mode) +- **Basic validation**: Limited DSL syntax validation + +## Next Steps + +- [ ] Add support for other ODMDB objects (jobads, recruiters, etc.) +- [ ] Interactive CLI for multiple queries +- [ ] Integration with actual ODMDB backend +- [ ] Enhanced field mapping and validation +- [ ] Multi-turn conversation support + +## Files + +- `poc.js` - Main PoC implementation +- `package.json` - Dependencies and scripts +- `main.json` - Optional schema context (if available) +- `lg.json` - Optional localization context (if available) \ No newline at end of file diff --git a/lg.json b/lg.json new file mode 100644 index 0000000..469db5a --- /dev/null +++ b/lg.json @@ -0,0 +1,281 @@ +[ + { + "comment": "lg/hobbies_fr.json" + }, + { + "Lecture": [ + "Roman", + "Science-fiction", + "Policier", + "Biographie", + "Essai", + "Poésie", + "Magazine", + "Bandes dessinées" + ], + "Jardinage": [ + "Potager", + "Fleurs", + "Aménagement paysager", + "Bonsaï", + "Jardinage urbain", + "Jardins d'intérieur" + ], + "Cuisine": [ + "Pâtisserie", + "Cuisine internationale", + "Cuisine végétarienne", + "Cuisine fusion", + "Cuisine moléculaire", + "Cuisine de rue", + "Confitures et conserves" + ], + "Photographie": [ + "Paysages", + "Portraits", + "Macro", + "Photographie de rue", + "Photographie animalière", + "Photographie de mode", + "Photographie de mariage" + ], + "Peinture": [ + "Acrylique", + "Huile", + "Aquarelle", + "Pastel", + "Peinture sur verre", + "Peinture sur toile", + "Peinture abstraite" + ], + "Randonnée": [ + "Randonnée en montagne", + "Randonnée pédestre", + "Randonnée en forêt", + "Randonnée côtière", + "Randonnée en groupe", + "Randonnée nocturne" + ], + "Musique": [ + "Jouer d'un instrument", + "Chant", + "Composition", + "Musique classique", + "Musique rock", + "Musique jazz", + "Musique électronique" + ], + "Danse": [ + "Salsa", + "Bachata", + "Tango", + "Hip-hop", + "Danse contemporaine", + "Danse de salon", + "Danse orientale" + ], + "Écriture": [ + "Roman", + "Nouvelle", + "Poésie", + "Scénario", + "Blog", + "Journal intime", + "Lettres" + ], + "Bricolage": [ + "Menuiserie", + "Électricité", + "Plomberie", + "Décoration", + "Peinture", + "Couture", + "Restauration de meubles" + ], + "Jeux de société": [ + "Jeu de cartes", + "Jeu de plateau", + "Jeu de rôle", + "Jeu de stratégie", + "Jeu de dés", + "Jeu de société coopératif", + "Jeu de société d'ambiance" + ], + "Sports": [ + "Football", + "Basketball", + "Tennis", + "Natation", + "Course à pied", + "Yoga", + "Cyclisme" + ], + "Voyages": [ + "Voyages en Europe", + "Voyages en Asie", + "Voyages en Amérique", + "Voyages en Afrique", + "Voyages en Océanie", + "Voyages d'aventure", + "Voyages culturels" + ], + "Collection de timbres": [ + "Timbres classiques", + "Timbres thématiques", + "Timbres rares", + "Timbres du monde", + "Timbres oblitérés", + "Timbres neufs", + "Timbres anciens" + ], + "Couture": [ + "Vêtements", + "Accessoires", + "Patchwork", + "Couture pour enfants", + "Broderie", + "Travail du cuir", + "Customisation" + ], + "Tricot": [ + "Écharpes", + "Pulls", + "Chaussettes", + "Bonnet", + "Gants", + "Couvertures", + "Peluches" + ], + "Modélisme": [ + "Modélisme ferroviaire", + "Modélisme naval", + "Modélisme aérien", + "Modélisme automobile", + "Modélisme architectural", + "Modélisme spatial", + "Modélisme militaire" + ], + "Jeu d'échecs": [ + "Parties classiques", + "Parties rapides", + "Variantes", + "Études", + "Problèmes d'échecs", + "Compétitions", + "Analyse de parties" + ], + "Sculpture": [ + "Argile", + "Pierre", + "Bois", + "Métal", + "Verre", + "Céramique", + "Sculpture sur glace" + ], + "Camping": [ + "Camping en tente", + "Camping-car", + "Randonnée avec camping", + "Feu de camp", + "Cuisine en plein air", + "Observation des étoiles", + "Activités de plein air" + ] + }, + { + "comment": "lg/persons_fr.json" + }, + { + "title": "Une Personne au niveau d'une tribut avec des informations personnelles", + "description": "Un alias peut se stocker comme un objet Person avec des informations supplémentaires permettant de qualifier son profil", + "properties": { + "alias": { + "title": "Une identité numérique d'apxtri" + }, + "owner": { + "title": "Le propriétaire de cet objet (celui qui posséde la clé)" + }, + "dt_create": { + "title": "Date de creation" + }, + "dt_update": { + "title": "Date de mise à jour" + }, + "dt_lastlogin": { + "title": "Date de derniere authentification" + }, + "dt_delete": { + "title": "Date de fermeture du compte" + }, + "will": { + "title": "Nom du script à lancer lors d'une fermeture de compte" + }, + "recoveryauth": { + "title": "Information pour recuperer ses codes d'accès", + "description": "Cette objet garde votre identité numérique, en vue d'une demande de recuperation par email.", + "properties": { + "email": { + "title": "email de recuperation" + }, + "privatekey": { + "title": "La cle privée associé à l'alias" + }, + "passphrase": { + "title": "La passphrase eventuelle" + } + } + }, + "firstname": { + "title": "Votre prenom", + "description": "Ce prénom s'affichera pour les memebres de smatchit" + }, + "lastname": { + "title": "Votre nom de famille", + "description": "Ce nom s'affichera pour les membres de smatchit" + }, + "termandcondition": { + "title": "J'accepte les conditions d'utilisation de smatchit", + "description": "Conditions générales d'utilisation et de ventes de Smatchit" + }, + "truthfullinformation": { + "title": "I certify all my information", + "description": "I certify all my information is truthful" + }, + "contactfromschool": { + "title": "J'accepte de recevoir des offres de formations de nos partenaires", + "description": "Nous pouvons vous recommander des formations coorespondant à votre profil" + }, + "dt_birth": { + "title": "Votre date anniversaire", + "description": "Date de naissance" + }, + "pronom": { + "title": "Pronom", + "description": "La façon dont on doit s'adresser à votre personne" + }, + "emailcom": { + "title": "L'email de communication", + "description": "Cet email sera utilisé pour communiqué avec les membres de smatchot" + }, + "hobbies": { + "title": "Mes hobbies" + }, + "biography": { + "title": "Quelques mots sur moi", + "description": "Ce texte sera partagé pour tous les membres de smatchit, recruteur et chercheur d'emploi" + }, + "mbti": { + "title": "Mon profil mbti en tant que personne" + }, + "imgavatar": { + "title": "Une image public qui me represente", + "description": "Image qui en dit long sur ma personnalité, attention elle est public" + }, + "profils": { + "title": "Liste de mes profils", + "description": "Chaque profils donne des droits particulier à chaque personne sur les objet de smatchit" + } + } + } +] \ No newline at end of file diff --git a/main.json b/main.json new file mode 100644 index 0000000..b9ca69a --- /dev/null +++ b/main.json @@ -0,0 +1,2868 @@ +[ + { + "comment": "sirets.json" + }, + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "/smatchit/schema/sirets", + "title": "Siret is a legal french company", + "description": "A recruiter can active a jobad if a maxnumber is not reach for this siret. Maxnumber is set depending of an offer or can be set manualy", + "comment": "Available from pappers api https://suggestions.pappers.fr/v2?q=string", + "type": "object", + "properties": { + "siret": { + "title": "siret", + "description": "A unique string identifying a company ", + "type": "string", + "minLength": 14, + "pattern": "^[0-9]*$" + }, + "owner": { + "title": "Owner of this siret", + "description": "For accessright purpose this is set by the alias that pay the first time a subscription", + "type": "string" + }, + "dt_create": { + "type": "string", + "format": "date-time", + "default": "dayjs.now()" + }, + "dt_update": { + "type": "string", + "format": "date-time" + }, + "subscription": { + "title": "Offer subscribe", + "type": "array", + "items": { + "type": "object", + "properties": { + "offer": { + "type": "string", + "enum": ["A", "B", "C", "FREE"] + }, + "dt_payment": { + "type": "string", + "format": "date-time" + } + } + } + }, + "mbti": { + "title": "List of MBTI profil order by the most adapted for this siret", + "type": "array", + "enum": [ + "ISTJ", + "ESTJ", + "ISTP", + "ESTP", + "ISFJ", + "ESFJ", + "ISFP", + "ESFP", + "INFJ", + "ENFJ", + "INFP", + "ENFP", + "INTP", + "ENTP", + "INTJ", + "ENTJ" + ] + }, + "emailbilling": { + "type": "string", + "format": "email" + }, + "namebilling": { + "type": "string" + }, + "billinglocation": { + "title": "Billing Location", + "description": "", + "type": "object", + "$ref": "nationchains/schema/frenchlocation" + }, + "maxactivejobad": { + "title": "Number of active jobad at the same time", + "description": "Alloaw by subscription payment for a date inside the last dt_payment and offer", + "type": "integer" + }, + "activejobad": { + "title": "Current number of active jobad", + "type": "integer" + }, + "businesslocation": { + "title": "Siret Enterprise location", + "description": "", + "type": "object", + "$ref": "nationchains/schema/frenchlocation" + }, + "businessname": { + "title": "Company name (denomination)", + "comment": "available in pappers api https://suggestions.pappers.fr/v2?q=string", + "type": "string" + }, + "tradename": { + "title": "Company usual name", + "comment": "available in pappers api https://suggestions.pappers.fr/v2?q=string", + "type": "string" + }, + "code_naf": { + "title": "Code NAF French activity classification of the company", + "comment": "available in pappers api https://suggestions.pappers.fr/v2?q=string", + "type": "string" + }, + "category": { + "title": "Smatchit category", + "comment": "deduce from the 2 first number of code_naf", + "type": "string", + "options": { + "$ref": "smatchit/options/category" + } + }, + "tranche_effectif": { + "title": "Classification base on number of employe", + "comment": "available in pappers api https://suggestions.pappers.fr/v2?q=string", + "type": "string" + }, + "website": { + "title": "Website", + "type": "string", + "format": "url" + }, + "socialnetworks": { + "title": "Socialnetworks", + "type": "array", + "items": { + "type": "string", + "format": "url" + } + }, + "imgbase64_backgroundimage": { + "title": "Temporary background image for update or create", + "aspect": [4, 3], + "description": "Propertie use to upload a base64 imagae that will be store, only use to create or update. The image file will be store as /objects/sirets/{apxid.value}_backgroundimage.webp", + "type": "string", + "format": "imgbase64" + }, + "backgroundimage": { + "title": "A background image filename", + "type": "string" + }, + "imgbase64_companylogo": { + "title": "Temporary logo image for update or create", + "description": "Propertie use to upload a base64 imagae that will be store with name /objects/sirets/{siret]_companylogo.webp", + "aspect": [4, 3], + "type": "string", + "format": "imgbase64" + }, + "companylogo": { + "title": "The company logo image link", + "type": "string" + }, + "title": { + "title": "A catchy phrase about company", + "type": "string" + }, + "description": { + "title": "A description of the comany", + "type": "string" + }, + "whyworkhere": { + "title": "Why work for this company, will be generate later by an AI", + "type": "string" + }, + "agreetorespectnorms": { + "title": "Agree to respect the norms and regulation", + "type": "boolean" + }, + "recruiters": { + "title": "Recruiters allow to publish jobads for this siret", + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/recruiters/idx/lst_alias" + } + } + }, + "adminrecruiters": { + "title": "Recruiters administrator", + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/recruiters/idx/lst_alias" + } + } + } + }, + "required": ["siret"], + "additionalProperties": true, + "apximgpublicstorage": "", + "apxid": "siret", + "apxuniquekey": ["siret"], + "apxidx": [ + { + "name": "lst_siret", + "type": "array", + "keyval": "siret" + }, + { + "name": "tradename_siret", + "type": "distribution", + "keyval": "tradename", + "filter": "" + }, + { + "name": "businessname_siret", + "type": "distribution", + "keyval": "businessname", + "filter": "" + } + ], + "apxaccessrights": { + "owner": { + "D": [], + "R": [ + "siret", + "dt_create", + "dt_update", + "subscription", + "maxactivejobad", + "activejobad", + "businesslocation", + "demomination", + "businessname", + "tradename" + ], + "U": ["frenchlocation", "demomination", "businessname", "tradename"] + }, + "persons": { + "R": [ + "businesslocation", + "businessname", + "tradename", + "code_naf", + "category", + "tranche_effectif", + "website", + "socialnetworks", + "backgroundimage", + "companylogo", + "title", + "description" + ] + }, + "druids": { + "C": [], + "D": [], + "R": [ + "siret", + "dt_create", + "dt_update", + "subscription", + "maxactivejobad", + "activejobad", + "frenchlocation", + "demomination", + "businessname", + "tradename" + ], + "U": [ + "subscription", + "maxactivejobad", + "activejobad", + "frenchlocation", + "demomination", + "businessname", + "tradename" + ] + }, + "pagans": { + "C": [] + }, + "adminsmatchits": { + "C": [], + "U": [], + "D": [], + "R": [] + }, + "adminrecruiters": { + "C": [], + "R": [ + "siret", + "dt_create", + "dt_update", + "subscription", + "maxactivejobad", + "activejobad", + "businesslocation", + "businessname", + "tradename", + "code_naf", + "category", + "tranche_effectif", + "website", + "socialnetworks", + "backgroundimage", + "companylogo", + "title", + "description", + "recruiters", + "adminrecruiters" + ], + "U": [ + "billinglocation", + "website", + "socialnetworks", + "imgbase64_backgroundimage", + "imgbase64_companylogo", + "businesslocation", + "businessname", + "tradename", + "title", + "description", + "recruiters", + "jobadmodels" + ] + }, + "recruiters": { + "R": [ + "siret", + "dt_create", + "dt_update", + "subscription", + "maxactivejobad", + "activejobad", + "frenchlocation", + "demomination", + "businessname", + "tradename", + "recruiters" + ] + } + } + }, + { + "_comment": "trainingprovider.json" + }, + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "/schema/trainingprovider", + "title": "Provider of training programm", + "description": "", + "type": "object", + "properties": { + "idprovider": { + "title": " A unique string", + "description": "A unique string identifying a unique training provider", + "type": "string", + "minLength": 5, + "pattern": "^[a-z0-9]*$" + }, + "name": { + "title": "Name of ", + "type": "string" + }, + "squarelogo": { + "title": "A square logo to provide app", + "type": "string", + "format": "url" + }, + "description": { + "type": "string", + "description": "html content respecting smatchit css" + } + }, + "required": ["idprovider", "name", "squarelogo", "description"], + "additionnalProperties": true, + "apxid": "idprovider", + "apxuniquekey": ["idprovider"], + "apxidx": [ + { + "name": "lst_idprovider", + "keyval": "idprovider" + } + ] + }, + { + "comment": "jobsteps.json" + }, + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "/schema/jobsteps", + "title": "Manage a recruitment process for a jobad", + "description": "This is a list of step betwwen a seeker a recruiter and a jobad to follow the step by step from seeker apply to sign contract ", + "type": "object", + "properties": { + "jobstepid": { + "type": "string", + "comment": "alias_jobaduuid_jobstepposition" + }, + "jobadid": { + "type": "string", + "options": { + "$ref": "smatchit/objects/jobads/idx/lst_jobadid" + } + }, + "stepposition": { + "type": "integer", + "title": "position in jobadid.jobsteps from 0 to n equivalent to index" + }, + "owner": { + "title": "Owner of this jobsteps", + "description": "For accessright purpose owner is the seeker", + "type": "string", + "options": { + "$ref": "adminapi/objects/pagans/idx/lst_alias" + } + }, + "seeker": { + "type": "string", + "options": { + "$ref": "smatchit/objects/seekers/idx/lst_alias" + } + }, + "recruiternotification": { + "comment": "from recruiter settings notificationjobstep", + "type": "boolean" + }, + "recruiter": { + "type": "string", + "options": { + "$ref": "smatchit/objects/recruiters/idx/lst_alias" + } + }, + "interviewer": { + "title": "The recruiter that will manage the jobstep", + "comment": "Can be different than the recruiter in charge of the jobad", + "type": "string", + "options": { + "$ref": "smatchit/objects/recruiters/idx/lst_alias" + } + }, + "interviewernotification": { + "comment": "from recruiter settings notificationjobstep", + "type": "boolean" + }, + "dt_create": { + "type": "string", + "format": "date-time" + }, + "jobsteptype": { + "type": "string", + "options": { + "$ref": "smatchit/objects/options/jobsteptype" + } + }, + "title": { + "type": "string" + }, + "iconurl": { + "type": "string" + }, + "meetingformat": { + "title": "Meeting format", + "type": "array", + "items": { + "type": "string", + "enum": ["f2f", "visio", "test"] + } + }, + "selectedmeeting": { + "title": "Meeting format selected by the seeker for this jobstep", + "type": "string", + "enum": ["f2f", "visio", "test"] + }, + "jobsteplocation": { + "title": "Where is located the jobstep in face to face", + "type": "object", + "$ref": "adminapi/schema/frenchlocation" + }, + "seekeremail": { + "type": "string", + "format": "email" + }, + "seekerphone": { + "type": "string", + "format": "telephoneinter" + }, + "duration": { + "type": "integer", + "title": "Duration in minutes" + }, + "personnalmessage": { + "title": "Personnal message for seeker", + "type": "string" + }, + "interviewerfeedback": { + "title": "Text message to give a feedback", + "type": "string" + }, + "seekerfeedback": { + "type": "string", + "enum": ["good", "meh", "bad"] + }, + "recruiterfeedback": { + "type": "string", + "enum": ["good", "meh", "bad"] + }, + "recruiterevaluation": { + "type": "string" + }, + "offerstartingdate": { + "type": "string", + "format": "date-time" + }, + "state": { + "title": "state of a jobstep", + "description": " seeker create as apply => recruiter: apply->toschedule or apply->end => recruiter: toschedule->tobook => seeker tobook->booked => recruiter(interviewer) update recruiterfeedback => recruiter booked->end or booked->nextstep or booked->offer => seeker offer->contract or offer->declined ", + "type": "string", + "enum": [ + "apply", + "toschedule", + "tobook", + "booked", + "end", + "nextstep", + "offer", + "contract", + "declined" + ] + }, + "dt_book": { + "title": "Jobstep book for a date ", + "type": "string", + "format": "date" + }, + "starttime_book": { + "title": "Jobstep book for a starting time ", + "type": "string", + "format": "timehhmm" + }, + "endtime_book": { + "title": "Jobstep book for ending time ", + "type": "string", + "format": "timehhmm" + }, + "rescheduleby": { + "title": "The requester that ask to reschedule", + "type": "string", + "enum": ["seeker", "interviewer"] + }, + "previousdate": { + "title": "The previous booked date requested to reschedule", + "description": "store as dt_book starttime_book endtime_book", + "type": "string" + } + }, + "required": [ + "jobstepid", + "jobadid", + "stepposition", + "seeker", + "interviewer", + "recruiter", + "state" + ], + "additionalProperties": true, + "apxid": "jobstepid", + "apxuniquekey": ["jobstepsid"], + "apxidx": [ + { + "name": "lst_jobstepid", + "type": "array", + "keyval": "jobstepid" + }, + { + "name": "jobadid_jobstepid", + "type": "distribution", + "keyval": "jobadid" + } + ], + "apxaccessrights": { + "owner": { + "C": [], + "R": [], + "U": ["seekerfeedback", "selectedmeeting", "seekeremail", "seekerphone"] + }, + "adminsmatchits": { + "R": [], + "C": [], + "U": [], + "D": [] + }, + "seekers": { + "C": [], + "R": [ + "jobstepid", + "jobadid", + "stepposition", + "seeker", + "recruiter", + "recruiternotification", + "interviewer", + "interviewernotification", + "interviewercontact", + "title", + "file", + "description", + "meetingformat", + "selectedmeeting", + "personnalmessage", + "duration", + "seekerfeedback", + "jobsteplocation", + "recruiterfeedback", + "state", + "dt_status", + "dt_book", + "starttime_book", + "endtime_book" + ], + "U": [ + "seekerfeedback", + "selectedmeeting", + "seekeremail", + "seekerphone", + "jobsteplocation" + ], + "D": [] + }, + "recruiters": { + "C": [], + "R": [], + "U": [ + "interviewer", + "duration", + "recruiternotification", + "interviewernotification", + "meetingformat", + "jobsteplocation", + "personnalmessage", + "interviewersfeedback", + "recruiterfeedback", + "recruiterevaluation" + ] + } + } + }, + { + "comment": "jobtitles.json" + }, + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "/schema/jobtitles", + "title": "Jobtitle definition", + "description": "Jobtitle is a smatchit referential of job that classify seeker search and experience and jobad skill", + "type": "object", + "properties": { + "jobtitleid": { + "title": "Unique identification string only lowercase and number", + "type": "string", + "pattern": "^[a-z0-9]*$" + }, + "jobtitle": { + "title": "Job title label", + "type": "string" + }, + "class": { + "title": "Class of the jobtitle", + "type": "string" + }, + "dt_create": { + "type": "string", + "format": "date-time" + }, + "dt_update": { + "type": "string", + "format": "date-time" + }, + "dt_publish": { + "type": "string", + "format": "date-time" + }, + "dt_close": { + "type": "string", + "format": "date-time" + }, + "state": { + "type": "string", + "enum": ["draft", "inprocess", "closed"] + }, + "jobadtitle": { + "title": "List of similar jobtitle found in jobad", + "type": "array" + }, + "jobaddescription": { + "title": "Generic Job title description ", + "description": "Providing a complete job description of this jobtitle, this will be generate by IA based on skills and other jobad elements", + "comment": "@Sagar, is it possible to store html format or markdown ?", + "type": "string" + }, + "baselineskill": { + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/options/skills" + } + } + }, + "specificskill": { + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/options/skills" + } + } + }, + "languageskill": { + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/options/skills" + } + } + }, + "commonknowhows": { + "title": "List of personnality that is commonly provide for this jobtitles", + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/options/knowhow" + } + } + }, + "behavior": { + "title": "Behavior for this jobtitle", + "type": "array", + "items": { + "type": "object", + "required": ["behavior", "justification", "personalitytrait"], + "properties": { + "behavior": { + "type": "string", + "options": { + "$ref": "smatchit/objects/options/personalitytrait" + } + }, + "justification": { + "type": "string", + "description": "Texte to explain why this behavior for this jobtitle" + }, + "personalitytrait": { + "type": "array", + "description": "List of trait link to this behavior for the jobtitle", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/options/personalitytrait" + } + }, + "minItems": 1, + "uniqueItems": true + } + } + } + }, + "salarycriteria": { + "type": "array", + "items": { + "type": "object", + "properties": { + "critname": { + "title": "Name of discriminant criteria", + "type": "string" + }, + "salaryrecommanded": { + "type": "string" + }, + "critrules": { + "title": "Value of critname rules that return true/false", + "type": "string" + }, + "min": { + "title": "In cents of Euro per hour min found in jobad", + "type": "integer" + }, + "max": { + "title": "In cents of Euro per hour max found in jobad", + "type": "integer" + }, + "median": { + "title": "In cents of Euro per hour median found in jobad", + "type": "integer" + }, + "average": { + "title": "In cents of Euro per hour average found in jobad", + "type": "integer" + }, + "distribution": { + "title": "Market distribution per salary level", + "type": "array", + "items": { + "title": "Pourcent on 100 that have this range of salary", + "description": "1st = Number (base 100) from min to (max-min)/10, second from (max-min)/10 to ((max-min)/10)x2 ...", + "type": "integer" + } + } + } + } + } + }, + "required": [ + "jobtitleid", + "state", + "jobaddescription", + "salarycriteria", + "baselineskill", + "specificskill", + "languageskill", + "commonknowhows" + ], + "additionalProperties": true, + "apxid": "jobtitleid", + "apxuniquekey": ["jobtitleid"], + "apxidx": [ + { + "name": "lst_jobtitleid", + "type": "array", + "keyval": "jobtitleid" + }, + { + "name": "jobtitleid", + "type": "view", + "keyval": "jobtitleid", + "objkey": ["jobtitleid", "jobtitle", "state"] + }, + { + "name": "jobadtitle_jobtitleid", + "type": "distribution", + "keyval": "jobadtitle", + "filter": "" + } + ], + "apxaccessrights": { + "owner": { + "C": [], + "D": [], + "R": [], + "U": [] + }, + "persons": { + "R": [] + }, + "adminsmatchits": { + "R": [], + "C": [], + "D": [], + "U": [] + } + } + }, + { + "comment": "seekers.json" + }, + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "/smatchit/schema/seekers", + "title": "Data Profil of a person that is in a seek process", + "description": "All those data have to store any useffull logistical data and profil about a seeker (skill, ...) ", + "type": "object", + "properties": { + "alias": { + "title": "alias", + "description": "A unique string identifying a unique public key", + "type": "string", + "options": { + "$ref": "adminapi/objects/pagans/idx/lst_alias" + } + }, + "owner": { + "title": "Owner of this person", + "description": "For accessright purpose this is always equal at alias", + "type": "string", + "options": { + "$ref": "adminapi/objects/pagans/idx/lst_alias" + } + }, + "dt_create": { + "title": "Creation date", + "type": "string", + "format": "date-time" + }, + "dt_update": { + "type": "string", + "format": "date-time" + }, + "email": { + "title": "Email to use for seeker process", + "type": "string", + "format": "email" + }, + "shortdescription": { + "title": " A quick description of the seeker", + "type": "string" + }, + "matchinglastdate": { + "title": "Date of the last matching update", + "type": "string" + }, + "seekstatus": { + "title": "How fast do you want to find a job?", + "description": "Do you need a job quickly? Do you rather take your time to find you next move?", + "type": "string", + "enum": ["startasap", "norush", "notlooking"] + }, + "seekworkingyear": { + "title": "How many years of experience do you have?", + "description": "In general,how long have you been working?", + "type": "string", + "options": { + "$ref": "smatchit/objects/options/candidateexperience" + } + }, + "seekjobtitleexperience": { + "title": "Which jobs do you have experience with?", + "description": "Start typing to see the existing job titles", + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/jobtitles/idx/lst_jobtitleid" + } + } + }, + "seeklocation": { + "title": "Where do you want to work", + "description": "Set the base location and a radius where you want to work", + "type": "array", + "items": { + "type": "object", + "$ref": "adminapi/schema/frenchlocation" + } + }, + "salaryexpectation": { + "title": "What is your salary expectation?", + "description": "We need a base salary to find the right jobs for you.", + "comments": "to calculate salary in any periode in euro on the fly you have in options/salaryunit a salarycoef you can appy to convert in any way depending of salaryunit selected then if you want in year then you * (salaryunit.salarycoef of your target / salaryunit.salarycoef of the seeker selection) ", + "type": "integer" + }, + "salarydevise": { + "type": "string", + "enum": ["€", "$"] + }, + "salaryunit": { + "type": "string", + "options": { + "$ref": "smatchit/objects/options/salaryunit" + } + }, + "seekjobtype": { + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/options/jobtype" + } + } + }, + "mbti": { + "title": "MBTI profil", + "description": "", + "comments": "${(m.E>m.I)?'E':'I'}${(m.S>m.N)?'S':'N'}${(m.T>m.F)?'T':'F'}${(m.J>m.P)?'J':'P'}", + "type": "object", + "properties": { + "E": { + "type": "integer" + }, + "I": { + "type": "integer" + }, + "S": { + "type": "integer" + }, + "N": { + "type": "integer" + }, + "T": { + "type": "integer" + }, + "F": { + "type": "integer" + }, + "J": { + "type": "integer" + }, + "P": { + "type": "integer" + }, + "nextq": { + "type": "integer" + }, + "value": { + "type": "string", + "comments": "${(m.E>m.I)?'E':'I'}${(m.S>m.N)?'S':'N'}${(m.T>m.F)?'T':'F'}${(m.J>m.P)?'J':'P'}", + "pattern": "^(E|I)(N|S)(T|F)(J|P)$" + } + } + }, + "activequizz": { + "title": "Active quizz to get data from seeker, if seekermbti and seekerknohow was completed", + "type": "string", + "options": { + "$ref": "smatchit/objects/quizz/idx/lst_quizzid" + } + }, + "countryavailabletowork": { + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/options/countrytoworkin" + } + } + }, + "lastlocation": { + "type": "object", + "$ref": "adminapi/schema/frenchlocation" + }, + "employmentstatus": { + "title": "Your current employment status", + "type": "string", + "options": { + "$ref": "smatchit/objects/options/employmentstatus" + } + }, + "polemploiid": { + "title": "Pole emploi ID", + "type": "string" + }, + "tipsadvice": { + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/articles/idx/lst_articleid" + } + } + }, + "mywords": { + "title": "Generate from chatgpt that selected few knowhow or MBTI", + "type": "array", + "items": { + "type": "string" + } + }, + "myworkexperience": { + "title": "Genrate from chatgpt that generate list of key word about experience skills", + "type": "array", + "items": { + "type": "string" + } + }, + "lookingforaction": { + "title": "Generate from chatgpt as action", + "type": "array", + "items": { + "type": "string" + } + }, + "lookingforother": { + "title": "Generate from chatgpt as area or other", + "type": "array", + "items": { + "type": "string" + } + }, + "lookingforjobtype": { + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/options/jobtype" + } + } + }, + "myjobradar": { + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/jobtitle/idx/lst_jobtitleid" + } + } + }, + "myjobradarlist": { + "type": "array", + "items": { + "type": "string" + } + }, + "skills": { + "title": "List of seeker skills and its evaluation", + "description": "From my jobradar we get all the uniques specific skill and request seeker to autoevaluate himself. Example:{'dressertable':2,'rangersalle':4}", + "type": "object", + "properties": { + "*": { + "title": "The id of a skill from smatchit/objects/options/skills", + "type": "integer", + "enum": [0, 1, 2, 3, 4] + } + } + }, + "languageskills": { + "title": "List of seeker skills and its evaluation", + "description": "From my jobradar we get all the uniques specific skill and request seeker to autoevaluate himself. Example:{'dressertable':2,'rangersalle':4}", + "type": "object", + "properties": { + "*": { + "title": "The id of a skill from smatchit/objects/options/skills", + "type": "integer", + "enum": [0, 1, 2, 3, 4] + } + } + }, + "knowhow": { + "type": "object", + "properties": { + "nextq": { + "title": "next question not already answer", + "type": "integer" + }, + "*": { + "title": "Answers classification fromsmatchit/objects/options/knowhow", + "type": "object", + "properties": { + "sumweight": { + "type": "integer" + }, + "atypenumber": { + "type": "integer" + }, + "level": { + "type": "integer", + "enum": [0, 1, 2, 3, 4] + } + } + } + } + }, + "thingsilike": { + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/options/thingslikedislike" + } + } + }, + "thingsidislike": { + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/options/thingslikedislike" + } + } + }, + "preferedworkinghours": { + "title": "Seeker prefered working days hour from start to end", + "description": "Preference, start and end hour of a working day", + "comment": "Screen 8.3.7.1 EDIT PROFILE", + "type": "object", + "properties": { + "start": { + "description": "The starting time of the time slot in 'HH:MM' format.", + "type": "string", + "format": "time" + }, + "end": { + "description": "The ending time of the time slot in 'HH:MM' format.", + "type": "string", + "format": "time" + } + }, + "required": ["start", "end"] + }, + "notavailabletowork": { + "title": "Day for which seeker is not available to work", + "description": "This will be used for matching and avoid seeker that is not available", + "comment": "example [{'day': 'Monday','hours': [{'start': '00:00','end': '24:00'}]}] means not available the monday", + "type": "array", + "items": { + "type": "object", + "properties": { + "day": { + "type": "string", + "enum": [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday" + ] + }, + "hours": { + "type": "array", + "items": { + "type": "object", + "properties": { + "start": { + "type": "string", + "format": "time" + }, + "end": { + "type": "string", + "format": "time" + } + }, + "required": ["start", "end"] + } + } + }, + "required": ["day", "hours"] + } + }, + "jobadview": { + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/jobads/idx/lst_jobadid.json" + } + } + }, + "jobadapply": { + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/jobads/idx/lst_jobadid.json" + } + } + }, + "jobadinvitedtoapply": { + "title": "Job add a recruiter suggest this seeker to apply", + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/jobads/idx/lst_jobadid.json" + } + } + }, + "jobadsaved": { + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/jobads/idx/lst_jobadid.json" + } + } + }, + "jobadofferaccepted": { + "title": "Jobad offer and accepted by the seeker", + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/jobads/idx/lst_jobadid.json" + } + } + }, + "jobadprocessstopseeker": { + "title": " A jobad start but ended by seeker", + "type": "array", + "items": { + "type": "object", + "properties": { + "jobadid": { + "type": "string", + "options": { + "$ref": "smatchit/objects/jobads/lst_jobadid.json" + } + }, + "evaluation": { + "type": "array", + "items": { + "options": { + "title": "why declined", + "$ref": "smatchit/objects/options/evaluationbyseeker" + } + } + } + } + } + }, + "jobadprocessstoprecruiter": { + "title": "After MVP A jobad start but ended by recruiter", + "type": "array", + "items": { + "type": "object", + "properties": { + "jobadid": { + "type": "string", + "options": { + "$ref": "smatchit/objects/jobads/idx/lst_jobadid.json" + } + }, + "evaluation": { + "type": "array", + "items": { + "options": { + "title": "Must be filter by seeker options jobstepevaluation is awaiting validation", + "$ref": "smatchit/objects/options/jobstepevaluation" + } + } + } + } + } + }, + "jobadnotinterested": { + "title": "Not interested by a jobadinvitedtoapply and/or jobadmacthscore", + "type": "array", + "items": { + "type": "object", + "properties": { + "jobadid": { + "type": "string", + "options": { + "$ref": "smatchit/objects/jobads/idx/lst_jobadid.json" + } + }, + "evaluation": { + "type": "array", + "items": { + "options": { + "title": "Must be filter by seeker options jobstepevaluation is awaiting validation", + "$ref": "smatchit/objects/options/jobstepevaluation" + } + } + } + } + } + }, + "jobadmatchscore": { + "title": "List of best matching score", + "type": "array", + "items": { + "type": "object", + "properties": { + "jobadid": { + "type": "string", + "options": { + "$ref": "smatchit/objects/jobads/idx/lst_jobadid.json" + } + }, + "score": { + "title": "matching coefficient between jobad and seeker", + "type": "integer" + }, + "newmatch": { + "title": "New match detected", + "description": "During the matching process, rules must be defined in matching process", + "type": "boolean" + }, + "aboutyou": { + "type": "string", + "title": "Short text that explain why it matches" + }, + "criteria": { + "title": "List of string that highlight key points", + "type": "array", + "items": { + "type": "string" + } + }, + "whyworkhere": { + "title": "Why work here for this seeker", + "type": "string" + }, + "distanceseekerjob": { + "title": "Distance between seeker location and jobad location in km", + "type": "integer" + }, + "currentlyinprocess": { + "title": "If this is true then do not remove this item because we need information for a seeker", + "string": "boolean" + } + } + } + }, + "jobstepstodo": { + "title": "Jobstep for jobad applied", + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/jobsteps/idx/lst_jobstepid.json" + } + } + }, + "jobstepsdone": { + "title": "Jobstep for jobad applied", + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/jobsteps/idx/lst_jobstepid.json" + } + } + }, + "improvmentrequest": { + "title": "Improvment of application", + "description": " Suggestion are comming from options/improvesmatchit filter by suggestto.includes('seekers') but are not mandatatory, content expected is a string with a date prefix, example: 2024-01-31:blabla", + "type": "array", + "items": { + "type": "string" + } + }, + "educations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "diploma": { + "type": "string", + "options": { + "$ref": "smatchit/objects/options/diploma" + } + }, + "schoolname": { + "type": "string" + }, + "startyear": { + "type": "integer" + }, + "endyear": { + "type": "integer" + } + } + } + }, + "recommandation": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/recommandation" + } + }, + "receivecommercialtraining": { + "comment": "not use yet", + "type": "boolean" + }, + "receivejobandinterviewtips": { + "comment": "not use yet", + "type": "boolean" + }, + "notificationformatches": { + "type": "boolean" + }, + "notificationforsupermatches": { + "type": "boolean" + }, + "notificationinvitedtoapply": { + "type": "boolean" + }, + "notificationrecruitprocessupdate": { + "comment": "used to send notification in the jobstep notification, request to book", + "type": "boolean" + }, + "notificationdirectmessage": { + "comment": "Use in jobstep process if jobstep.satus is tobbook, booked, end, nextstep, offer, contract,declined,", + "type": "boolean" + }, + "emailactivityreportweekly": { + "comment": "not use yet", + "type": "boolean" + }, + "emailactivityreportbiweekly": { + "comment": "not use yet", + "type": "boolean" + }, + "emailactivityreportmonthly": { + "comment": "not use yet", + "type": "boolean" + }, + "emailpersonnalizedcontent": { + "comment": "not use yet", + "type": "boolean" + }, + "emailnewsletter": { + "comment": "not use yet", + "type": "boolean" + } + }, + "$defs": { + "recommandation": { + "$id": "#/recommandation", + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + }, + "phone": { + "type": "string", + "format": "telephoneinter" + }, + "alias": { + "type": "string", + "options": { + "$ref": "adminapi/objects/pagans/idx/lst_alias" + } + }, + "fisrtname": { + "type": "string" + }, + "lastname": { + "type": "string" + }, + "jobtitle": { + "type": "string" + }, + "description": { + "title": "Area of recomandation", + "description": "Describe why this recomandation is relevant to confirm your skills", + "type": "string" + } + } + } + }, + "required": [], + "additionalProperties": true, + "apxid": "alias", + "apxuniquekey": ["alias"], + "apxidx": [ + { + "name": "lst_alias", + "type": "array", + "keyval": "alias" + }, + { + "name": "seekstatus_alias", + "type": "distribution", + "keyval": "seekstatus", + "filter": "" + }, + { + "name": "alias", + "type": "view", + "keyval": "alias", + "objkey": [ + "alias", + "dt_create", + "dt_update", + "last_login", + "firstname", + "lastname", + "dt_birth", + "pronom", + "emailcom", + "hobbies", + "biography", + "imgavatar", + "profilaccess" + ], + "filter": "" + } + ], + "commentaccessrights": "Only a persons with seekers profil can create an item seekers. Any recruiters, recruitermanager and interviewers can read only ", + "apxaccessrights": { + "seekers": { + "C": [] + }, + "owner": { + "D": [], + "R": [], + "U": [ + "email", + "shortdescription", + "seekstatus", + "seekworkingyear", + "seekjobtitleexperience", + "seeklocation", + "salaryexpectation", + "salarydevise", + "salaryunit", + "seekjobtype", + "mbti", + "countryavailabletowork", + "lastlocation", + "employmentstatus", + "polemploiid", + "tipsadvice", + "mywords", + "myworkexperience", + "lookingforaction", + "lookingforother", + "lookingforjobtype", + "myjobradar", + "skills", + "languageskills", + "knowhow", + "preferedworkinghours", + "notavailabletowork", + "thingsilike", + "thingsidislike", + "jobadnotinterested", + "jobadapply", + "jobadinvitedtoapply", + "jobadsaved", + "educations", + "recommandation", + "receivecommercialtraining", + "receivejobandinterviewtips", + "notificationformatches", + "notificationforsupermatches", + "notificationinvitedtoapply", + "notificationrecruitprocessupdate", + "notificationupcominginterview", + "notificationdirectmessage", + "emailactivityreportweekly", + "emailactivityreportbiweekly", + "emailactivityreportmonthly", + "emailpersonnalizedcontent", + "emailnewsletter" + ] + }, + "recruiters": { + "R": [ + "email", + "shortdescription", + "seekstatus", + "seekworkingyear", + "seekjobtitleexperience", + "seeklocation", + "salaryexpectation", + "salarydevise", + "salaryunit", + "seekjobtype", + "mbti", + "countryavailabletowork", + "lastlocation", + "employmentstatus", + "polemploiid", + "tipsadvice", + "mywords", + "myworkexperience", + "lookingforaction", + "lookingforother", + "lookingforjobtype", + "myjobradar", + "skills", + "languageskills", + "knowhow", + "thingsilike", + "thingsidislike" + ] + }, + "adminsmatchits": { + "R": [], + "C": [], + "U": [], + "D": [] + }, + "mayors": { + "D": [], + "R": ["alias"] + }, + "druids": { + "D": [], + "R": ["alias"] + } + } + }, + { + "comment": "jobads.json" + }, + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "/schema/jobads", + "title": "Jobad definition", + "description": "Essential information to qualify a jobad needs", + "type": "object", + "properties": { + "jobadid": { + "type": "string", + "format": "uuid" + }, + "owner": { + "title": "Owner of this person", + "description": "For accessright purpose equal recruiter at the creation but can evolve", + "type": "string", + "options": { + "$ref": "adminapi/objects/pagans/idx/lst_alias" + } + }, + "dt_create": { + "type": "string", + "format": "date-time" + }, + "dt_update": { + "type": "string", + "format": "date-time" + }, + "dt_publish": { + "type": "string", + "format": "date-time" + }, + "dt_close": { + "type": "string", + "format": "date-time" + }, + "state": { + "title": "Status of a jobad that make is online or not", + "description": "At creation this must be set at draft, when it is finish an endpoint PUT api/smatchit/jobads/publish/{jobadid} that will check if sirets.activejobad < sirets.maxactivejobad. If yes then state = publish if no then state=ready, a jobad publish cannot be modify and it can only archive with endpoint /api/smatchit/jobads/archive/{jobadid} ( in unpublish button means state=archive (that will decrease sirets.activejobad)", + "type": "string", + "enum": ["draft", "publish", "ready", "archive"] + }, + "matchinglastdate": { + "title": "Date of the last matching update", + "type": "string" + }, + "jobtitle": { + "title": "Job title classification", + "type": "string", + "options": { + "$ref": "smatchit/objects/jobtitles/idx/lst_jobtitleid" + } + }, + "urgenthiring": { + "title": "If this jobad is urgent to provide", + "type": "boolean" + }, + "category": { + "title": "jobad classification", + "type": "string", + "options": { + "$ref": "smatchit/options/category" + } + }, + "jobadmbti": { + "title": "Generate by chatgpt after creation", + "type": "string" + }, + "jobadtitle": { + "title": "Title for the jobad", + "type": "string" + }, + "candidateexperience": { + "type": "string", + "options": { + "$ref": "smatchit/objects/options/candidateexperience" + } + }, + "fulltime": { + "type": "string", + "enum": ["full", "partial", "fullandpartial"] + }, + "remote": { + "type": "integer", + "title": "Remote", + "description": "-1 dont care 0 mean no remote, 50 means half a time in remote 100 mean full remote" + }, + "workingdayshours": { + "type": "array", + "items": { + "type": "object", + "properties": { + "day": { + "type": "string", + "enum": [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday" + ] + }, + "hours": { + "type": "array", + "items": { + "type": "object", + "properties": { + "start": { + "type": "string", + "format": "time" + }, + "end": { + "type": "string", + "format": "time" + } + }, + "required": ["start", "end"] + } + } + }, + "required": ["day", "hours"] + } + }, + "jobtype": { + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/options/jobtype" + } + } + }, + "description": { + "description": "A chatGPT text generate to describe the job for SEO purpose 'about the job'", + "type": "string", + "title": "Describe in few words job description " + }, + "idealcandidate": { + "description": "A chatGPT text generate to describe the ideal candidate for SEO purpose 'about you'", + "type": "string", + "title": "Describe in few words the ideal candidate" + }, + "recruiter": { + "type": "string", + "options": { + "$ref": "smatchit/objects/recruiters/idx/lst_alias" + } + }, + "jobadlocation": { + "title": "Where is located this jobad", + "type": "array", + "items": { + "type": "object", + "$ref": "adminapi/schema/frenchlocation" + } + }, + "siret": { + "type": "string", + "options": { + "$ref": "smatchit/objects/sirets/idx/lst_sirets" + } + }, + "specificskills": { + "title": "Evaluation of specific skill from jobtitle", + "type": "object", + "properties": { + "*": { + "title": "The id of a skill from smatchit/objects/options/skills", + "type": "integer", + "enum": [0, 1, 2, 3, 4] + } + } + }, + "languageskills": { + "title": "Evaluation of specific skill from jobtitle", + "type": "object", + "properties": { + "*": { + "title": "The id of a skill from smatchit/objects/options/skills", + "type": "integer", + "enum": [0, 1, 2, 3, 4] + } + } + }, + "knowhows": { + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/options/knowhow" + } + } + }, + "dealbreakers": { + "title": "dealbreakers", + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/options/dealbreaker" + } + } + }, + "salary": { + "title": "Salary", + "description": "Salary for this job.", + "type": "integer" + }, + "salarydevise": { + "type": "string", + "enum": ["€", "$"] + }, + "salaryunit": { + "type": "string", + "options": { + "$ref": "smatchit/objects/options/salaryunit" + } + }, + "critrulesalary": { + "type": "string" + }, + "jobsteps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "jobsteptype": { + "type": "string", + "options": { + "$ref": "smatchit/objects/options/jobsteptype" + } + }, + "interviewer": { + "type": "string", + "options": { + "$ref": "smatchit/objects/recruiters/idx/lst_alias" + } + }, + "email": { + "title": "Email in case alias is not known by this recruiter", + "type": "string", + "format": "email" + } + } + } + }, + "isPast": { + "title": "@Mit can you tell me what it is for?", + "type": "boolean" + }, + "savedseekers": { + "title": "Potential candidate matching this jobad a recruiter want to save", + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/seekers/idx/lst_alias" + } + } + }, + "seekermatchscore": { + "title": "List of best seekers matching score", + "type": "array", + "items": { + "type": "object", + "properties": { + "seeker": { + "type": "string", + "options": { + "$ref": "smatchit/objects/seekers/idx/lst_alias" + } + }, + "score": { + "title": "matching coefficient between jobad and seeker", + "type": "integer" + }, + "newmatch": { + "title": "New match detected", + "description": "During the matching process", + "type": "boolean" + }, + "aboutyoutitle": { + "type": "string", + "title": "Short title to present the candidate" + }, + "aboutyou": { + "type": "string", + "title": "Short text that explain why it matches" + }, + "criteria": { + "title": "List of string that highlight key points", + "type": "array", + "items": { + "type": "string" + } + }, + "distanceseekerjob": { + "title": "Distance between seeker location and jobad location in km", + "type": "integer" + }, + "currentlyinprocess": { + "title": "It this is true then do not remove because this item is currnetly use to show result for this jobad, for a jobsteps or whatever", + "type": "boolean" + } + } + } + } + }, + "required": [ + "recruiter", + "Jobadid", + "jobtitle", + "jobadtitle", + "siret", + "jobtitle", + "state", + "salary" + ], + "additionalProperties": true, + "apxid": "jobadid", + "apxuniquekey": ["jobadid"], + "apxidx": [ + { + "name": "lst_jobadid", + "type": "array", + "keyval": "jobadid" + }, + { + "name": "jobadid", + "type": "view", + "keyval": "jobadid", + "objkey": ["jobadid", "jobadtitle", "jobtitle", "state"] + }, + { + "name": "jobtype_jobadid", + "type": "distribution", + "keyval": "jobtype", + "filter": "" + }, + { + "name": "state_jobadid", + "type": "distribution", + "keyval": "state", + "filter": "" + }, + { + "name": "jobtitle_jobadid", + "type": "distribution", + "keyval": "jobtitle", + "filter": "" + }, + { + "name": "jobadid", + "type": "view", + "keyval": "jobadid", + "objkey": ["jobadid", "jobadtitle", "jobtitle", "state"] + } + ], + "apxaccessrights": { + "adminsmatchits": { + "R": [], + "C": [], + "U": [], + "D": [] + }, + "owner": { + "D": [], + "R": [], + "U": ["savedseekers"] + }, + "recruiters": { + "C": [], + "R": [] + }, + "seekers": { + "R": [] + } + } + }, + { + "comment": "screens.json" + }, + { + "$id": "https://smatchit.io/schema/screens", + "$comment": "To describe screens as tree to navigate inside", + "title": "Screens description", + "description": "Each propertie value is a mustache template string where a data must be provide to display screen with value", + "type": "object", + "properties": { + "screenid": { + "title": "Screen identification used in html tag id", + "type": "string" + }, + "title": { + "title": "A title in a screen", + "type": "string" + }, + "subtitle": { + "title": "A subtitle in a screen", + "type": "string" + }, + "icon": { + "title": "an icon name", + "type": "string" + }, + "warning": { + "title": "A text to highlight something, this text is between 2 ! icon", + "type": "string" + }, + "formcontrol": { + "title": "A key word to presents this content", + "type": "string", + "enum": ["squarebluebtn", "listbtn", "form"] + }, + "forms": { + "title": "Liste of data collection element into the screen", + "type": "array", + "items": { + "type": "objects" + } + }, + "action": { + "title": "List of possible action on this element", + "type": "string", + "enum": ["onclick"] + }, + "function": { + "title": "Function name to call, if action", + "comment": "other function than nextlevel", + "type": "string" + }, + "params": { + "title": " an object containning parameter to send to function", + "comment": "can be empty {}", + "type": "object" + }, + "nextlevel": { + "title": "List of new screens to show if function is nextlevel $ref:# means it same current schema", + "type": "array", + "items": { + "$ref": "#" + } + } + }, + "required": ["screenid", "title"], + "apxid": "screenid", + "apxuniquekey": ["screenid"], + "apxidx": [ + { + "name": "lst_screens", + "type": "array", + "keyval": "screenid" + }, + { + "name": "screens", + "type": "view", + "keyval": "screenid", + "objkey": [], + "filter": "" + } + ], + "apxaccessrights": { + "owner": { + "R": [], + "U": [], + "D": [] + }, + "anonymous": { + "R": [] + } + } + }, + { + "comment": "conf.json" + }, + { + "schema": "schema/", + "objects": [ + { + "name": "nations", + "lastversion": 0 + }, + { + "name": "paganss", + "lastversion": 0 + }, + { + "name": "towns", + "lastversion": 0 + }, + { + "name": "tribes", + "lastversion": 0 + } + ], + "comment": "schema are in english to get translate part a /lg/schemaname_lg.json allow to replace by lg language the relevant key. Each time a modification is done in schema lastupdate take a timestamp" + }, + { + "comment": "quizz.json" + }, + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "smatchit/schema/quizz", + "title": "Quizz object", + "description": "This describe a list of question answer and the way it is store as a result", + "type": "object", + "properties": { + "id": { + "title": "Quizz name", + "description": "A unique string identifying a set of question", + "type": "string", + "minLength": 3, + "pattern": "^[a-z0-9]*$" + }, + "title": { + "title": "Short text about the quizz", + "type": "string" + }, + "description": { + "title": "Info about this quizz", + "type": "string" + }, + "urlimg": { + "type": "string", + "format": "url", + "comment": "if an image exist per qlabelid and alabelid file is accessible into uri/{qlabelid}_{alabelid}.webp" + }, + "questions": { + "title": "Liste of question and option", + "type": "array", + "items": { + "type": "object", + "properties": { + "qlabelid": { + "type": "integer", + "comment": "from 0 to n" + }, + "qlabel": { + "type": "string" + }, + "answers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "alabelid": { + "type": "integer", + "comment": "from 0 to n" + }, + "alabel": { + "type": "string" + }, + "weight": { + "type": "integer" + }, + "atype": { + "type": "string" + }, + "img": { + "type": "string" + } + } + } + } + } + } + }, + "result": { + "title": "Way to store", + "type": "string" + } + }, + "required": ["id", "questions", "result"], + "apxid": "id", + "apxuniquekey": ["id"], + "apxidx": [ + { + "name": "lst_id", + "type": "array", + "keyval": "id" + } + ], + "apxaccessrights": { + "owner": { + "D": [], + "R": [], + "U": [] + }, + "persons": { + "R": [] + }, + "adminsmatchit": { + "C": [], + "R": [], + "U": [], + "D": [] + } + } + }, + { + "comment": "persons.json" + }, + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "/schema/person", + "title": "Person minimum definition to link a person to a pagan identity", + "description": "A person is a human with a apxtri identity (alias = Public Private Key) that accept to be part of a tribe (a person is store inside a tribe). Information stored for a person are only visible from the town's Mayor and the tribe's Druid. You need at least trust the druid that trust the mayor (for sensitive data Mayor and Druid can be the same apx Identity.) Only a pagan that have the privateKey can read cipher data. The purpose of this schema is to link a person to a tribe and manage basic activities, profil for specific purpose will be a tribe object that can be add additionalProperties of this is set at true.", + "type": "object", + "properties": { + "alias": { + "title": "alias", + "description": "A unique string identifying a unique public key", + "type": "string", + "options": { + "$ref": "adminapi/objects/pagans/idx/lst_alias" + } + }, + "owner": { + "title": "Owner of this person", + "description": "For accessright purpose this is always equal as alias", + "type": "string", + "options": { + "$ref": "adminapi/objects/pagans/idx/lst_alias" + } + }, + "dt_create": { + "title": "Creation date", + "type": "string", + "format": "date-time" + }, + "dt_update": { + "type": "string", + "format": "date-time" + }, + "dt_lastlogin": { + "title": "Last time login", + "description": "Last time this person authentify as alias access to this tribe", + "type": "string", + "format": "date-time" + }, + "dt_delete": { + "title": "Date of leaving tribe", + "description": "Date from when this alias is ban of tribe by druid or want to leave. A pocess of data cleaning has to be run depending of Tribe's rules.", + "type": "string", + "format": "date" + }, + "will": { + "title": "Will script after leaving tribe", + "description": "This will script will be apply on your data 30 days after your delete", + "type": "string" + }, + "recoveryauth": { + "title": "Store numeric identity to recover it by email", + "description": "This object store numeric identity alias with an email mainly used at Person level to recover by email a private and passphrase key associate to alias", + "type": "object", + "properties": { + "email": { + "title": "Recovery email", + "type": "string", + "format": "email" + }, + "privatekey": { + "title": "Private key link to alias", + "type": "string", + "format": "pgpprivatekey" + }, + "passphrase": { + "title": "Passphrase to uncipher privatekey", + "type": "string" + } + } + }, + "firstname": { + "title": "A firstname", + "description": "This will be use to present yourself to smatchit's member", + "type": "string" + }, + "lastname": { + "title": "A lastname", + "description": "This will be use to present yourself to smatchit's member", + "type": "string" + }, + "termandcondition": { + "title": "I accept the terms and conditions", + "description": "Accept conditions of smatchit", + "type": "boolean" + }, + "truthfullinformation": { + "title": "I certify all my information", + "description": "I certify all my information is truthful", + "type": "boolean" + }, + "tester": { + "title": "Tester Profile", + "description": "Used to mark if this profile was created by a team member for testing the real app", + "type": "boolean" + }, + "contactfromschool": { + "title": "I accept to be receibe contact from recommended school", + "description": "Scholl recommendataion acceptation", + "type": "boolean" + }, + "dt_birth": { + "title": "Your birthdate", + "description": "Date of birth you want to communicate", + "type": "string", + "format": "date" + }, + "firebaseid": { + "title": "Firebase unique ID", + "type": "string" + }, + "pronom": { + "title": "Your pronom", + "description": "The way you want people communicate with you", + "type": "string", + "options": { + "$ref": "smatchit/objects/options/pronom" + } + }, + "emailcom": { + "title": "email use to communicate with you", + "description": "email used by tribe to communicate with you, depending of your profil you can also define other mail to interact with other person", + "type": "string", + "format": "email" + }, + "hobbies": { + "title": "My hobbies", + "type": "array", + "comment": "from a tree word combinaison /lg/hobbies_xx" + }, + "biography": { + "title": "Your bio or few words to define yourself", + "description": "Use this to share your values, this will be public to all of tribe's members and link to your person", + "type": "string", + "pattern": "^[\\s\\S]{0,300}$" + }, + "imgbase64_imgavatar": { + "title": "Temporary img to update or create", + "description": "image will be store in /objects/persons/{alias}_imgavatar.webp", + "sizeHW": [80, 80], + "type": "string", + "format": "imgbase64" + }, + "mycurrentrole": { + "title": "Current role in my main company", + "description": "This is use to present myself to seeker or recruiter as a professionnal", + "type": "string" + }, + "imgavatar": { + "title": "A picture of your person or personnality", + "description": "This picture will be public to all tribe's member", + "type": "string", + "format": "uri" + }, + "profils": { + "title": "Array of profil", + "description": "List of profil to get accessright on object", + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/options/profil" + } + } + }, + "mylastprofil": { + "title": "My current profil", + "description": "A person can be seekers as well recruiters as well interviewers as well adminrecruiters, this propertie can save the last screen app profil used", + "type": "string", + "options": { + "$ref": "smatchit/objects/options/profil" + } + }, + "notificationbyemail": { + "title": "Accept notification by email", + "type": "boolean" + }, + "notificationbypush": { + "title": "Accept notification by push app", + "type": "boolean" + } + }, + "required": ["alias", "profils"], + "additionalProperties": true, + "apxref": [], + "apxid": "alias", + "apxuniquekey": ["alias"], + "apxidx": [ + { + "name": "lst_alias", + "type": "array", + "keyval": "alias" + }, + { + "name": "lst_profils", + "type": "array", + "keyval": "profils" + }, + { + "name": "alias", + "type": "view", + "keyval": "alias", + "objkey": [ + "alias", + "dt_create", + "dt_update", + "last_login", + "firstname", + "lastname", + "dt_birth", + "pronom", + "emailcom", + "hobbies", + "biography", + "imgavatar", + "profils" + ], + "filter": "" + }, + { + "name": "profils_alias", + "type": "distribution", + "keyval": "profils", + "filter": "" + }, + { + "name": "emailrecovery_alias", + "type": "distribution", + "keyval": "recoveryauth.email", + "filter": "" + }, + { + "name": "emailcom_alias", + "type": "distribution", + "keyval": "emailcom", + "filter": "" + }, + { + "name": "hobbies_alias", + "type": "distribution", + "keyval": "hobbies", + "filter": "" + } + ], + "commentaccessrights": "only a pagans can create a persons by joining the tribe smatchit. An adminrecruiters can Read person and update only profil by adding value recruiters or interviewers, that will create for this alias a recruiters itm or interviewers itm, use /api/smatchit/profilmanagers/join/:profils/:recruiteralias", + "apxaccessrights": { + "owner": { + "D": [], + "R": [ + "alias", + "owner", + "profils", + "dt_create", + "dt_update", + "dt_lastlogin", + "firstname", + "lastname", + "termandcondition", + "truthfullinformation", + "contactfromschool", + "dt_birth", + "firebaseid", + "pronom", + "emailcom", + "hobbies", + "biography", + "imgavatar", + "profils", + "mylastprofil", + "notificationbyemail", + "notificationbypush" + ], + "U": [ + "firstname", + "lastname", + "termandcondition", + "truthfullinformation", + "contactfromschool", + "dt_birth", + "firebaseid", + "pronom", + "emailcom", + "hobbies", + "biography", + "imgbase64_imgavatar", + "profils", + "mylastprofil", + "notificationbyemail", + "notificationbypush" + ] + }, + "pagans": { + "C": [] + }, + "recruiters": { + "R": [ + "firstname", + "lastname", + "contactfromschool", + "dt_birth", + "firebaseid", + "pronom", + "emailcom", + "hobbies", + "biography", + "imgavatar" + ] + }, + "adminsmatchit": { + "R": [], + "U": [] + }, + "mayors": { + "D": [], + "R": ["alias"] + }, + "druids": { + "D": [], + "R": ["alias"] + } + } + }, + { + "comment": "recruiters.json" + }, + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "smatchit/schema/recruiters", + "title": "", + "description": "A recruiter recrute for one or more siret, he create jobad, he update if a jobstep end or contnue or contract. Only an adminrecruiters can add or remove a sirets of a recruiter", + "type": "object", + "properties": { + "alias": { + "title": "alias", + "description": "A unique string identifying a unique public key", + "type": "string", + "options": { + "$ref": "adminapi/objects/pagans/idx/lst_alias" + } + }, + "owner": { + "title": "Owner of this person", + "description": "For accessright purpose this is always equal as alias", + "type": "string", + "options": { + "$ref": "adminapi/objects/pagans/idx/lst_alias" + } + }, + "dt_create": { + "title": "Creation date", + "type": "string", + "format": "date-time" + }, + "dt_update": { + "type": "string", + "format": "date-time" + }, + "sirets": { + "title": "Company recruiter recrute for self registration", + "description": "List of siret", + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/sirets/idx/lst_sirets" + } + } + }, + "email": { + "title": "Email to use for recruitment", + "type": "string", + "format": "email" + }, + "phone": { + "title": "Phone to use for recruitment", + "type": "string", + "format": "telephoneinter" + }, + "tipsadvice": { + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/articles/idx/lst_articleid" + } + } + }, + "jobads": { + "title": "List of active draft, ready and publish Jobad that this recruiter is in charge to manage", + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/jobads/idx/lst_jobadid" + } + } + }, + "archivejobads": { + "title": "List of archive Jobad (unpublished) that this recruiter is in charge to manage", + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/jobads/idx/lst_jobadid" + } + } + }, + "durationsetting": { + "title": "My standard interview duration", + "description": "My availableslot will be set with this parameter", + "type": "integer", + "enum": [30, 60, 90, 120] + }, + "starttimesetting": { + "title": "Starting time to set availableslot", + "type": "string", + "format": "time", + "default": "08:00" + }, + "endtimesetting": { + "title": "Ending time to set availableslot", + "type": "string", + "format": "time", + "default": "19:00" + }, + "availableslot": { + "title": "Interviewer availibility for the futur", + "type": "object", + "patternProperties": { + "d{4}-[01]d-[0-3]d": { + "title": "date", + "type": "object", + "patternProperties": { + "[0-2]d:[0-5]d:[0-5]d.d{1,3}": { + "title": "time key with value F for Free, B for Booked, J booked for a jobstep", + "type": "string" + } + } + } + } + }, + "availableslotold": { + "title": "A SUP My next available slot", + "description": "Next available slot only", + "type": "array", + "items": { + "type": "object", + "properties": { + "date": { + "title": "Day", + "format": "date" + }, + "startslot": { + "title": "First starting slot time of the day", + "type": "string", + "format": "time" + }, + "endslot": { + "title": "Last starting slot time of the day", + "type": "string", + "format": "time" + }, + "duration": { + "title": "Duration of each slot in minutes", + "type": "integer", + "enum": [30, 60, 90, 120] + }, + "breakbetweenslot": { + "title": "Time break between each slot in minutes", + "type": "integer", + "enum": [0, 5, 10, 15, 30] + }, + "slotavailable": { + "title": "list of available starting slot", + "type": "array", + "items": { + "title": "starting hour of an available slot", + "type": "string", + "format": "time" + } + } + } + } + }, + "jobstepstodo": { + "title": "Jobstep for jobad applied", + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/jobsteps/idx/lst_jobstepsid" + } + } + }, + "jobstepsdone": { + "title": "Jobstep for jobad applied", + "type": "array", + "items": { + "type": "string", + "options": { + "$ref": "smatchit/objects/jobsteps/idx/lst_jobstepsid" + } + } + }, + "improvmentrequest": { + "title": "Improvment of application", + "description": " Suggestion are comming from options/improvesmatchit filterbu suggestto.includes('recruiters') but are not mandatatory, content expected is a string with a date prefix, example: 2024-01-31:blabla", + "type": "array", + "items": { + "type": "string" + } + }, + "notificationjobstep": { + "comment": "use to follow the status of a jobstep", + "type": "boolean" + } + }, + "required": ["alias"], + "additionalProperties": true, + "apxref": [], + "apxid": "alias", + "apxuniquekey": ["alias"], + "apxidx": [ + { + "name": "lst_alias", + "type": "array", + "keyval": "alias" + }, + { + "name": "email_alias", + "type": "distribution", + "keyval": "email" + }, + { + "name": "sirets_alias", + "type": "view", + "keyval": "sirets" + } + ], + "apxaccessrights": { + "owner": { + "D": [], + "R": [], + "U": [ + "sirets", + "phone", + "email", + "jobstepsdone", + "jobstepstodo", + "availableslot", + "improvmentrequest", + "notificationjobstep" + ] + }, + "persons": { + "C": [] + }, + "adminrecruiters": { + "R": [], + "U": ["jobadmodel"] + }, + "adminsmatchits": { + "R": [], + "U": [] + }, + "seekers": { + "R": ["availableslot", "notificationjobstep"] + }, + "mayors": { + "D": [], + "R": ["alias"] + }, + "druids": { + "D": [], + "R": ["alias"] + } + } + } +] diff --git a/package.json b/package.json new file mode 100644 index 0000000..62f446b --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "nl2odmdb-poc", + "version": "0.1.0", + "type": "module", + "private": true, + "scripts": { + "start": "node poc.js" + }, + "dependencies": { + "openai": "^4.60.0", + "zod": "^3.23.8" + } +} diff --git a/poc.js b/poc.js new file mode 100644 index 0000000..9ec829a --- /dev/null +++ b/poc.js @@ -0,0 +1,159 @@ +// PoC: NL → ODMDB query (seekers) +// Usage: +// 1) export OPENAI_API_KEY=sk-... +// 2) node poc.js + +import fs from "node:fs"; +import OpenAI from "openai"; +import { z } from "zod"; + +// ---- Config ---- +const MODEL = process.env.OPENAI_MODEL || "gpt-5"; +const MAIN_SCHEMA_PATH = "./main.json"; // optional context; safe if missing +const LG_SCHEMA_PATH = "./lg.json"; // optional context; safe if missing + +// Hardcoded NL query for the PoC (no multi-turn) +const NL_QUERY = + "give me new seekers since last week with email and experience"; + +// ---- Load schemas if present (not required for output) ---- +function loadJsonSafe(path) { + try { + if (fs.existsSync(path)) { + return JSON.parse(fs.readFileSync(path, "utf-8")); + } + } catch {} + return null; +} +const SCHEMAS = { + main: loadJsonSafe(MAIN_SCHEMA_PATH), + lg: loadJsonSafe(LG_SCHEMA_PATH), +}; + +// ---- Seekers mapping (from our agreement) ---- +const seekersMapping = { + object: "seekers", + readableFieldsForRecruiters: [ + "alias", + "email", + "seekstatus", + "seekworkingyear", + "seekjobtitleexperience", + ], +}; + +// ---- Output contract (strict) ---- +const OdmdbQueryZ = z.object({ + object: z.literal("seekers"), + condition: z.array(z.string()), + fields: z.array(z.string()), // always an array +}); + +// JSON Schema for Structured Output +const RESPONSE_JSON_SCHEMA = { + type: "object", + additionalProperties: false, + properties: { + object: { type: "string", enum: ["seekers"] }, + condition: { type: "array", items: { type: "string" } }, + fields: { type: "array", items: { type: "string" }, minItems: 1 }, + }, + required: ["object", "condition", "fields"], +}; + +// ---- Prompt builders ---- +function systemPrompt() { + return [ + "You convert a natural language request into an ODMDB search payload.", + "Return ONLY a compact JSON object that matches the provided JSON Schema. The 'fields' property MUST be an array of strings.", + "", + "ODMDB DSL:", + "- join(remoteObject:localKey:remoteProp:operator:value)", + "- idx.(value)", + "- prop.(operator:value) with dates or scalars.", + "", + "Rules:", + "- Object must be 'seekers'.", + "- For 'new'/'recent' recency, map to prop.dt_create with a resolved absolute date.", + "- For 'experience', map to seekworkingyear.", + "- Prefer recruiter-readable fields if a small set is requested. If the request is generic, return this default shortlist:", + seekersMapping.readableFieldsForRecruiters.join(", "), + "", + "Timezone is Europe/Paris. Today is 2025-10-13.", + "Interpret 'last week' as now minus 7 days → 2025-10-06.", + "", + "Schemas (context only, may be null):", + JSON.stringify(SCHEMAS, null, 2), + ].join("\n"); +} +function userPrompt(nl) { + return `Natural language request: "${nl}"\nReturn ONLY the JSON object.`; +} + +// ---- OpenAI call using Responses API (text.format) ---- +const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); + +async function inferQuery(nlText) { + const resp = await client.responses.create({ + model: MODEL, + input: [ + { role: "system", content: systemPrompt() }, + { role: "user", content: userPrompt(nlText) }, + ], + text: { + // <= new location for structured output format + format: { + name: "OdmdbQuery", + type: "json_schema", + schema: RESPONSE_JSON_SCHEMA, + strict: true, + }, + }, + }); + + const jsonText = + resp.output_text || + resp.output?.[0]?.content?.[0]?.text || + (() => { + throw new Error("Empty model output"); + })(); + + const parsed = JSON.parse(jsonText); + const validated = OdmdbQueryZ.parse(parsed); + + // Light safety check on DSL tokens + const allowed = ["join(", "idx.", "prop."]; + for (const c of validated.condition) { + const ok = allowed.some((t) => c.includes(t)); + const ascii = /^[\x09\x0A\x0D\x20-\x7E()_:\[\].,=> { + try { + if (!process.env.OPENAI_API_KEY) { + throw new Error("Missing OPENAI_API_KEY env var."); + } + + const out = await inferQuery(NL_QUERY); + + // Just output the created query (no execution) + console.log( + JSON.stringify( + { + object: out.object, + condition: out.condition, + fields: out.fields, + }, + null, + 2 + ) + ); + } catch (e) { + console.error("PoC failed:", e.message || e); + process.exit(1); + } +})();