jobs-monorepo/libs/browser-automation/src/openrouter/openrouter.ts
2026-01-26 17:33:48 +05:00

256 lines
9.6 KiB
TypeScript

import {
FormExtractionResult,
JobAnalysisResult,
JobDescription,
OpenRouterRequest,
OpenRouterResponse,
} from "./types";
export interface OpenRouterServiceOptions {
model: string;
apiKey: string;
}
export class OpenRouterService {
private model: string;
private apiKey: string;
private baseUrl = "https://openrouter.ai/api/v1/chat/completions";
constructor(options: OpenRouterServiceOptions) {
this.model = options.model;
this.apiKey = options.apiKey;
}
/**
* Clean markdown code blocks from response
*/
private cleanMarkdownCodeBlocks(content: string): string {
content = content.trim();
if (content.startsWith("```json")) {
content = content.slice(7);
} else if (content.startsWith("```")) {
content = content.slice(3);
}
if (content.endsWith("```")) {
content = content.slice(0, -3);
}
return content.trim();
}
/**
* Make a request to OpenRouter API
*/
private async makeRequest(
systemMessage: string,
userMessage: string
): Promise<string> {
const request: OpenRouterRequest = {
model: this.model,
messages: [
{ role: "system", content: systemMessage },
{ role: "user", content: userMessage },
],
};
const response = await fetch(this.baseUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(request),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`OpenRouter API error: ${response.status} - ${errorText}`
);
}
const data = (await response.json()) as OpenRouterResponse;
if (!data.choices || data.choices.length === 0) {
throw new Error("No response choices received from API");
}
const content = data.choices[0].message.content;
if (content.startsWith("<")) {
throw new Error(
`API returned HTML/XML instead of JSON: ${content.slice(0, 200)}`
);
}
return this.cleanMarkdownCodeBlocks(content);
}
/**
* Analyze a job description against a CV
*/
async analyzeJobDescription(
cv: string,
jobDesc: JobDescription
): Promise<JobAnalysisResult> {
const userMessage = `Analyze the following CV against the job description and criteria, then provide a recommendation following the schema below.
1) If there are missing skills, try to guess if they still match based on similar skills or experience in the cv.
for example: Javascript is mentioned in the cv, but the job requires Vanilla js, since they are the same thing, it should be included in the matching skills.
2) The job shouldn't require any language skills, preferbly only english.
3) The job should be remote, or provide relocation to the country.
CV:
${cv}
Job Description:
${jobDesc.description}
Job Criteria (key-value):
${JSON.stringify(jobDesc.criteria, null, 2)}
CRITICAL OUTPUT REQUIREMENTS:
- Return ONLY raw JSON - NO markdown formatting whatsoever
- NO backticks, NO code blocks, NO json prefix
- NO additional text before or after the JSON
- Start your response directly with { and end with }
- Use this exact schema and key names:
{
"recommendation": "apply" | "do_not_apply",
"confidence_score": number, // integer 0-100
"matching_skills": [string],
"missing_skills": [string],
"experience_match": "excellent" | "good" | "fair" | "poor",
"summary": string,
"improvement_suggestions": [string]
}`;
const systemMessage =
"You are an expert HR assistant specializing in job application analysis. You help candidates determine if they should apply for specific positions based on their CV and the job requirements. CRITICAL: You must respond with ONLY raw JSON - no markdown, no code blocks, no backticks, no additional text. Start directly with { and end with }.";
const jsonContent = await this.makeRequest(systemMessage, userMessage);
console.log("Raw API response:", jsonContent);
const result = JSON.parse(jsonContent) as JobAnalysisResult;
return result;
}
/**
* Create a job description from HTML content
*/
async createJobDescription(htmlContent: string): Promise<JobDescription> {
const userMessage = `You are a job posting data extractor. Analyze the HTML content and extract data according to this JSON structure:
{"description":"job description text","criteria":{"title":"Job Title","company":"Company","location":"Location","salary":"Salary","skills":"Skills","experience":"Experience","job_type":"Job Type","remote":"Remote"}}
CRITICAL RULES:
1. Return ONLY valid JSON - no markdown, no code blocks, no backticks, no explanations
2. Use SINGLE-LINE JSON (no pretty printing, no newlines inside the JSON)
3. All criteria values MUST be strings (never use booleans, numbers, or arrays)
4. Properly escape all quotes inside strings using \\"
5. Use only standard ASCII quotes ("), never smart quotes (" " ' ')
7. If no job description is found, use empty string for "description": ""
8. All criteria fields must be present with empty string "" if not found
Example output:
{"description":"Develop software applications...","criteria":{"title":"Backend Engineer","company":"Tech Corp","location":"Remote","salary":"$120k","skills":"Go, Docker","experience":"3+ years","job_type":"Full-time","remote":"Yes"}}
Extract from this HTML Content:
${htmlContent}`;
const systemMessage =
"You are an expert job description extractor. You convert unstructured job description text into a structured JSON format. CRITICAL: You must respond with ONLY raw JSON - no markdown, no code blocks, no backticks, no additional text. Start directly with { and end with }.";
const jsonContent = await this.makeRequest(systemMessage, userMessage);
console.log("Raw API response for job description:", jsonContent);
const result = JSON.parse(jsonContent) as JobDescription;
return result;
}
/**
* Extract form fields from HTML and populate them based on CV data.
* Also adjusts the CV based on job type (frontend/backend/fullstack).
*/
async applyForJob(
htmlForm: string,
cvMarkdown: string,
jobDescription: string
): Promise<FormExtractionResult> {
const userMessage = `You are a form field extractor, auto-filler, and CV adapter. Analyze the HTML form and job description, then:
1. Extract ALL form fields and populate them with appropriate values from the CV
HTML FORM:
${htmlForm}
JOB DESCRIPTION:
${jobDescription}
ORIGINAL CV (Markdown):
${cvMarkdown}
CV ADJUSTMENT RULES:
- Detect if the job is: FRONTEND, BACKEND, or FULLSTACK based on the job description
- If FRONTEND: Keep only frontend-related skills, projects, and experience (React, Vue, Angular, CSS, HTML, UI/UX, etc.). Remove backend-specific content.
- If BACKEND: Keep only backend-related skills, projects, and experience (APIs, databases, servers, Go, Node.js, Python, etc.). Remove frontend-specific content.
- If FULLSTACK: Keep both frontend and backend content.
- For any OTHER job type (not frontend/backend/fullstack): Treat as FRONTEND by default.
- Maintain the same markdown structure and formatting as the original CV.
- Do NOT invent new skills or experience - only filter existing content.
CRITICAL RULES:
1. Return ONLY valid JSON - no markdown, no code blocks, no backticks, no explanations
2. Extract ALL input fields, textareas, selects, and buttons from the form
3. For each field, determine the appropriate value from the ADJUSTED CV
4. If a field cannot be populated from the CV (e.g., password, captcha), leave value as empty string
5. Identify the apply/submit button text
6. Use the exact JSON schema below
7. If a field is already populated, ignore it (don't send it in the results)
JSON Schema:
{
"fields": [
{
"label": "Field label or name attribute",
"field_name": "name or id attribute of the input",
"field_type": "text|email|tel|password|number|date|datetime-local|time|month|week|url|search|color|range|file|hidden|checkbox|radio|select|textarea|button|submit|reset",
"value": "Value to populate based on CV data",
"placeholder": "Placeholder text if any",
"required": true|false,
"selector": "Unique CSS selector for the input (e.g., #email, input[name='email'], .form-field-email). Use id selector if available, otherwise name attribute, otherwise class. Empty string if no unique selector can be determined."
}
],
"apply_button": "Unique CSS selector for the form's submit button",
}
FIELD MAPPING GUIDELINES:
- Name fields: Extract full name, first name, last name from CV
- Email: Use email from CV contact info
- Phone: Use phone number from CV
- LinkedIn/Portfolio/Website: Use URLs from CV
- Experience/Years: Calculate from CV work history
- Current company/title: Use most recent from CV
- Skills: List relevant skills from ADJUSTED CV
- Education: Use education details from CV
- Cover letter/Message: Generate a brief professional message based on ADJUSTED CV
- Salary expectations: Leave empty unless specified in CV
- Location/Address: Use from CV contact info
- Resume/CV upload: Leave value empty (file upload)
Start your response with { and end with }`;
const systemMessage =
"You are an expert form analyzer and auto-filler. You extract form fields from HTML and intelligently populate them with data from a CV/resume. CRITICAL: You must respond with ONLY raw JSON - no markdown, no code blocks, no backticks, no additional text. Start directly with { and end with }.";
const jsonContent = await this.makeRequest(systemMessage, userMessage);
console.log("Raw API response for form extraction:", jsonContent);
const result = JSON.parse(jsonContent) as FormExtractionResult;
return result;
}
}