Turn CV to structured data with GPT-3 and Node.js
Tags: node, gpt3, resume, tutorial
Published: 2023-01-24 13:49:50

Large language models like GPT-3 are useful to simplify routine tasks. For example, we can take unstructured data, like a job applicant's resume in PDF, and turn them into structured data and fill them into our Applicant Tracking System (ATS) like Greenhouse or Workable.

In this article, I will show you a Node.js application which does that. But while GPT-3 simplifies routine data entry jobs, as developers we still need to connect all the respective APIs to download the applicant's CV, get text from the PDF, and put data to the ATS. Luckily, Superface gets us rid of this routine API wiring. We provide a unified interface across many API providers. Switching from, for example, Workable to Breezy HR, is a one line change.

Let's check what steps this application does:

1. Loads a CV of a candidate who applied for a given job from an Applicant Tracking System (ATS), like Workable or Greenhouse.
2. Converts the CV to text and parses the text into machine-readable data.
3. Updates the candidate in the ATS with the data obtained from the CV.

You can find the complete application in the [CV Analyzer repository](https://github.com/superfaceai/cv-analyzer). It uses the following packages:

- [@superfaceai/one-sdk](https://www.npmjs.com/package/@superfaceai/one-sdk) - Superface OneSDK is used to integrate with ATS, OpenAI and Cloudmersive SaaS providers.
- [node-fetch](https://www.npmjs.com/package/node-fetch) - NodeFetch is used to fetch the CV document.
- [dotenv](https://www.npmjs.com/package/dotenv) - dotenv is used to load environment variables.
- [inquirer](https://www.npmjs.com/package/inquirer) - Inquirer.js is used to build interactive user interface.

We also rely on these use cases from [Superface Catalog](https://superface.ai/catalog):

- [List Jobs]
- [List Candidates]
- [Get CV]
- [Convert Document to Text]
- [Text completion]
- [Update Candidate]

In this demo we use [Workable] as ATS, [Cloudmersive] to convert a candidate's CV to plain text, and [OpenAI] provider for conversion of CV to structured data. But you are not limited to just this three providers. For ATS use cases you can use also Breezy HR, Greenhouse and [many others](https://superface.ai/domain/ats).

## Setting up the application

1\. Clone the app repository and enter its directory.

```shell
git clone git@github.com:superfaceai/cv-analyzer.git
cd cv-analyzer
```

2\. Install dependencies.

```shell
npm install
```

3\. Create accounts (if you do not have them already) for the providers:

  - Workable offers [15 days free trial](https://www.workable.com/free-trial). A work e-mail address is required to create the account.
  - Cloudmersive has a [free tier](https://account.cloudmersive.com/signup) limited to 800 requests per month.
  - OpenAI offers a [free trial](https://openai.com/api/pricing/) with $18 in free credit usable during the first 3 months.

4\. Copy the example `.env` file.

```shell
cp .env.example .env
```

5\. Set credentials and integration parameters for the providers in the `.env` file.

  - Get [Workable Access Token and Subdomain](https://workable.readme.io/reference/generate-an-access-token#generate-an-api-access-token).
  - Create Cloudmersive [Free Tier Key](https://account.cloudmersive.com/keys).
  - Create OpenAI [Secret API Key](https://beta.openai.com/account/api-keys).

6\. Create a job position and add new candidate in Workable ATS.

7\. Start the app.

```shell
npm start
```

Once the application starts, it will show you a list of candidates. When you pick one, it will update candidate's entry with data parsed from their CV.


![A screenshot of a running application with selection of job position and a list of candidates for the position.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3hnrpie7xf7lg6gual1y.png)

## How the application works

The main flow of the application is in the [src/index.js] file, which calls use cases defined in [src/use_cases.js] file.

### Selecting the candidate and getting its CV

First we list open job positions and candidates for the selected position. This is handled with the [List Jobs] use case; once the user picks a job position, we use [List Candidates] use case with the respective ID of the job position:

```js
exports.listCandidates = async (sdk, providerOptions, jobId) => {
  const listCandidatesProfile = await sdk.getProfile(
    'recruitment/list-candidates@1.0.0'
  );
  const listCandidatesResult = await listCandidatesProfile
    .getUseCase('ListCandidates')
    .perform(
      {
        jobId,
      },
      providerOptions
    );
  return listCandidatesResult.unwrap().candidates;
};
```

We take the ID of the selected candidate and use the [Get CV] use case to obtain the URL of a document with the candidate's resume (typically that's a PDF, but the ATS may accept other formats as well):

```js
exports.getCVUrl = async (sdk, providerOptions, candidateId) => {
  const getCVProfile = await sdk.getProfile('recruitment/get-cv@1.0.0');
  const getCVResult = await getCVProfile.getUseCase('GetCV').perform(
    {
      candidateId,
    },
    providerOptions
  );
  return getCVResult.unwrap().cv.documentUrl;
};
```

### Converting CV to text

Now we know where the candidate's CV can be downloaded, but we need to extract the text from it somehow. We use [Convert Document to Text] use case for that – we fetch the document and upload it to the conversion provider (Cloudmersive in this example) and get a plain text in return.

```js
exports.convertCVToText = async (sdk, providerOptions, cvDocumentUrl) => {
  const docToTextProfile = await sdk.getProfile(
    'file-conversion/doc-to-text@1.0.0'
  );

  const fetchDocumentResponse = await fetch(cvDocumentUrl);

  if (!fetchDocumentResponse.body) {
    console.error('Failed to fetch CV document.');
    return;
  }

  const result = await docToTextProfile
    .getUseCase('ConvertDocumentToText')
    .perform(
      {
        fileName: 'cv.pdf',
        content: BinaryData.fromStream(fetchDocumentResponse.body),
      },
      providerOptions
    );

  return result.unwrap().text;
};
```

### Extracting structured data

Now it's time for some AI magic! We use OpenAI's GPT-3 model through the [Text Completion] use case. We provide the model with a prompt explaining we need particular data returned as JSON:

```js
exports.analyzeCV = async (sdk, providerOptions, cvText) => {
  try {
    const generateTextProfile = await sdk.getProfile('ai/generate-text@1.0.0');

    const promptCommand = `Parse following job applicant resume and return json object with properties 
    { 
      "firstName", "lastName", "address", "phone", 
      "education":
      [{"school", "fieldOfStudy", "studiedFrom_ISO8601":"YYYY-MM-DD", "studiedTill_ISO8601":"YYYY-MM-DD"}],
    "workHistory":
      [{"company", "position", "summary", "workedFrom_ISO8601:"YYYY-MM-DD"", "workedTill_ISO8601":"YYYY-MM-DD"}]
    }. `;

    const result = await generateTextProfile.getUseCase('CompleteText').perform(
      {
        prompt: promptCommand + cvText,
        creativity: 0.8,
        approxMaxWords: 1000,
        model: 'large',
      },
      providerOptions
    );

    analyzeCVOutcome = result.unwrap();
  } catch (error) {
    console.error('Failed to analyze CV.', error);
  }
  // ...
};
```

If the analysis goes well and the model “completes” our prompt, we turn the returned completion to JSON:

```js
const parsedCV = JSON.parse(analyzeCVOutcome.completions[0]);
const mappedCV = {
  ...parsedCV,
  education: parsedCV.education?.map(school => {
    return {
      school: school.school,
      degree: school.degree,
      fieldOfStudy: school.fieldOfStudy,
      startedAt: school.studiedFrom_ISO8601,
      endedAt: school.studiedTill_ISO8601,
    };
  }),
  workHistory: parsedCV.workHistory?.map(work => {
    // ...
  }),
};

return mappedCV;
```

### Update the candidate data

Now that we have a JSON with structured data extracted from the CV, we can send the data back to the ATS to update the candidate's data using the [Update Candidate] use case:

```js
exports.updateCandidate = async (sdk, providerOptions, candidate) => {
  const profile = await sdk.getProfile('recruitment/update-candidate@1.0.0');

  const result = await profile
    .getUseCase('UpdateCandidate')
    .perform(candidate, providerOptions);

  result.unwrap();

  return true;
};
```

## Using another ATS?

If you take a look at the code, there aren't any mentions of Workable, Cloudmersive or OpenAI. That's because the use cases are designed in provider-neutral way. In case of ATS, we use a [unified terminology](https://superface.ai/blog/integrating-any-applicant-tracking-system), so if you need to use another ATS, you don't need to rewrite the whole application. Instead, you can change `atsProviderOptions` in [src/index.js] file:

```js
// Options for Workable
const atsProviderOptions = {
  provider: 'workable',
  parameters: {
    SUBDOMAIN: process.env.WORKABLE_SUBDOMAIN,
  },
  security: {
    bearer_token: {
      token: process.env.WORKABLE_TOKEN,
    },
  },
};
```

And if you'd like to use, for example, Breezy HR, set `atsProviderOptions` like this:

```js
// Options for Breezy HR
const atsProviderOptions = {
  provider: 'breezy-hr',
  parameters: {
    COMPANY_ID: process.env.BREEZY_HR_COMPANY_ID,
    ACCESS_TOKEN: process.env.BREEZY_HR_ACCESS_TOKEN,
  },
};
```

## Next steps

In this demo application, we've used 6 different API use cases from 3 different API providers, without ever looking at the API docs. That's the advantage of Superface.

Do you need to integrate Applicant Tracking Systems like Workable, Lever, or SAP SuccessFactors? Check out our [Applicant Tracking Systems integrations](https://superface.ai/domain/ats). And don't miss other integrations in the [Superface catalog](https://superface.ai/catalog).

[workable]: https://www.workable.com
[cloudmersive]: https://cloudmersive.com
[openai]: https://openai.com
[src/index.js]: https://github.com/superfaceai/cv-analyzer/blob/main/src/index.js
[src/use_cases.js]: https://github.com/superfaceai/cv-analyzer/blob/main/src/use_cases.js
[list jobs]: https://superface.ai/recruitment/list-jobs
[list candidates]: https://superface.ai/recruitment/list-candidates
[get cv]: https://superface.ai/recruitment/get-cv
[convert document to text]: https://superface.ai/file-conversion/doc-to-text
[text completion]: https://superface.ai/ai/generate-text
[update candidate]: https://superface.ai/recruitment/update-candidate