├── .gitignore ├── bot ├── commands │ ├── error.js │ ├── github.js │ ├── start.js │ ├── aboutme.js │ ├── profile.js │ └── jobs.js ├── bot.js ├── errorHandling.js ├── sendMarkdown.js └── onMessage.js ├── .prettierrc.json ├── .eslintrc.json ├── ai ├── openai.js └── actions │ ├── joke.js │ ├── opinion.js │ ├── username.js │ ├── help.js │ ├── searchQuery.js │ ├── summarizer.js │ └── chat.js ├── .env.example ├── db ├── db.js └── models │ └── user.js ├── .github └── workflows │ └── main.yml ├── infojobs ├── login.js ├── token.js ├── profile.js ├── curriculum.js └── offers.js ├── github └── technologies.js ├── package.json ├── LICENSE ├── index.js ├── cron └── cron.js ├── readme.md └── language └── messages.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | npm-debug.log* 3 | yarn-debug.log* 4 | yarn-error.log* 5 | /dist 6 | .env 7 | .idea 8 | .vscode 9 | *.swp 10 | *.swo -------------------------------------------------------------------------------- /bot/commands/error.js: -------------------------------------------------------------------------------- 1 | const bot = require('../bot'); 2 | module.exports = () => { 3 | bot.on('polling_error', (error) => { 4 | console.log(error); 5 | }); 6 | }; 7 | -------------------------------------------------------------------------------- /bot/bot.js: -------------------------------------------------------------------------------- 1 | const TelegramBot = require('node-telegram-bot-api'); 2 | 3 | const bot = new TelegramBot(process.env.TELEGRAM_TOKEN, { polling: true }); 4 | 5 | module.exports = bot; 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 150, 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 4, 6 | "trailingComma": "none", 7 | "useTabs": true, 8 | "bracketSameLine": true, 9 | "bracketSpacing": true 10 | } 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parserOptions": { 4 | "ecmaVersion": 12, 5 | "sourceType": "module" 6 | }, 7 | "extends": ["eslint:recommended", "prettier"], 8 | "env": { 9 | "es2021": true, 10 | "node": true 11 | }, 12 | "rules": {} 13 | } 14 | -------------------------------------------------------------------------------- /ai/openai.js: -------------------------------------------------------------------------------- 1 | const { Configuration, OpenAIApi } = require('openai'); 2 | 3 | const openAIToken = process.env.OPENAI_TOKEN ?? ''; 4 | 5 | const configuration = new Configuration({ apiKey: openAIToken }); 6 | const ai = new OpenAIApi(configuration); 7 | 8 | module.exports = ai; 9 | -------------------------------------------------------------------------------- /bot/errorHandling.js: -------------------------------------------------------------------------------- 1 | const messages = require('../language/messages.json'); 2 | const sendMarkdownMessage = require('./sendMarkdown'); 3 | 4 | async function errorHandling(chatId, error) { 5 | console.error(error); 6 | await sendMarkdownMessage(chatId, messages.errorMessage); 7 | } 8 | 9 | module.exports = errorHandling; 10 | -------------------------------------------------------------------------------- /bot/sendMarkdown.js: -------------------------------------------------------------------------------- 1 | const bot = require('./bot'); 2 | 3 | async function sendMarkdownMessage(chatId, message) { 4 | try { 5 | await new Promise((resolve) => setTimeout(resolve, 2000)); 6 | await bot.sendMessage(chatId, message, { parse_mode: 'HTML' }); 7 | } catch (err) { 8 | console.error('Error sending message: ', err); 9 | } 10 | } 11 | 12 | module.exports = sendMarkdownMessage; 13 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT=5000 2 | TELEGRAM_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 3 | MONGODB_URI="mongodb+srv://:@xxx/yyy" 4 | MONGODB_USERNAME=xxx 5 | MONGODB_PASSWORD=yyy 6 | OPENAI_TOKEN=zzz 7 | AI_MODEL=gpt-3.5-turbo 8 | OPEN_AI_RATE_LIMIT_RETRIES=5 9 | OPEN_AI_DELAY_BETWEEN_RETRIES=20000 10 | INFOJOBS_CLIENT_ID=xxx 11 | INFOJOBS_CLIENT_SECRET=yyy 12 | INFOJOBS_REDIRECT_URI=xxxx/infojobs/callback 13 | GITHUB_API_TOKEN=xxx 14 | -------------------------------------------------------------------------------- /db/db.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const connectDB = async () => { 4 | let dbUrl = process.env.MONGODB_URI; 5 | dbUrl = dbUrl.replace('', process.env.MONGODB_USERNAME).replace('', process.env.MONGODB_PASSWORD); 6 | try { 7 | mongoose.set('strictQuery', false); 8 | await mongoose.connect(dbUrl, { 9 | useNewUrlParser: true, 10 | useUnifiedTopology: true 11 | }); 12 | console.log('MongoDB connection SUCCESS'); 13 | } catch (error) { 14 | console.log(error); 15 | console.error('MongoDB connection FAIL'); 16 | process.exit(1); 17 | } 18 | }; 19 | 20 | module.exports = connectDB; 21 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Deploy PD Infojobs Hackaton 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | Deploy: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: ENTRECOT DEPLOY 13 | uses: D3rHase/ssh-command-action@v0.2.2 14 | with: 15 | host: ${{secrets.HOST}} 16 | user: ${{secrets.USERNAME}} 17 | private_key: ${{secrets.KEY}} 18 | command: | 19 | cd infojobs-hackaton && 20 | git pull origin main && 21 | npm prune && 22 | npm install && 23 | pm2 restart infojobs-hackaton 24 | -------------------------------------------------------------------------------- /infojobs/login.js: -------------------------------------------------------------------------------- 1 | const INFOJOBS_API_LOGIN_URL = "https://www.infojobs.net/api/oauth/user-authorize/index.xhtml?scope=MY_APPLICATIONS,CANDIDATE_PROFILE_WITH_EMAIL,CANDIDATE_READ_CURRICULUM_SKILLS,CV,CANDIDATE_READ_CURRICULUM_EXPERIENCE&client_id=##CLIENT_ID##&redirect_uri=##CALLBACK_URL##&response_type=code&state=##CHAT_ID##"; 2 | 3 | async function getLoginUrl(chatId) { 4 | let callbackUrl = INFOJOBS_API_LOGIN_URL; 5 | callbackUrl = callbackUrl.replace('##CLIENT_ID##', process.env.INFOJOBS_CLIENT_ID); 6 | callbackUrl = callbackUrl.replace('##CALLBACK_URL##', process.env.INFOJOBS_REDIRECT_URI); 7 | callbackUrl = callbackUrl.replace('##CHAT_ID##', chatId); 8 | return callbackUrl; 9 | } 10 | 11 | module.exports = { 12 | getLoginUrl 13 | }; 14 | -------------------------------------------------------------------------------- /github/technologies.js: -------------------------------------------------------------------------------- 1 | const GITHUB_REPOS_URL = "https://api.github.com/users/##USERNAME##/repos"; 2 | 3 | async function getGithubUsedTechnologies(username) { 4 | const url = GITHUB_REPOS_URL.replace('##USERNAME##', username); 5 | const headers = { 6 | Authorization: `token ${process.env.GITHUB_API_TOKEN}`, 7 | }; 8 | 9 | try { 10 | const response = await fetch(url, { headers }); 11 | const repos = await response.json(); 12 | 13 | const languages = repos.map(repo => repo.language); 14 | const uniqueLanguages = [...new Set(languages)]; 15 | return uniqueLanguages.join(', ').replaceAll(', ,', ','); 16 | } catch (error) { 17 | console.error(error); 18 | } 19 | } 20 | 21 | module.exports = { 22 | getGithubUsedTechnologies 23 | }; 24 | -------------------------------------------------------------------------------- /infojobs/token.js: -------------------------------------------------------------------------------- 1 | const INFOJOBS_API_GET_TOKEN = "https://www.infojobs.net/oauth/authorize?grant_type=authorization_code&client_id=##CLIENT_ID##&client_secret=##CLIENT_SECRET##&code=##CODE##&redirect_uri=##CALLBACK_URL##"; 2 | 3 | async function getToken(chatId, code) { 4 | 5 | let tokenUrl = INFOJOBS_API_GET_TOKEN; 6 | tokenUrl = tokenUrl.replace('##CODE##', code); 7 | tokenUrl = tokenUrl.replace('##CLIENT_ID##', process.env.INFOJOBS_CLIENT_ID); 8 | tokenUrl = tokenUrl.replace('##CLIENT_SECRET##', process.env.INFOJOBS_CLIENT_SECRET); 9 | tokenUrl = tokenUrl.replace('##CALLBACK_URL##', process.env.INFOJOBS_REDIRECT_URI); 10 | 11 | const res = await fetch(tokenUrl, { 12 | method:"POST" 13 | }) 14 | 15 | const data = await res.json(); 16 | return data.access_token; 17 | } 18 | 19 | module.exports = { 20 | getToken 21 | }; 22 | -------------------------------------------------------------------------------- /infojobs/profile.js: -------------------------------------------------------------------------------- 1 | const CLIENT_ID = process.env.INFOJOBS_CLIENT_ID; 2 | const CLIENT_SECRET = process.env.INFOJOBS_CLIENT_SECRET; 3 | const BASIC_TOKEN = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString( 4 | "base64" 5 | ); 6 | 7 | const PROFILE_URL = 'https://api.infojobs.net/api/6/candidate'; 8 | 9 | async function getInfojobsProfile(token) { 10 | 11 | const res = await fetch(PROFILE_URL, { 12 | headers: { 13 | 'Content-Type': 'application/json', 14 | Authorization: `Basic ${BASIC_TOKEN}, Bearer ${token}` 15 | } 16 | }); 17 | 18 | const json = await res.json(); 19 | 20 | if (!json) { 21 | throw new Error('Profile not found'); 22 | } 23 | 24 | const profile = { 25 | name: `${json.name} ${json.surname1}`, 26 | city: json.city 27 | } 28 | 29 | return profile; 30 | } 31 | 32 | module.exports = { 33 | getInfojobsProfile 34 | }; -------------------------------------------------------------------------------- /db/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const UserSchema = new mongoose.Schema({ 4 | chatId: { 5 | type: Number, 6 | required: true, 7 | unique: true 8 | }, 9 | executing: { 10 | type: Boolean, 11 | default: false 12 | }, 13 | firstName: String, 14 | lastName: String, 15 | aboutMe: String, 16 | age: Number, 17 | city: String, 18 | category: String, 19 | position: String, 20 | experienceYears: Number, 21 | teleworking: String, 22 | remote: Boolean, 23 | keywords: String, 24 | willingToRelocate: Boolean, 25 | relocationCities: [String], 26 | sentJobOffersIds: [String], 27 | score: Number, 28 | recomendation: String, 29 | availableForJobSearch: { 30 | type: Boolean, 31 | default: false 32 | }, 33 | canSendDailyJobOffers: { 34 | type: Boolean, 35 | default: true 36 | }, 37 | registeredAt: { 38 | type: Date, 39 | default: Date.now 40 | } 41 | }); 42 | 43 | const User = mongoose.model('users', UserSchema); 44 | 45 | module.exports = User; 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "infojobs-hackaton", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint .", 8 | "format": "prettier --write .", 9 | "dev": "npm run lint && npm run format && nodemon index.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/ericrisco/infojobs-hackaton.git" 14 | }, 15 | "keywords": [], 16 | "author": "", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/ericrisco/infojobs-hackaton/issues" 20 | }, 21 | "homepage": "https://github.com/ericrisco/infojobs-hackaton#readme", 22 | "dependencies": { 23 | "axios": "1.4.0", 24 | "dotenv": "16.0.3", 25 | "express": "4.18.2", 26 | "mongoose": "7.1.1", 27 | "node-cron": "^3.0.2", 28 | "node-telegram-bot-api": "0.61.0", 29 | "openai": "^3.2.1" 30 | }, 31 | "devDependencies": { 32 | "eslint": "8.40.0", 33 | "eslint-config-prettier": "8.8.0", 34 | "nodemon": "2.0.22", 35 | "prettier": "2.8.8" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /bot/commands/github.js: -------------------------------------------------------------------------------- 1 | const sendMarkdownMessage = require('../sendMarkdown'); 2 | const messages = require('../../language/messages.json'); 3 | const util = require('util'); 4 | const getUsername = require('../../ai/actions/username'); 5 | const { getGithubUsedTechnologies } = require('../../github/technologies'); 6 | const aboutMe = require('./aboutme'); 7 | 8 | async function extractTechnologies(chatId, message) { 9 | 10 | var github = await getUsername(chatId, message); 11 | if(github.username) { 12 | var technologies = await getGithubUsedTechnologies(github.username); 13 | if(technologies){ 14 | await sendMarkdownMessage(chatId, util.format(messages.extractedTechnologies, technologies)); 15 | await aboutMe(chatId, `he trabajado con ${technologies}`, true); 16 | }else{ 17 | await sendMarkdownMessage(chatId, messages.githubTechnologiesNotFound); 18 | } 19 | }else{ 20 | await sendMarkdownMessage(chatId, github.message); 21 | } 22 | } 23 | 24 | module.exports = { 25 | extractTechnologies 26 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Eric Risco de la Torre 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bot/commands/start.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | const User = require('../../db/models/user'); 3 | const messages = require('../../language/messages.json'); 4 | const sendMarkdownMessage = require('../sendMarkdown'); 5 | const bot = require('../bot'); 6 | const errorHandling = require('../errorHandling'); 7 | 8 | module.exports = () => { 9 | bot.onText(/\/start/, async (msg) => { 10 | const chatId = msg.chat.id; 11 | const { first_name, last_name } = msg.from; 12 | 13 | try { 14 | let user = await User.findOne({ chatId }); 15 | if (user) { 16 | const welcomeBackMessage = user.firstName === '' ? messages.welcomeBackNoName : util.format(messages.welcomeBack, user.firstName); 17 | await sendMarkdownMessage(chatId, welcomeBackMessage); 18 | 19 | if (!user.availableForJobSearch) { 20 | await sendMarkdownMessage(chatId, messages.giveMeInfo); 21 | await sendMarkdownMessage(chatId, messages.goDescription); 22 | } 23 | } else { 24 | let newUser = new User({ 25 | chatId, 26 | firstName: first_name, 27 | lastName: last_name 28 | }); 29 | await newUser.save(); 30 | 31 | await sendMarkdownMessage(chatId, messages.help); 32 | await sendMarkdownMessage(chatId, messages.exampleProfile); 33 | } 34 | } catch (err) { 35 | errorHandling(chatId, err); 36 | } 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const express = require('express'); 3 | const cron = require('node-cron'); 4 | const url = require('url'); 5 | 6 | const app = express(); 7 | 8 | const port = process.env.PORT || 3000; 9 | 10 | const connectDB = require('./db/db'); 11 | 12 | const startCommand = require('./bot/commands/start'); 13 | const onMessage = require('./bot/onMessage'); 14 | const errorCommand = require('./bot/commands/error'); 15 | const dailyOffer = require('./cron/cron'); 16 | const { remoteProfile } = require('./bot/commands/profile'); 17 | const aboutMe = require('./bot/commands/aboutme'); 18 | 19 | startCommand(); 20 | onMessage(); 21 | errorCommand(); 22 | 23 | cron.schedule('0 9 * * *', function () { 24 | console.log('Running cron job'); 25 | dailyOffer(); 26 | }); 27 | 28 | app.get('/infojobs/ping', (req, res) => { 29 | res.json({ success: true }); 30 | }); 31 | 32 | app.get('/infojobs/callback', async (req, res) => { 33 | const queryObject = url.parse(req.url,true).query; 34 | const code = queryObject.code; 35 | const state = queryObject.state; 36 | const profile = await remoteProfile(state, code); 37 | await aboutMe(state, JSON.stringify(profile)); 38 | res.send(` 39 | 40 | 41 | 44 | 45 | 46 | 47 | 48 | `); 49 | }); 50 | 51 | app.listen(port, async () => { 52 | console.log(`Server running on http://localhost:${port}`); 53 | await connectDB(); 54 | }); 55 | -------------------------------------------------------------------------------- /cron/cron.js: -------------------------------------------------------------------------------- 1 | const User = require('../db/models/user'); 2 | const sendMarkdownMessage = require('../bot/sendMarkdown'); 3 | const messages = require('../language/messages.json'); 4 | const { getLastOffersByUser } = require('../infojobs/offers'); 5 | const { printJobs } = require('../bot/commands/jobs'); 6 | 7 | const dailyOffer = async () => { 8 | const users = await User.find({ availableForJobSearch: true, canSendDailyJobOffers: true }); 9 | 10 | if (!users || users.length === 0) return; 11 | 12 | console.log(`Sending daily offer to ${users.length} users`); 13 | 14 | for (let i = 0; i < users.length; i++) { 15 | const user = users[i]; 16 | var offers = await getLastOffersByUser(user, 20); 17 | console.log(`Sending daily offer to ${user.chatId}`); 18 | console.log(`Found ${offers.totalResults} offers`); 19 | 20 | if (!offers || offers.length === 0) { 21 | sendMarkdownMessage(user.chatId, messages.noDailyOffer); 22 | return; 23 | } 24 | 25 | const offersIds = offers.items.map((offer) => offer.id); 26 | const userSentJobOffersIds = user.sentJobOffersIds; 27 | let notSentId = offersIds.find((offerId) => !userSentJobOffersIds.includes(offerId)); 28 | 29 | if (notSentId) { 30 | console.log(`Sending daily offer ${notSentId} to ${user.chatId}`); 31 | const notSentOffer = offers.items.find((offer) => offer.id === notSentId); 32 | offers.items = [notSentOffer]; 33 | await printJobs(user.chatId, offers, true); 34 | 35 | await new Promise((resolve) => setTimeout(resolve, 60000)); 36 | 37 | await sendMarkdownMessage(user.chatId, messages.dailyOffer); 38 | 39 | user.sentJobOffersIds.push(notSentId); 40 | await user.save(); 41 | } else { 42 | console.log(`No daily offer to ${user.chatId}`); 43 | sendMarkdownMessage(user.chatId, messages.noDailyOffer); 44 | } 45 | } 46 | }; 47 | 48 | module.exports = dailyOffer; 49 | -------------------------------------------------------------------------------- /ai/actions/joke.js: -------------------------------------------------------------------------------- 1 | const { ChatCompletionRequestMessageRoleEnum } = require('openai'); 2 | 3 | const ai = require('../openai'); 4 | const sendMarkdownMessage = require('../../bot/sendMarkdown'); 5 | const AI_MODEL = process.env.AI_MODEL ?? ''; 6 | const OPEN_AI_RATE_LIMIT_RETRIES = process.env.OPEN_AI_RATE_LIMIT_RETRIES ?? 5; 7 | const OPEN_AI_DELAY_BETWEEN_RETRIES = process.env.OPEN_AI_DELAY_BETWEEN_RETRIES ?? 20000; 8 | const messages = require('../../language/messages.json'); 9 | 10 | const INITIAL_MESSAGES = [ 11 | { 12 | role: ChatCompletionRequestMessageRoleEnum.System, 13 | content: ` 14 | Quiero que escojas una sola tematica entre las siguientes y te inventes un chiste creativo y original: 15 | """ 16 | - Buscar trabajo 17 | - Entrevistas de trabajo 18 | - Curriculum 19 | - Trabajo remoto 20 | - Trabajo freelance 21 | - Trabajo en equipo 22 | - Trabajo en equipo remoto 23 | - Inteligencia artificial 24 | """ 25 | Puedes usar el humor que quieras, pero no seas ofensivo. 26 | 27 | Solo contestame con un solo chiste. Si no me gusta, te lo hare saber. Si me gusta, te lo hare saber tambien. 28 | ` 29 | } 30 | ]; 31 | 32 | async function getJoke(chatId, attempts = OPEN_AI_RATE_LIMIT_RETRIES) { 33 | try { 34 | const completion = await ai.createChatCompletion({ 35 | model: AI_MODEL, 36 | temperature: 0.8, 37 | messages: [...INITIAL_MESSAGES] 38 | }); 39 | 40 | const data = completion.data.choices[0].message?.content ?? ''; 41 | return data; 42 | } catch (err) { 43 | if (err.response.status === 429 && attempts > 0) { 44 | sendMarkdownMessage(chatId, messages.giveMeTime); 45 | await new Promise((resolve) => setTimeout(resolve, OPEN_AI_DELAY_BETWEEN_RETRIES)); 46 | return getJoke(chatId, attempts - 1); 47 | } 48 | return messages.aiError; 49 | } 50 | } 51 | 52 | module.exports = getJoke; 53 | -------------------------------------------------------------------------------- /bot/commands/aboutme.js: -------------------------------------------------------------------------------- 1 | const User = require('../../db/models/user'); 2 | const sendMarkdownMessage = require('../sendMarkdown'); 3 | const messages = require('../../language/messages.json'); 4 | const util = require('util'); 5 | const errorHandling = require('../errorHandling'); 6 | const getAboutMeSummarized = require('../../ai/actions/summarizer'); 7 | 8 | async function aboutMe(chatId, text, modify = false) { 9 | if (text) { 10 | try { 11 | const user = await User.findOne({ chatId }); 12 | const message = modify ? `${user.aboutMe} ${text}` : text; 13 | await User.updateOne({ chatId }, { aboutMe: message }); 14 | 15 | await sendMarkdownMessage(chatId, messages.giveMeTime); 16 | 17 | const summary = await getAboutMeSummarized(chatId, message); 18 | 19 | if (summary.error) { 20 | await sendMarkdownMessage(chatId, summary.message); 21 | } else { 22 | const summaryMessage = util.format( 23 | messages.userProfileSummary, 24 | summary.age || 'N/A', 25 | summary.city || 'N/A', 26 | summary.position || 'N/A', 27 | summary.experienceYears || 'N/A', 28 | summary.remote ? 'Sí' : 'No', 29 | summary.keywords || 'N/A', 30 | summary.willingToRelocate ? 'Sí' : 'No', 31 | summary.relocationCities ? summary.relocationCities.join(', ') : 'N/A', 32 | summary.recomendation || 'N/A' 33 | ); 34 | 35 | await sendMarkdownMessage(chatId, summaryMessage); 36 | 37 | const score = summary.score; 38 | const availableForJobSearch = score >= 6; 39 | 40 | if (!availableForJobSearch) { 41 | await sendMarkdownMessage(chatId, messages.incompleteProfileMessage); 42 | } 43 | await sendMarkdownMessage(chatId, messages.exampleModify); 44 | 45 | summary.score = score; 46 | summary.availableForJobSearch = availableForJobSearch; 47 | 48 | await User.findOneAndUpdate({ chatId }, { ...summary }); 49 | } 50 | } catch (err) { 51 | errorHandling(chatId, err); 52 | } 53 | } 54 | } 55 | 56 | module.exports = aboutMe; 57 | -------------------------------------------------------------------------------- /ai/actions/opinion.js: -------------------------------------------------------------------------------- 1 | const { ChatCompletionRequestMessageRoleEnum } = require('openai'); 2 | 3 | const ai = require('../openai'); 4 | const sendMarkdownMessage = require('../../bot/sendMarkdown'); 5 | const AI_MODEL = process.env.AI_MODEL ?? ''; 6 | const OPEN_AI_RATE_LIMIT_RETRIES = process.env.OPEN_AI_RATE_LIMIT_RETRIES ?? 5; 7 | const OPEN_AI_DELAY_BETWEEN_RETRIES = process.env.OPEN_AI_DELAY_BETWEEN_RETRIES ?? 20000; 8 | const messages = require('../../language/messages.json'); 9 | 10 | const INITIAL_MESSAGES = [ 11 | { 12 | role: ChatCompletionRequestMessageRoleEnum.System, 13 | content: `Imagina que eres un experto comparando ofertas de trabajo. Yo te voy a pasar el perfil de un posible candidato en formato json y la oferta de trabajo que ha encontrado 14 | tambien en formato json. 15 | 16 | Tu trabajo sera crear un texto en primera persona diciendo si el candidato es apto para el puesto de trabajo o no dando razones de peso y si le recomiendas presentarse a la oferta o no. 17 | 18 | También le puedes proponer al usuario que podria hacer, por ejemplo estudiar, para poder presentarse y darle alguna recomendación. 19 | 20 | Devuelve solo el texto, nada mas, no hace falta que saludes. Un texto de máximo 200 letras. Usa algun emoji cuando lo veas necesario y utiliza una entonación amigable y de motivación. 21 | ` 22 | } 23 | ]; 24 | 25 | async function getOpinion(user, offer, attempts = OPEN_AI_RATE_LIMIT_RETRIES) { 26 | try { 27 | const completion = await ai.createChatCompletion({ 28 | model: AI_MODEL, 29 | temperature: 0, 30 | messages: [ 31 | ...INITIAL_MESSAGES, 32 | { 33 | role: ChatCompletionRequestMessageRoleEnum.User, 34 | content: `profile: ${JSON.stringify(user)} offer: ${JSON.stringify(offer)}` 35 | } 36 | ] 37 | }); 38 | 39 | const data = completion.data.choices[0].message?.content ?? ''; 40 | return data; 41 | } catch (err) { 42 | if (err.response.status === 429 && attempts > 0) { 43 | sendMarkdownMessage(user.chatId, messages.giveMeTime); 44 | await new Promise((resolve) => setTimeout(resolve, OPEN_AI_DELAY_BETWEEN_RETRIES)); 45 | return getOpinion(user, offer, attempts - 1); 46 | } 47 | return messages.aiError; 48 | } 49 | } 50 | 51 | module.exports = getOpinion; 52 | -------------------------------------------------------------------------------- /infojobs/curriculum.js: -------------------------------------------------------------------------------- 1 | const CLIENT_ID = process.env.INFOJOBS_CLIENT_ID; 2 | const CLIENT_SECRET = process.env.INFOJOBS_CLIENT_SECRET; 3 | const BASIC_TOKEN = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString( 4 | "base64" 5 | ); 6 | 7 | const CURRICULUM_URL = 'https://api.infojobs.net/api/2/curriculum'; 8 | const CURRICULUM_EXPERIENCE_URL = 'https://api.infojobs.net/api/2/curriculum/##CODE##/experience'; 9 | const CURRICULUM_SKILLS_URL = 'https://api.infojobs.net/api/2/curriculum/##CODE##/skill'; 10 | 11 | async function getPrincipalCurriculumId(token) { 12 | 13 | const res = await fetch(CURRICULUM_URL, { 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | Authorization: `Basic ${BASIC_TOKEN}, Bearer ${token}` 17 | } 18 | }); 19 | 20 | const json = await res.json(); 21 | if (json.error) return null; 22 | 23 | const cv = json.filter((curriculum) => curriculum.principal === true)[0]; 24 | 25 | if (json.error) return null; 26 | 27 | return cv.code; 28 | } 29 | 30 | async function getExperiences(token, curriculumId) { 31 | 32 | const url = CURRICULUM_EXPERIENCE_URL.replace('##CODE##', curriculumId); 33 | const res = await fetch(url, { 34 | headers: { 35 | 'Content-Type': 'application/json', 36 | Authorization: `Basic ${BASIC_TOKEN}, Bearer ${token}` 37 | } 38 | }); 39 | 40 | const json = await res.json(); 41 | if (json.error) return null; 42 | 43 | 44 | let experiences = json.experience.map((experience) => { 45 | const job = experience.job; 46 | const categories = experience.subcategories.join(", "); 47 | return `${job}, ${categories}`; 48 | }); 49 | 50 | experiences = experiences.join(", "); 51 | let words = experiences.split(" "); 52 | let uniqueExperiences = Array.from(new Set(words)); 53 | uniqueExperiences = uniqueExperiences.join(", ").replaceAll(', ,', ',');; 54 | 55 | return uniqueExperiences; 56 | }; 57 | 58 | async function getSkills(token, curriculumId) { 59 | 60 | const url = CURRICULUM_SKILLS_URL.replace('##CODE##', curriculumId); 61 | const res = await fetch(url, { 62 | headers: { 63 | 'Content-Type': 'application/json', 64 | Authorization: `Basic ${BASIC_TOKEN}, Bearer ${token}` 65 | } 66 | }); 67 | 68 | const json = await res.json(); 69 | 70 | if (json.error) return null; 71 | 72 | let expertises = json.expertise.map((expertises) => { 73 | return expertises.skill; 74 | }); 75 | expertises = expertises.join(", ").replaceAll(', ,', ',');; 76 | 77 | return expertises; 78 | }; 79 | 80 | 81 | module.exports = { 82 | getPrincipalCurriculumId, 83 | getExperiences, 84 | getSkills 85 | }; -------------------------------------------------------------------------------- /ai/actions/username.js: -------------------------------------------------------------------------------- 1 | const { ChatCompletionRequestMessageRoleEnum } = require('openai'); 2 | 3 | const ai = require('../openai'); 4 | const sendMarkdownMessage = require('../../bot/sendMarkdown'); 5 | const AI_MODEL = process.env.AI_MODEL ?? ''; 6 | const OPEN_AI_RATE_LIMIT_RETRIES = process.env.OPEN_AI_RATE_LIMIT_RETRIES ?? 5; 7 | const OPEN_AI_DELAY_BETWEEN_RETRIES = process.env.OPEN_AI_DELAY_BETWEEN_RETRIES ?? 20000; 8 | const messages = require('../../language/messages.json'); 9 | 10 | const INITIAL_MESSAGES = [ 11 | { 12 | role: ChatCompletionRequestMessageRoleEnum.System, 13 | content: ` 14 | Quiero que de un mensaje de entrada del usuario intentes extraer el nombre de usuario de github. El usuario puede escribir cualquier cosa, pero tu tienes que ser capaz de extraer el nombre de usuario de github. 15 | 16 | Ejempos de entrada: 17 | "Mi usuario de github es midudev" 18 | "Mi usuario de github es ericrisco" 19 | "Quiero que cojas mi perfil de github, mi usuario es ericrisco" 20 | "Coje mi perfil de github" 21 | 22 | Quiero que devuelvas un json con el "username" y un "message". 23 | El "message" puede ser null o un mensaje de error si no has podido extraer el usuario de github. 24 | El "username" puede ser null o el nombre de usuario de github. 25 | Si el "username" no es null, entonces el "message" tiene que ser null. 26 | Si el "message" no es null, entonces el "username" tiene que ser null. 27 | No quiero que me expliques como has llegado al resultado ni nada mas, solo quiero el JSON. 28 | 29 | Ejemplos de respuesta: 30 | 31 | { "username": "aboutMe", "message": null } 32 | { "username": null, "message": "No me has informado el usuario de Github" } 33 | ` 34 | } 35 | ]; 36 | 37 | async function getUsername(chatId, message, attempts = OPEN_AI_RATE_LIMIT_RETRIES) { 38 | try { 39 | const completion = await ai.createChatCompletion({ 40 | model: AI_MODEL, 41 | temperature: 0, 42 | messages: [ 43 | ...INITIAL_MESSAGES, 44 | { 45 | role: ChatCompletionRequestMessageRoleEnum.User, 46 | content: message 47 | } 48 | ] 49 | }); 50 | 51 | const data = completion.data.choices[0].message?.content ?? ''; 52 | 53 | try { 54 | return JSON.parse(data); 55 | } catch (err) { 56 | return { action: 'other', message: message.promptError }; 57 | } 58 | } catch (err) { 59 | if (err.response.status === 429 && attempts > 0) { 60 | sendMarkdownMessage(chatId, messages.giveMeTime); 61 | await new Promise((resolve) => setTimeout(resolve, OPEN_AI_DELAY_BETWEEN_RETRIES)); 62 | return getUsername(chatId, message, attempts - 1); 63 | } 64 | return { username: null, message: messages.aiError }; 65 | } 66 | } 67 | 68 | module.exports = getUsername; 69 | -------------------------------------------------------------------------------- /infojobs/offers.js: -------------------------------------------------------------------------------- 1 | const CLIENT_ID = process.env.INFOJOBS_CLIENT_ID; 2 | const CLIENT_SECRET = process.env.INFOJOBS_CLIENT_SECRET; 3 | const BASIC_TOKEN = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString( 4 | "base64" 5 | ); 6 | 7 | const OFFER_LIST_URL = 'https://api.infojobs.net/api/9/offer?'; 8 | const OFFER_ID_URL = 'https://api.infojobs.net/api/7/offer/'; 9 | 10 | async function getLastOffersByUser(user, maxResults = 3) { 11 | let url = OFFER_LIST_URL; 12 | url += 'order=relevancia-desc'; 13 | url += '&maxResults=' + maxResults; 14 | url += '&sinceDate=_7_DAYS'; 15 | url += user.category !== '' && user.category ? '&category=' + encodeURIComponent(user.category) : ''; 16 | // url += '&country=espana'; 17 | url += user.keywords !== '' && user.keywords ? '&q=descripcion:' + encodeURIComponent(formatKeywords(user.keywords)) : ''; 18 | url += user.city !== '' && user.city ? '&city=' + encodeURIComponent(user.city) : ''; 19 | 20 | if (user.willingToRelocate) { 21 | user.relocationCities.forEach((city) => { 22 | url += '&city=' + encodeURIComponent(city); 23 | }); 24 | } 25 | 26 | const res = await fetch(url, { 27 | headers: { 28 | 'Content-Type': 'application/json', 29 | Authorization: `Basic ${BASIC_TOKEN}` 30 | } 31 | }); 32 | 33 | return await res.json(); 34 | } 35 | 36 | async function getOffersByQuery(query) { 37 | let url = OFFER_LIST_URL; 38 | url += 'order=updated-desc'; 39 | url += '&maxResults=3'; 40 | url += '&sinceDate=_7_DAYS'; 41 | url += query.category !== '' ? '&category=' + encodeURIComponent(query.category) : ''; 42 | // url += '&country=espana'; 43 | url += query.keywords !== '' ? '&q=descripcion:' + encodeURIComponent(formatKeywords(query.keywords)) : ''; 44 | 45 | query.cities.forEach((city) => { 46 | url += '&city=' + encodeURIComponent(city); 47 | }); 48 | 49 | const res = await fetch(url, { 50 | headers: { 51 | 'Content-Type': 'application/json', 52 | Authorization: `Basic ${BASIC_TOKEN}` 53 | } 54 | }); 55 | 56 | return await res.json(); 57 | } 58 | 59 | async function getOfferById(offerId) { 60 | let url = OFFER_ID_URL + offerId; 61 | 62 | const res = await fetch(url, { 63 | headers: { 64 | 'Content-Type': 'application/json', 65 | Authorization: `Basic ${BASIC_TOKEN}` 66 | } 67 | }); 68 | 69 | return await res.json(); 70 | } 71 | 72 | function formatKeywords(text) { 73 | const keywords = text.split(',').map((keyword) => keyword.trim()); 74 | 75 | const filteredKeywords = keywords.filter((keyword) => keyword.length > 4); 76 | 77 | const formattedString = filteredKeywords.join('*'); 78 | 79 | return `*${formattedString}*`; 80 | } 81 | 82 | module.exports = { 83 | getLastOffersByUser, 84 | getOffersByQuery, 85 | getOfferById 86 | }; 87 | -------------------------------------------------------------------------------- /bot/onMessage.js: -------------------------------------------------------------------------------- 1 | const getNextAction = require('../ai/actions/chat'); 2 | const createSearchQuery = require('../ai/actions/searchQuery'); 3 | const messages = require('../language/messages.json'); 4 | const bot = require('./bot'); 5 | const aboutMe = require('./commands/aboutme'); 6 | const sendMarkdownMessage = require('./sendMarkdown'); 7 | const { getOffersByQuery, getLastOffersByUser } = require('../infojobs/offers'); 8 | const { printJobs } = require('./commands/jobs'); 9 | const User = require('../db/models/user'); 10 | const getHelp = require('../ai/actions/help'); 11 | const getJoke = require('../ai/actions/joke'); 12 | const { getProfile, deleteProfile, remoteProfile } = require('./commands/profile'); 13 | const { extractTechnologies } = require('./commands/github'); 14 | 15 | module.exports = () => { 16 | bot.on('message', async (msg) => { 17 | if (msg.text === '/start') return; 18 | 19 | const chatId = msg.chat.id; 20 | const user = await User.findOne({ chatId }); 21 | if (!user) { 22 | await sendMarkdownMessage(chatId, messages.profileNotFound); 23 | return; 24 | } 25 | 26 | if (user.executing) { 27 | await sendMarkdownMessage(chatId, messages.executing); 28 | return; 29 | } 30 | 31 | await User.findOneAndUpdate({ chatId }, { executing: true }); 32 | 33 | const nextAction = await getNextAction(chatId, msg.text); 34 | 35 | console.log(user.chatId, nextAction.action, msg.text); 36 | 37 | switch (nextAction.action) { 38 | case 'remoteProfile': 39 | await remoteProfile(chatId); 40 | break; 41 | case 'githubProfile': 42 | await extractTechnologies(chatId, msg.text); 43 | break; 44 | case 'aboutMe': 45 | await aboutMe(chatId, msg.text, user.aboutMe ? true : false); 46 | break; 47 | case 'aboutMeModify': 48 | await aboutMe(chatId, msg.text, true); 49 | break; 50 | case 'profile': 51 | await getProfile(chatId); 52 | break; 53 | case 'jobs': 54 | var query = await createSearchQuery(chatId, msg.text); 55 | if (query) { 56 | var offersByQuery = await getOffersByQuery(query); 57 | await printJobs(chatId, offersByQuery, false); 58 | } else { 59 | await sendMarkdownMessage(chatId, messages.searchQueryError); 60 | } 61 | break; 62 | case 'jobsProfile': 63 | var offersByUser = await getLastOffersByUser(user); 64 | await printJobs(chatId, offersByUser); 65 | break; 66 | case 'help': 67 | var response = await getHelp(chatId, msg.text); 68 | await sendMarkdownMessage(chatId, response); 69 | await sendMarkdownMessage(chatId, messages.helpSimple); 70 | break; 71 | case 'joke': 72 | var joke = await getJoke(chatId); 73 | await sendMarkdownMessage(chatId, joke); 74 | break; 75 | case 'delete': 76 | await deleteProfile(chatId); 77 | break; 78 | default: 79 | await sendMarkdownMessage(chatId, nextAction.message ?? messages.aiError); 80 | break; 81 | } 82 | 83 | await User.findOneAndUpdate({ chatId }, { executing: false }); 84 | 85 | return; 86 | }); 87 | }; 88 | -------------------------------------------------------------------------------- /ai/actions/help.js: -------------------------------------------------------------------------------- 1 | const { ChatCompletionRequestMessageRoleEnum } = require('openai'); 2 | 3 | const ai = require('../openai'); 4 | const sendMarkdownMessage = require('../../bot/sendMarkdown'); 5 | const AI_MODEL = process.env.AI_MODEL ?? ''; 6 | const OPEN_AI_RATE_LIMIT_RETRIES = process.env.OPEN_AI_RATE_LIMIT_RETRIES ?? 5; 7 | const OPEN_AI_DELAY_BETWEEN_RETRIES = process.env.OPEN_AI_DELAY_BETWEEN_RETRIES ?? 20000; 8 | const messages = require('../../language/messages.json'); 9 | 10 | const INITIAL_MESSAGES = [ 11 | { 12 | role: ChatCompletionRequestMessageRoleEnum.System, 13 | content: `Imagina que eres un chatbot que ayuda a las personas a encontrar trabajo. Entre tus funciones estan: 14 | 15 | """ 16 | Interacción en Lenguaje Natural: Interactúa con el chatbot en lenguaje natural, sin necesidad de comandos predefinidos. Funciona de la misma manera que ChatGPT. 17 | Extracción del Perfil Laboral desde InfoJobs: Extrae tu perfil laboral de InfoJobs a través de lenguaje natural. 18 | Extracción de tecnologias usadas desde github: Extrae las tecnologias usadas en tus repositorios de github a través de lenguaje natural. 19 | Generación del Perfil Laboral: Genera tus perfiles laborales a través de lenguaje natural. 20 | Modificación del Perfil Laboral: Modifica perfiles laborales a través de lenguaje natural. 21 | Búsqueda de Empleo por Perfil: Usa tu perfil laboral para formular consultas a la API de InfoJobs y obtener ofertas de empleo. 22 | Búsqueda de Empleo por Solicitud: Usa una solicitud para formular consultas a la API de InfoJobs y obtener ofertas de empleo. 23 | Evaluación de Ofertas de Empleo: Compara una oferta de empleo con tu perfil y obtén retroalimentación. 24 | Generador de Malos Chistes: Genera malos chistes relacionados con la IA, la programación y la búsqueda de empleo. 25 | Ofertas de Empleo Diarias: Envía una oferta de empleo a tu Telegram basada en tu perfil laboral cada día. 26 | """ 27 | 28 | Con el mensaje que te paso de entrada del usuario quiero que intentes ayudarlo sabiendo cuales son tus funciones y limitaciones. Si no puedes ayudarlo, crea un mensaje diciendole al usuario que no puedes ayudarle haciendo un chiste de robots. 29 | ` 30 | } 31 | ]; 32 | 33 | async function getHelp(chatId, message, attempts = OPEN_AI_RATE_LIMIT_RETRIES) { 34 | try { 35 | const completion = await ai.createChatCompletion({ 36 | model: AI_MODEL, 37 | temperature: 0, 38 | messages: [ 39 | ...INITIAL_MESSAGES, 40 | { 41 | role: ChatCompletionRequestMessageRoleEnum.User, 42 | content: message 43 | } 44 | ] 45 | }); 46 | 47 | const data = completion.data.choices[0].message?.content ?? ''; 48 | return data; 49 | } catch (err) { 50 | if (err.response.status === 429 && attempts > 0) { 51 | sendMarkdownMessage(chatId, messages.giveMeTime); 52 | await new Promise((resolve) => setTimeout(resolve, OPEN_AI_DELAY_BETWEEN_RETRIES)); 53 | return getHelp(chatId, message, attempts - 1); 54 | } 55 | return messages.aiError; 56 | } 57 | } 58 | 59 | module.exports = getHelp; 60 | -------------------------------------------------------------------------------- /bot/commands/profile.js: -------------------------------------------------------------------------------- 1 | const sendMarkdownMessage = require('../sendMarkdown'); 2 | const messages = require('../../language/messages.json'); 3 | const util = require('util'); 4 | const errorHandling = require('../errorHandling'); 5 | const User = require('../../db/models/user'); 6 | const { getLoginUrl } = require('../../infojobs/login'); 7 | const { getToken } = require('../../infojobs/token'); 8 | const { getPrincipalCurriculumId, getSkills } = require('../../infojobs/curriculum'); 9 | const { getInfojobsProfile } = require('../../infojobs/profile'); 10 | 11 | async function getProfile(chatId) { 12 | try { 13 | const user = await User.findOne({ chatId }); 14 | 15 | if (!user) { 16 | await sendMarkdownMessage(chatId, messages.profileNotFound); 17 | await sendMarkdownMessage(chatId, messages.incompleteProfileMessage); 18 | await sendMarkdownMessage(chatId, messages.exampleProfile); 19 | } else { 20 | if (user.recomendation) { 21 | const summaryMessage = util.format( 22 | messages.userProfileSummary, 23 | user.age || 'N/A', 24 | user.city || 'N/A', 25 | user.position || 'N/A', 26 | user.experienceYears || 'N/A', 27 | user.remote ? 'Sí' : 'No', 28 | user.keywords || 'N/A', 29 | user.willingToRelocate ? 'Sí' : 'No', 30 | user.relocationCities ? user.relocationCities.join(', ') : 'N/A', 31 | user.recomendation 32 | ); 33 | 34 | await sendMarkdownMessage(chatId, summaryMessage); 35 | } 36 | 37 | const score = user.score; 38 | const availableForJobSearch = score >= 7; 39 | 40 | const profileStatusmessage = availableForJobSearch ? messages.completeProfileMessage : messages.incompleteProfileMessage; 41 | 42 | await sendMarkdownMessage(chatId, profileStatusmessage); 43 | if (!availableForJobSearch){ 44 | await sendMarkdownMessage(chatId, messages.exampleModify); 45 | } 46 | } 47 | } catch (err) { 48 | errorHandling(chatId, err); 49 | } 50 | } 51 | 52 | async function remoteProfile(chatId, code) { 53 | try { 54 | if(!code){ 55 | const loginUrl = await getLoginUrl(chatId); 56 | var remoteProfileMessage = util.format(messages.remoteProfile, loginUrl); 57 | await sendMarkdownMessage(chatId, remoteProfileMessage); 58 | }else{ 59 | await sendMarkdownMessage(chatId, messages.thanksAccessMessage); 60 | 61 | const token = await getToken(chatId, code); 62 | const profile = await getInfojobsProfile(token); 63 | const cvId = await getPrincipalCurriculumId(token); 64 | const skills = await getSkills(token, cvId); 65 | 66 | const final = { 67 | profile, 68 | skills 69 | } 70 | return final 71 | } 72 | } catch (err) { 73 | errorHandling(chatId, err); 74 | } 75 | 76 | } 77 | 78 | async function deleteProfile(chatId) { 79 | try { 80 | const user = await User.findOne({ chatId }); 81 | 82 | if (!user) { 83 | await sendMarkdownMessage(chatId, messages.profileNotFound); 84 | } else { 85 | await User.deleteOne({ chatId }); 86 | await sendMarkdownMessage(chatId, messages.profileDeleted); 87 | } 88 | } catch (err) { 89 | errorHandling(chatId, err); 90 | } 91 | } 92 | 93 | module.exports = { 94 | getProfile, 95 | deleteProfile, 96 | remoteProfile 97 | }; 98 | -------------------------------------------------------------------------------- /ai/actions/searchQuery.js: -------------------------------------------------------------------------------- 1 | const { ChatCompletionRequestMessageRoleEnum } = require('openai'); 2 | 3 | const ai = require('../openai'); 4 | const sendMarkdownMessage = require('../../bot/sendMarkdown'); 5 | const AI_MODEL = process.env.AI_MODEL ?? ''; 6 | const OPEN_AI_RATE_LIMIT_RETRIES = process.env.OPEN_AI_RATE_LIMIT_RETRIES ?? 5; 7 | const OPEN_AI_DELAY_BETWEEN_RETRIES = process.env.OPEN_AI_DELAY_BETWEEN_RETRIES ?? 20000; 8 | const messages = require('../../language/messages.json'); 9 | 10 | const INITIAL_MESSAGES = [ 11 | { 12 | role: ChatCompletionRequestMessageRoleEnum.System, 13 | content: `Imagina que eres un chatbot que usa la API de Infojobs para buscar ofertas de trabajo a partir del input del usuario 14 | Es muy importante que solo me devuelvas un json. No necesito ningun otro tipo de texto explicando como lo has hecho o cual ha sido tu proceso de pensamiento. Solo el json. 15 | 16 | Algunos ejemplos de input del usuario son: 17 | - Busco ofertas de trabajo de desarrollador en Madrid o Tarragona 18 | - Busco una oferta de trabajo de maestro de guitarra en Barcelona 19 | - Busco ofertas de trabajo de camarero en Valencia 20 | 21 | Tu trabajo será transformar este input en un json que usaremos para para hacer querys a la API de Infojobs. Teniendo en cuenta los ejemplos anteriores, el json que deberías generar sería algo así: 22 | - { "keywords": "desarrollador", "cities": ["Madrid", "Tarragona"], "category": "informatica-telecomunicaciones" } 23 | - { "keywords": "maestro*guitarra", "cities": ["Barcelona"], "category": "educacion-formacion" } 24 | - { "keywords": "camarero", "cities": ["Valencia"], "category": "turismo-restauracion" } 25 | 26 | El campo keywords quiero que resumas la descripción y se lo mas preciso y concreto y no añadas poblaciones. La lista que sea de maximo 4 palabras clave. Sin articulos "de", "el", "la" ni espacios, para separar las palabras pon un asterisco (*). Ejemplo: lider*gestion*proyectos*mobile 27 | 28 | El campo category tiene que ser un valor de la lista de categorias, escoge uno segun tu criterio y el texto que nos da el usuario. 29 | categorias = ['administracion-publica','administracion-empresas','atencion-a-cliente','calidad-produccion-id','comercial-ventas', 'compras-logistica-almacen', 'diseno-artes-graficas', 'educacion-formacion', 'finanzas-banca', 'informatica-telecomunicaciones','ingenieros-tecnicos', 'inmobiliario-construccion', 'legal', 'marketing-comunicacion', 'profesiones-artes-oficios', 'recursos-humanos', 'sanidad-salud', 'sector-farmaceutico', 'turismo-restauracion','venta-detalle', 'otros'] 30 | 31 | ` 32 | } 33 | ]; 34 | 35 | async function createSearchQuery(chatId, text, attempts = OPEN_AI_RATE_LIMIT_RETRIES) { 36 | try { 37 | const completion = await ai.createChatCompletion({ 38 | model: AI_MODEL, 39 | temperature: 0, 40 | messages: [ 41 | ...INITIAL_MESSAGES, 42 | { 43 | role: ChatCompletionRequestMessageRoleEnum.User, 44 | content: `{'text':'${text}'}` 45 | } 46 | ] 47 | }); 48 | 49 | const data = completion.data.choices[0].message?.content ?? ''; 50 | let json; 51 | 52 | try { 53 | json = JSON.parse(data); 54 | return json; 55 | } catch (err) { 56 | return null; 57 | } 58 | } catch (err) { 59 | if (err.response.status === 429 && attempts > 0) { 60 | sendMarkdownMessage(chatId, messages.giveMeTime); 61 | await new Promise((resolve) => setTimeout(resolve, OPEN_AI_DELAY_BETWEEN_RETRIES)); 62 | return createSearchQuery(chatId, text, attempts - 1); 63 | } 64 | return null; 65 | } 66 | } 67 | 68 | module.exports = createSearchQuery; 69 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 🧠🧑‍💼 InfojobsGPT 2 | 3 | InfojobsGPT is an innovative Telegram chatbot that utilizes the OpenAI API and the Infojobs API to assist individuals in their job search. Using the GPT-3 model, it allows for natural language interaction, removing the need for predefined commands. Besides assisting in job searching, InfojobsGPT can generate job profiles for users, extract Infojobs profile, formulate queries to the Infojobs API, evaluate job offers, and, of course, tell bad jokes to brighten up your day. Additionally, InfojobsGPT can send you a job offer that matches your profile every day. 4 | 5 | ## 💻 Features 6 | 7 | 1. **Natural Language Interaction**: Interact with the chatbot in natural language, without the need for predefined commands. Works the same as ChatGPT. 8 | 2. **Job Profile extract from InfoJobs**: Extract your job profile from Infojobs through natural language. 9 | 3. **Job Profile Generation**: Generate your job profiles through natural language. 10 | 4. **Job Profile Modification**: Modify job profiles through natural language. 11 | 5. **Technology Extraction from GitHub**: Extract the technologies used in your GitHub repositories through natural language. 12 | 6. **Job Search by Profile**: Use your job profile to formulate queries to the Infojobs API and get job offers. 13 | 7. **Job Search by Prompt**: Use a prompt to formulate queries to the Infojobs API and get job offers. 14 | 8. **Job Offer Evaluation**: Compare a job offer with your profile and get feedback. 15 | 9. **Bad Joke Generator**: Generate bad jokes related to AI, programming, and job searching. 16 | 10. **Daily Job Offers**: Sends a job offer to your Telegram based on your job profile each day. 17 | 18 | ## 💻 Technologies 19 | 20 | - [Node.js](https://nodejs.org/) 21 | - [OpenAI API](https://platform.openai.com/) 22 | - [Infojobs API](https://developer.infojobs.net/) 23 | - [Telegram API](https://core.telegram.org/) 24 | - [MongoDB](https://www.mongodb.com/) 25 | 26 | ## 📝 Try the bot 27 | 28 | You can test the bot on Telegram at the following link: [@infojobsgpt_bot](https://t.me/infojobsgpt_bot) 29 | 30 | ## 📝 Installation 31 | 32 | 1. Clone the repository 33 | 34 | ```sh 35 | git clone 36 | ``` 37 | 38 | 2. Install NPM packages 39 | 40 | ```sh 41 | npm install 42 | ``` 43 | 44 | 3. Create a .env file with the following environment variables: 45 | 46 | ```sh 47 | PORT=5000 48 | TELEGRAM_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 49 | MONGODB_URI="mongodb+srv://:@xxx/yyy" 50 | MONGODB_USERNAME=xxx 51 | MONGODB_PASSWORD=yyy 52 | OPENAI_TOKEN=zzz 53 | AI_MODEL=gpt-3.5-turbo 54 | OPEN_AI_RATE_LIMIT_RETRIES=5 55 | OPEN_AI_DELAY_BETWEEN_RETRIES=20000 56 | INFOJOBS_CLIENT_ID=xxx 57 | INFOJOBS_CLIENT_SECRET=yyy 58 | INFOJOBS_REDIRECT_URI=xxxx/infojobs/callback 59 | GITHUB_API_TOKEN=xxx 60 | ``` 61 | 62 | You need a Telegram bot token, an Infojobs API key, a MongoDB URI, a MongoDB username, a MongoDB password, an OpenAI API token. 63 | 64 | - You can get a Telegram bot token by talking to the [BotFather](https://t.me/botfather). 65 | - You can get an Infojobs Client ID and Client Secret [Infojobs API team](https://developer.infojobs.net/). 66 | - You can get an OpenAI API token by signing up for the [OpenAI API](https://beta.openai.com/). 67 | - You can get a MongoDB URI, a MongoDB username, and a MongoDB password by signing up for [MongoDB Atlas](https://www.mongodb.com/cloud/atlas). 68 | - You can get a GitHub API token by following the instructions [Github](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token). 69 | 70 | 4. Run the dev server 71 | 72 | ```sh 73 | npm run dev 74 | ``` 75 | -------------------------------------------------------------------------------- /bot/commands/jobs.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | const User = require('../../db/models/user'); 3 | const messages = require('../../language/messages.json'); 4 | 5 | const sendMarkdownMessage = require('../sendMarkdown'); 6 | 7 | const errorHandling = require('../errorHandling'); 8 | const { getLastOffersByUser, getOfferById } = require('../../infojobs/offers'); 9 | const getOpinion = require('../../ai/actions/opinion'); 10 | 11 | async function printJobs(chatId, offers, cron = false) { 12 | try { 13 | const currentResults = offers.currentResults; 14 | const totalResults = offers.totalResults; 15 | 16 | if (totalResults == 0) { 17 | await sendMarkdownMessage(chatId, messages.noJobsFound); 18 | } else { 19 | const user = await User.findOne({ chatId }); 20 | if (!cron) { 21 | await sendMarkdownMessage(chatId, util.format(messages.jobsFound, totalResults, currentResults)); 22 | await sendMarkdownMessage(chatId, messages.analaizingJobs); 23 | } 24 | 25 | for (let i = 0; i < offers.items.length; i++) { 26 | const offer = offers.items[i]; 27 | const title = offer['title'] || 'N/A'; 28 | const city = offer['city'] || 'N/A'; 29 | const link = offer['link'] || 'N/A'; 30 | const category = offer['category']?.value || 'N/A'; 31 | const contractType = offer['contractType']?.value || 'N/A'; 32 | const salary = offer['salaryDescription'] || 'N/A'; 33 | const experienceMin = offer['experienceMin']?.value || 'N/A'; 34 | const workDay = offer['workDay']?.value || 'N/A'; 35 | const study = offer['study']?.value || 'N/A'; 36 | const teleworking = offer['teleworking']?.value || 'N/A'; 37 | 38 | const offerDetail = await getOfferById(offer['id']); 39 | const description = offerDetail.description; 40 | 41 | const opinion = await getOpinion(user, { title, description, city, salary, study, teleworking }); 42 | 43 | const formattedJobOfferSummary = util.format( 44 | messages.jobOfferSummary, 45 | title, 46 | city, 47 | link, 48 | category, 49 | contractType, 50 | salary, 51 | experienceMin, 52 | workDay, 53 | study, 54 | teleworking, 55 | opinion 56 | ); 57 | 58 | await sendMarkdownMessage(chatId, formattedJobOfferSummary); 59 | } 60 | } 61 | } catch (err) { 62 | errorHandling(chatId, err); 63 | } 64 | } 65 | 66 | async function getJobsFromProfile(chatId) { 67 | try { 68 | const user = await User.findOne({ chatId }); 69 | 70 | if (user.availableForJobSearch) { 71 | const offers = await getLastOffersByUser(user); 72 | 73 | const currentResults = offers.currentResults; 74 | const totalResults = offers.totalResults; 75 | 76 | const message = totalResults == 0 ? messages.noJobsFound : util.format(messages.jobsFound, totalResults, currentResults); 77 | await sendMarkdownMessage(chatId, message); 78 | await sendMarkdownMessage(chatId, messages.analaizingJobs); 79 | 80 | for (let i = 0; i < offers.items.length; i++) { 81 | const offer = offers.items[i]; 82 | const title = offer['title'] || 'N/A'; 83 | const city = offer['city'] || 'N/A'; 84 | const link = offer['link'] || 'N/A'; 85 | const category = offer['category']?.value || 'N/A'; 86 | const contractType = offer['contractType']?.value || 'N/A'; 87 | const salary = offer['salaryDescription'] || 'N/A'; 88 | const experienceMin = offer['experienceMin']?.value || 'N/A'; 89 | const workDay = offer['workDay']?.value || 'N/A'; 90 | const study = offer['study']?.value || 'N/A'; 91 | const teleworking = offer['teleworking']?.value || 'N/A'; 92 | 93 | const offerDetail = await getOfferById(offer['id']); 94 | const description = offerDetail.description; 95 | 96 | const opinion = await getOpinion(user, { title, description, city, salary, study, teleworking }); 97 | 98 | const formattedJobOfferSummary = util.format( 99 | messages.jobOfferSummary, 100 | title, 101 | city, 102 | link, 103 | category, 104 | contractType, 105 | salary, 106 | experienceMin, 107 | workDay, 108 | study, 109 | teleworking, 110 | opinion 111 | ); 112 | 113 | await sendMarkdownMessage(chatId, formattedJobOfferSummary); 114 | } 115 | 116 | await sendMarkdownMessage(chatId, messages.helpSimple); 117 | } else { 118 | await sendMarkdownMessage(chatId, messages.incompleteProfileMessage); 119 | } 120 | } catch (err) { 121 | errorHandling(chatId, err); 122 | } 123 | } 124 | 125 | module.exports = { 126 | printJobs, 127 | getJobsFromProfile 128 | }; 129 | -------------------------------------------------------------------------------- /ai/actions/summarizer.js: -------------------------------------------------------------------------------- 1 | const { ChatCompletionRequestMessageRoleEnum } = require('openai'); 2 | 3 | const ai = require('../openai'); 4 | const sendMarkdownMessage = require('../../bot/sendMarkdown'); 5 | const AI_MODEL = process.env.AI_MODEL ?? ''; 6 | const OPEN_AI_RATE_LIMIT_RETRIES = process.env.OPEN_AI_RATE_LIMIT_RETRIES ?? 5; 7 | const OPEN_AI_DELAY_BETWEEN_RETRIES = process.env.OPEN_AI_DELAY_BETWEEN_RETRIES ?? 20000; 8 | const messages = require('../../language/messages.json'); 9 | 10 | const INITIAL_MESSAGES = [ 11 | { 12 | role: ChatCompletionRequestMessageRoleEnum.System, 13 | content: `Quiero que cuando te pase una descripción nos da un usuario sobre el me lo resumas a un json como este. 14 | 15 | El formato de respuesta JSON será el siguiente: 16 | 17 | { 18 | "age": [age], 19 | "city": "[city]", 20 | "category": "[category]", 21 | "position": "[position]", 22 | "experienceYears": [experienceYears], 23 | "remote": [remote], 24 | "teleworking": "[teleworking]", 25 | "keywords": "[keywords]", 26 | "willingToRelocate": [willingToRelocate], 27 | "relocationCities": ["city1", "city2", "city3"], 28 | "score": [score], 29 | "recomendation": "[recomendation]" 30 | } 31 | 32 | El campo category tiene que ser un valor de la lista de categorias, escoge uno segun tu criterio y el de la descripción del usuario. 33 | categorias = ['administracion-publica','administracion-empresas','atencion-a-cliente','calidad-produccion-id','comercial-ventas', 'compras-logistica-almacen', 'diseno-artes-graficas', 'educacion-formacion', 'finanzas-banca', 'informatica-telecomunicaciones','ingenieros-tecnicos', 'inmobiliario-construccion', 'legal', 'marketing-comunicacion', 'profesiones-artes-oficios', 'recursos-humanos', 'sanidad-salud', 'sector-farmaceutico', 'turismo-restauracion','venta-detalle', 'otros'] 34 | 35 | experienceYears es un valor numerico que representa los años de experiencia que tiene el usuario. Si no lo menciona pon un valor null. 36 | 37 | score es un valor numerico que representa la puntuación que le das al json resultado del perfil del usuario. Si no lo menciona pon un valor null. 38 | 39 | remote es un valor booleano que representa si el usuario quiere trabajar en remoto. Si no lo menciona pon un valor null. 40 | 41 | teleworking es un valor string que representa si el usuario quiere poder teletrabajar o no. Si no lo menciona pon un valor 'no-se-sabe-no-esta-decidido'. 42 | los posibles valores son: trabajo-solo-presencial, solo-teletrabajo, teletrabajo-posible, no-se-sabe-no-esta-decidido 43 | Según tu criterio con el texto que te pase el usuario escoge uno de estos valores. 44 | 45 | Tienes que cambiar lo que hay entre corchetes por lo que consigas resumir de la descripción. Si no consigues resumir nada, pon un valor null. 46 | 47 | Para el campo "keywords" dame una lista las 4 palabras clave mas importantes que resalten solo mis habilidades relacionadas con un trabajo quitando cualquier otra información y articulos como 'el', 'la', 'de' y todos los demás. Este seria un ejemplo de descripción: 48 | """Me llamo Julio, tengo 26 años, 3 años de experiencia programando en React y javascript y busco trabajo en Madrid, pero tambien me interesa Valencia y Barcelona.""" 49 | Y la lista resultante seria: 50 | """React, javascript""" 51 | El resultado es una lista de palabras separadas por comas y sin espacios entre ellas. Sino consigues nada, pon null 52 | 53 | Segun tu criterio si la descripción es correcta pon una valoracion de 0 a 10. Crea una recomendación de como deberia mejorar su descripción de un máximo de 100 palabras teniendo en cuenta el score 54 | relocationCities es un array de ciudades que el usuario ha mencionado en su descripción. En caso de que no haya mencionado ninguna ciudad, pon un array vacio. 55 | 56 | Es posible que te encuentres que el mensaje primero diga una cosa, y luego se contradiga. Ten más en cuenta la última frase que diga el usuario. 57 | Ejemplo: "Me llamo Julio, tengo 26 años, 3 años de experiencia programando en React y javascript pero ya no quiero programar, ahora quiero ser lider de proyectos." 58 | 59 | Este seria un ejemplo de resultado: 60 | 61 | { 62 | "age": 32, 63 | "city": "Madrid", 64 | "category": null, 65 | "position": null, 66 | "experienceYears": 8, 67 | "remote": yes, 68 | "teleworking": "no-se-sabe-no-esta-decidido", 69 | "keywords": "lider, gestion de proyectos, mobile" 70 | "willingToRelocate": true, 71 | "relocationCities": ["Barcelona", "Valencia"], 72 | "score": 7, 73 | "recomendation": "Deberías mejorar tu descripción, es importante que me explicas a que te dedicas o a que te quieres dedicar." 74 | } 75 | Es muy importante que solo me devuelvas un json. No necesito ningun otro tipo de texto explicando como lo has hecho o cual ha sido tu proceso de pensamiento. Solo el json. 76 | 77 | ` 78 | } 79 | ]; 80 | 81 | async function getAboutMeSummarized(chatId, text, attempts = OPEN_AI_RATE_LIMIT_RETRIES) { 82 | try { 83 | const completion = await ai.createChatCompletion({ 84 | model: AI_MODEL, 85 | temperature: 0, 86 | messages: [ 87 | ...INITIAL_MESSAGES, 88 | { 89 | role: ChatCompletionRequestMessageRoleEnum.User, 90 | content: text 91 | } 92 | ] 93 | }); 94 | 95 | const data = completion.data.choices[0].message?.content ?? ''; 96 | let json; 97 | 98 | try { 99 | json = JSON.parse(data); 100 | return json; 101 | } catch (err) { 102 | return { error: true, message: messages.promptError }; 103 | } 104 | } catch (err) { 105 | console.log(err.response); 106 | if (err.response.status === 429 && attempts > 0) { 107 | sendMarkdownMessage(chatId, messages.giveMeTime); 108 | await new Promise((resolve) => setTimeout(resolve, OPEN_AI_DELAY_BETWEEN_RETRIES)); 109 | return getAboutMeSummarized(chatId, text, attempts - 1); 110 | } 111 | return { action: 'error', message: messages.aiError }; 112 | } 113 | } 114 | 115 | module.exports = getAboutMeSummarized; 116 | -------------------------------------------------------------------------------- /language/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcomeBack": "¡Hola de nuevo, %s! ¿Cómo puedo ayudarte hoy?", 3 | "welcomeBackNoName": "¡Hola de nuevo! ¿Cómo puedo ayudarte hoy?", 4 | "giveMeInfo": "Me gustaría conocerte un poco mejor para poder ayudarte de la mejor manera. Por favor, sé lo más detallado y concreto posible en tu respuesta. Algunos puntos que podrías considerar son:\n\n- ¿De dónde eres?\n- ¿Cuántos años tienes?\n- ¿Qué aspiraciones de trabajo tienes? ¿En qué industria te gustaría trabajar? ¿Qué puesto te gustaría obtener?\n- ¿Podrías contarme un poco sobre tu experiencia laboral? ¿Has trabajado en el campo que te interesa?\n- ¿Estás interesado en el trabajo remoto (teletrabajo), o prefieres trabajar en persona?\n- ¿Estarías dispuesto a mudarte a otra ciudad por trabajo? Si es así, ¿hay alguna ciudad en particular donde te gustaría trabajar?\n\nPor favor, toma tu tiempo para responder estas preguntas. Cuanto más preciso seas más fácil será para mí ayudarte. ¡Gracias!", 5 | "goDescription": "¡Adelante! 🚀 Tus aspiraciones y experiencias son fundamentales para nosotros. Describirte a ti mismo ahora es el primer paso para descubrir las mejores oportunidades laborales. 💼🔍 ¡Comienza ya!", 6 | "errorMessage": "Disculpa, estoy fallando internamente (mi creador es muy mal programador)... ¿Podrías repetirlo?", 7 | "exampleProfile": "Te doy algunos ejemplos:\n\n - Coge mi perfil de InfoJobs \n - Soy un diseñador gráfico con 7 años de experiencia trabajando con Adobe Creative Suite. \n - Soy un ingeniero de software con especialización en Python y Django", 8 | "exampleModify": "Recuerda que puedes modificar tu perfil añadiendo o quitando información extra:\n\n - Busco trabajo en remoto\n - Añade mi perfil de Github, mi usuario es hal9000 \n - Ahora mismo estoy aprendiendo React\n - Añade mi perfil de InfoJobs\n - Me gustaría trabajar en remoto\n - Ya no vivo en Barcelona, vivo en Valencia...", 9 | "userProfileSummary": "Perfil: \n\n👤 Edad: %s\n📍 Ciudad: %s\n📚 Posición: %s\n🕰️ Años de experiencia: %s\n🌐 Trabajo remoto: %s\n🎓 Otras habilidades: %s\n🚀 Disponibilidad para mudarse: %s\n🌍 Otras ciudades de interés: %s\n\n\n💡🧠 Recomendación basada en tu perfil: %s", 10 | "incompleteProfileMessage": "Parece que la información que nos proporcionaste no es suficiente para brindarte las mejores opciones laborales. Para garantizar que obtenemos los mejores resultados posibles, necesitamos conocer un poco más sobre ti. 🤗\n\nDetalles como la categoría laboral a la que te dedicas o aspiras y la posición que deseas ocupar, son vitales para ajustar las opciones de trabajo a tus preferencias y habilidades. 💼✨\n\nTe animamos a que vuelvas a describirte, proporcionando un poco más de detalle. Cuanto más precisa sea la información, mejor podremos ayudarte a encontrar la opción de trabajo perfecta para ti. 🚀", 11 | "completeProfileMessage": "Te hemos activado la función de recibir ofertas para tu perfil de forma diaria, y ya puedes pedirme que busque ofertas de trabajo segun tu perfil 👍💼", 12 | "noJobsFound": "Lo siento, no he encontrado ningún trabajo que se ajuste a tu perfil. 😔", 13 | "profileNotFound": "Lo siento, no he encontrado tu perfil. 😔 Intenta crearlo usando el comando /start", 14 | "jobsFound": "📌 He encontrado %s trabajos. Voy a buscar los %s más relevantes y te voy a dar mi opinión: 🎉", 15 | "analaizingJobs": "🔎 Analizando los trabajos encontrados...", 16 | "jobOfferSummary": "🔖 Título: %s\n📍 Ciudad: %s\n🔗 Enlace: %s\n💼 Categoría: %s\n📝 Tipo de contrato: %s\n💰 Salario: %s\n⏳ Experiencia mínima: %s\n⏰ Jornada: %s\n🎓 Estudios: %s\n🌐 Trabajo remoto: %s\n\n💡 Opinión personal: %s", 17 | "searchQueryError": "Esta petición de buscar trabajo es un poco ambigua. Por favor, sé más específico en tu búsqueda. Por ejemplo, podrías buscar trabajos en una ciudad específica, o en una categoría laboral específica. 🤔\n\nPor favor, intenta de nuevo.", 18 | "aiError": "En estos momentos no he podido procesar lo que me has pedido... lo siento 😔. Por favor, inténtalo de nuevo.", 19 | "promptError": "No he logrado entender lo que me estas pidiendo... 😔. Por favor, inténtalo de nuevo.", 20 | "giveMeTime": "...", 21 | "profileDeleted": "Tu perfil ha sido eliminado correctamente. Si deseas volver a crearlo, simplemente usa el comando /start. ¡Gracias por usar InfoJobs en Telegram! 🤗", 22 | "help": "¡Hola! 🤗 Soy la IA de InfoJobs que te ayudará a buscar empleo.🚀 \n\nMi funcionamiento es muy facil; cuentame cosas sobre ti y buscaremos las mejores ofertas con la ayuda de la alucinante base de datos de InfoJobs. 🌟💼 🧐 Cuanto más detallado sea lo que me cuentas, mejor podré buscar.\n\n Si te da pereza escribir, pideme de busque tu perfil en InfoJobs o Github y lo automatizaremos 🤖 \n\nAdemás, te daré consejos y opiniones sobre cada oferta que encuentre. 😎 \n\n También te enviaré ofertas diarias basadas en tu perfil. ¿No las quieres? Solo dímelo y lo cancelamos.\n\nPuedo buscar ofertas específicas o novedosas. ¿Quieres un trabajo de fin de semana de camarero? Pidemelo y lo buscamos! \n\n También estoy en camino de aprender buenos chistes, pidemelos, no te aseguro que sean buenos 🤣\n\n Sinó quieres que tenga tu información,... pideme de eliminar tu perfil y solucionado \n\n ¡Vamos! 💪", 23 | "helpSimple": "Recuerda que aunque sea una IA super avanzada mi creador me ha limitado un poco. \n\n Puedo: \n - Escuchar como me explicas tu perfil y guardarlo \n - Buscar trabajos que se asemejen a tu perfil \n - Buscar trabajos de cualquier otra temática \n - Dar opinión sobre tu perfil \n - Dar opinión sobre las ofertas que encuentro \n - Enviar ofertas de trabajo relevantes diariamente \n - Contar chistes malos \n - Eliminar tu perfil", 24 | "dailyOffer": "Y esta ha sido la oferta que he encontrado hoy para ti! Recuerda que siempre puedes pedirme que modifique tu perfil, que busque ofertas de trabajo concretas, que te cuente un chiste malo, que elimine tu perfil o que desactive la notificación diaria. \n\n ¡Hasta mañana! 🤗", 25 | "noDailyOffer": "Hoy no he podido ninguna oferta relevante. No te preocupes, seguro que mañana encuentro alguna! ¡Gracias por usar InfoJobs en Telegram! 🤗", 26 | "executing": "Ya tengo una petición tuya en curso, por favor, espera a que termine para hacer otra.", 27 | "remoteProfile": "Perfecto! Para que pueda procesar tu perfil de Infojobs primero tienes que darme permiso a verlo. No te preocupes no haremos nada raro, solo lo usaremos para buscar ofertas de trabajo que se ajusten a tu perfil. \n\n Sigue este enlace: %s", 28 | "thanksAccessMessage": "Gracias por darme acceso a tu perfil de InfoJobs, empiezo a procesarlo 🤖 y en breve te daré mi opinión sobre él", 29 | "extractedTechnologies": "Estas son las tecnologias que he encontrado en tus repos de Github: \n\n %s \n\n Ahora mismo las añado a tu perfil...", 30 | "githubTechnologiesNotFound": "No he podido encontrar ninguna tecnología en tus repos de Github. ¿Estas seguro de que tienes repositorios públicos? Si es así, asegurate de que estan escritos en inglés y que tienen un README.md con información sobre el proyecto." 31 | } 32 | -------------------------------------------------------------------------------- /ai/actions/chat.js: -------------------------------------------------------------------------------- 1 | const { ChatCompletionRequestMessageRoleEnum } = require('openai'); 2 | 3 | const ai = require('../openai'); 4 | const sendMarkdownMessage = require('../../bot/sendMarkdown'); 5 | const AI_MODEL = process.env.AI_MODEL ?? ''; 6 | const OPEN_AI_RATE_LIMIT_RETRIES = process.env.OPEN_AI_RATE_LIMIT_RETRIES ?? 5; 7 | const OPEN_AI_DELAY_BETWEEN_RETRIES = process.env.OPEN_AI_DELAY_BETWEEN_RETRIES ?? 20000; 8 | const messages = require('../../language/messages.json'); 9 | 10 | const INITIAL_MESSAGES = [ 11 | { 12 | role: ChatCompletionRequestMessageRoleEnum.System, 13 | content: `Imagina que eres un chatbot y a partir de un mensaje tienes que devolver un json. 14 | 15 | Ejemplos de respuesta: 16 | 17 | { "action": "aboutMe", "message": null } 18 | { "action": "other", "message": "Tu comportamiento es inadecuado!!" } 19 | 20 | Es muy importante que solo me devuelvas un json. No necesito ningun otro tipo de texto explicando como lo has hecho o cual ha sido tu proceso de pensamiento. Solo el json. 21 | 22 | El campo "action" puede tener los siguientes valores: 23 | 24 | - remoteProfile: En este caso el usuario esta pidiendo que cojamos su perfil de la pagina de infojobs y lo guardemos en nuestra base de datos. 25 | Ejemplos remoteProfile: 26 | "Quiero que cojais mi perfil de infojobs y lo guardeis en vuestra base de datos" 27 | "Coje mi perfil de infojobs" 28 | "Tengo cuenta en infojobs" 29 | 30 | - githubProfile: En este caso el usuario esta pidiendo que cojamos su perfil de github. Basicamente, si el usuario nombra github, seguramente esta pidiendo esto. en [nombre de usuario] el usuario pondra su nombre de usuario de github, por ejemplo "ericrisco". 31 | Ejemplos githubProfile: 32 | "Quiero que cojais mi perfil de github, mi usuario es [nombre de usuario]" 33 | "Coje mi perfil de github, mi usuario es [nombre de usuario]" 34 | "Tengo cuenta en github" 35 | "Mi usuario de github es [nombre de usuario]" 36 | "mi usuario de github es ericrisco" 37 | 38 | - aboutMe: Para esta categoría, el usuario está proporcionando información sobre sí mismo, incluyendo su experiencia laboral, habilidades y ubicación actual o preferida. Es importante que el modelo reconozca la introducción de datos personales y profesionales para procesar y almacenar adecuadamente. 39 | Ejemplos aboutMe: 40 | "Soy un diseñador gráfico con 7 años de experiencia trabajando con Adobe Creative Suite. Actualmente, estoy basado en Barcelona y siempre he soñado con trabajar en una agencia de diseño de vanguardia en Madrid." 41 | "Soy un ingeniero de software con especialización en Python y Django. Con más de 3 años de experiencia, estoy buscando nuevas oportunidades desafiantes en remoto" 42 | 43 | - aboutMeModify: En este caso, el usuario está actualizando o modificando la información previamente proporcionada en su perfil. Esto puede incluir la adición de nuevas habilidades o la eliminación de las existentes, así como cambios en las preferencias de ubicación. 44 | Ejemplos aboutMeModify: 45 | "Además, recientemente he estado explorando la animación 3D en Cinema 4D. Sin embargo, ya no estoy interesado en el diseño de UX." 46 | "ya no busco trabajo en Barcelona." 47 | "en verdad ya no soy experto en c# pero tengo 12 años de experiencia en java" 48 | "Ya no busco trabajo en Barcelona, ahora lo busco en Valencia" 49 | "ya no quiero mudarme, quiero ofertas de barcelona" 50 | 51 | - help: Aquí, el usuario está solicitando asistencia con algo relacionado con el servicio o la plataforma. 52 | Ejemplos help: 53 | "Necesito ayuda para entender cómo funciona este bot." 54 | 55 | - jobs: En esta categoría, el usuario está buscando ofertas de trabajo de forma general, no se refiere a el mismo. Sino que esta consultando la base de datos de ofertas de trabajo. 56 | Ejemplos jobs: 57 | "Busco oportunidades laborales para arquitectos en Barcelona." 58 | "Estoy buscando ofertas de trabajo en Barcelona que solo tengan C# para ingenieros de software." 59 | 60 | - jobsProfile: Similar a "jobs", pero aquí el usuario desea que las ofertas de trabajo se basen en la información de su perfil. Siempre que se refiera a si mismo, "buscame", "encuantrame" o "mostrame" y pida algo relacionado con trabajo, se refiere a esta acción. 61 | Ejemplos jobsProfile: 62 | "Me gustaría encontrar ofertas de trabajo que coincidan con mi perfil." 63 | "Buscame trabajo" 64 | "¿Podrías mostrarme trabajos que sean adecuados para mis habilidades y experiencia?" 65 | 66 | - profile: En este caso, el usuario está solicitando ver su perfil. El modelo debe estar preparado para presentar la información de perfil del usuario en un formato fácil de entender. 67 | Ejemplos profile: 68 | "Quisiera ver mi perfil." 69 | "Que información tienes sobre mi?" 70 | "¿Podría revisar la información de mi perfil?" 71 | 72 | - joke: Aquí, el usuario está solicitando una broma o algo para reírse. El modelo debe ser capaz de proporcionar un chiste adecuado y apropiado para una audiencia general. 73 | Ejemplos joke: 74 | "Me gustaría escuchar un chiste." 75 | "¿Podrías contarme una broma para alegrar el día?" 76 | 77 | - delete: Para esta categoría, el usuario está solicitando la eliminación de su perfil o cuenta. El modelo debe estar preparado para guiar al usuario a través de este proceso o proporcionar información sobre cómo proceder. 78 | Ejemplos delete: 79 | "Quisiera eliminar mi perfil." 80 | "Quiero borrar toda mi información de este sitio." 81 | 82 | - other: Esta categoría es para todas las demás entradas que no se ajustan a las categorías anteriores. El modelo debe estar preparado para manejar una variedad de temas y proporcionar respuestas útiles y relevantes cuando sea posible. 83 | Ejemplos other: 84 | "Hoy hace un buen día." 85 | "¿Has visto la última película de la saga Star Wars?" 86 | "¿Cuál es tu color favorito?" 87 | 88 | Quiero que devuelvas un json con la acción el usuario quiere realizar. 89 | En el caso de "other", en el campo message quiero que respondas como lo haria Chatgpt3, pero tienes prohibido hacer preguntas. 90 | No quiero que respondas con un "no te entiendo" o "no se que quieres decir". 91 | El campo "message" no puede superar los 200 caracteres. 92 | En el campo "message" no uses emojis. 93 | Si el usuario usa insultos o tiene comportamientos adecuados responde acorde a tu entrenamiento poniendo lo que responderia Chatgpt3 en el campo "message". 94 | 95 | 96 | ` 97 | } 98 | ]; 99 | 100 | async function getNextAction(chatId, message, attempts = OPEN_AI_RATE_LIMIT_RETRIES) { 101 | try { 102 | const completion = await ai.createChatCompletion({ 103 | model: AI_MODEL, 104 | temperature: 0, 105 | messages: [ 106 | ...INITIAL_MESSAGES, 107 | { 108 | role: ChatCompletionRequestMessageRoleEnum.User, 109 | content: `{'message':'${message}'}` 110 | } 111 | ] 112 | }); 113 | 114 | const data = completion.data.choices[0].message?.content ?? {}; 115 | 116 | try { 117 | return JSON.parse(data); 118 | } catch (err) { 119 | return { action: 'other', message: messages.promptError }; 120 | } 121 | } catch (err) { 122 | if (err.response.status === 429 && attempts > 0) { 123 | sendMarkdownMessage(chatId, messages.giveMeTime); 124 | await new Promise((resolve) => setTimeout(resolve, OPEN_AI_DELAY_BETWEEN_RETRIES)); 125 | return getNextAction(chatId, message, attempts - 1); 126 | } 127 | return { action: 'other', message: messages.aiError }; 128 | } 129 | } 130 | 131 | module.exports = getNextAction; 132 | --------------------------------------------------------------------------------