Skip to content

Commit d457cf9

Browse files
committed
GH-173: Refactor resume generation and introduce server API
this close #173 Separated PDF and LaTeX generation logic from script entry points to reusable functions and added a standalone HTTP server for resume-related operations. The server provides endpoints for tailored resume generation, improving modularity and automation capabilities. Additional changes include updates to Dockerfile, ignore files, and CLI commands to support new features. Signed-off-by: David Ng <[email protected]>
1 parent a735a70 commit d457cf9

11 files changed

+282
-65
lines changed

systems/ai-assistant/README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,9 @@ You can utilize the `ai-assistant` system for various purposes, including runnin
139139
```bash
140140
node cli/commands/resume-summary.js
141141
```
142-
- To manage a CMS:
142+
- To generate cord intro messages:
143143
```bash
144-
node cli/commands/cms.js
144+
op run --env-file="./.env" -- node ./commands/cord-intro-messages.js
145145
```
146146

147147
Refer to specific command scripts in the `cli/commands` directory for more examples.

systems/ai-assistant/cli/.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
commands/chat.js
22
commands/chat.*.js
33
assets/jd.txt
4-
assets/*.json
4+
assets/*.json
5+
assets/*.pdf
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import * as openAi from './open-ai.js';
2+
import { getAssetContent } from './workspace.js';
3+
4+
const jd = await getAssetContent('jd.txt');
5+
6+
// Define the specific job/title of the person conducting the interview
7+
const interviewerPosition = 'Talent Lead'; // Examples: 'Engineering Team', 'CTO', 'HR Manager', etc.
8+
9+
const prompts = [
10+
openAi.jdSystem,
11+
await openAi.loadPortfolioIntoPrompts(),
12+
await openAi.processJD(jd).then(openAi.loadJDIntoPrompts),
13+
{
14+
content: `Please generate **up to 4 thoughtful and specific questions**, categorized under appropriate themes, using **simple and clear English**,
15+
that I can ask at the end of an interview with a **${interviewerPosition}** (the company representative conducting the interview).
16+
17+
Your response should:
18+
19+
1. Be tailored to the JD and the company's focus areas (e.g., tech stack, product, or culture).
20+
2. Address aspects most relevant to the **${interviewerPosition}'s** expertise, responsibilities, or perspective.
21+
3. Reflect curiosity, critical thinking, and genuine enthusiasm for the position.
22+
4. Organize questions under the most relevant themes, such as:
23+
- **Technical Work** (if applicable): Questions about technical challenges and team processes.
24+
- **Role-Specific**: Questions clarifying role expectations, success factors, or growth opportunities.
25+
- **Culture and Welfare**: Questions about company culture, inclusivity, employee development, and work-life balance.
26+
- **Leadership and Management** (if applicable): Questions about decision-making, collaboration, or leadership styles.
27+
28+
Examples of questions for each theme:
29+
- **Technical Work**: What are the most significant technical challenges your team is solving right now?
30+
- **Role-Specific**: What does success look like for this role, and how is it measured?
31+
- **Culture and Welfare**: How does your company support employees’ well-being or career growth?
32+
- **Leadership and Management**: Can you share how leadership fosters collaboration across teams?
33+
34+
Using this structure, **categorize** your response into the relevant themes based on the provided **${interviewerPosition}** and generate a maximum of 4 insightful, specific, and compelling questions.
35+
`,
36+
role: 'user',
37+
},
38+
];
39+
40+
openAi.prompt(prompts).then(openAi.readMessageFromPrompt).then(console.log);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as openAi from './open-ai.js';
2+
import { getAssetContent } from './workspace.js';
3+
4+
const jd = await getAssetContent('jd.txt');
5+
const prompts = [
6+
openAi.jdSystem,
7+
await openAi.loadPortfolioIntoPrompts(),
8+
await openAi.processJD(jd).then(openAi.loadJDIntoPrompts),
9+
{
10+
content: `I will share my draft cover letter in the next prompt.
11+
Please review and rewrite it using **simple and clear English**
12+
`,
13+
role: 'user',
14+
},
15+
];
16+
17+
openAi
18+
.withFeedbackLoop(openAi.prompt)(prompts)
19+
.then(openAi.readMessageFromPrompt)
20+
.then(console.log);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const resumeBuilderUrl = 'http://localhost:8080';
2+
3+
export function generateTailoredResumePdf(tailoredResumeJsonId) {
4+
return fetch(
5+
new URL(
6+
`/api/resume/resume-to-pdf?tailorResumeId=${tailoredResumeJsonId}`,
7+
resumeBuilderUrl,
8+
),
9+
{
10+
method: 'GET',
11+
},
12+
).then(res => res.body);
13+
}
14+
15+
export function generateTailoredATSResumePdf(tailoredResumeJsonId) {
16+
return fetch(
17+
new URL(
18+
`/api/resume/ats-resume-to-pdf?tailorResumeId=${tailoredResumeJsonId}`,
19+
resumeBuilderUrl,
20+
),
21+
{
22+
method: 'GET',
23+
},
24+
).then(res => res.body);
25+
}

systems/ai-assistant/cli/commands/tailor-resume-json.js

+18-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1+
import _fs from 'node:fs';
12
import fs from 'node:fs/promises';
23
import path from 'node:path';
4+
import { pipeline } from 'node:stream/promises';
35
import { scheduler } from 'node:timers/promises';
46

57
import * as cms from './cms.js';
68
import * as openAI from './open-ai.js';
9+
import {
10+
generateTailoredATSResumePdf,
11+
generateTailoredResumePdf,
12+
} from './resume.js';
713
import * as web from './web.js';
814
import {
15+
ASSETS_FOLDER,
916
getAssetContent,
1017
getTempDirectory,
1118
resolveAssetPath,
@@ -336,15 +343,20 @@ await generateTailoredProject().then(projects =>
336343
);
337344
await scheduler.wait(5000);
338345

339-
await fs.writeFile(
340-
resolveAssetPath(`${jdJson.id}.json`),
341-
JSON.stringify(defaultResume, null, 2),
342-
);
343346
await fs.writeFile(
344347
path.join(getTempDirectory(), `${jdJson.id}.json`),
345348
JSON.stringify(defaultResume, null, 2),
346349
);
347350

348-
console.log(
349-
`Use VITE_IS_TAILORED_RESUME=true VITE_RESUME_SOURCE=${jdJson.id}.json to use the tailored resume in resume`,
351+
await generateTailoredATSResumePdf(jdJson.id).then(data =>
352+
pipeline(
353+
data,
354+
_fs.createWriteStream(resolveAssetPath(`ats-${jdJson.id}.pdf`)),
355+
),
350356
);
357+
358+
await generateTailoredResumePdf(jdJson.id).then(data =>
359+
pipeline(data, _fs.createWriteStream(resolveAssetPath(`${jdJson.id}.pdf`))),
360+
);
361+
362+
console.log(`Check file named with ${jdJson.id} on ${ASSETS_FOLDER}`);

systems/resume/Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ FROM mcr.microsoft.com/playwright:v1.50.1 AS base
33

44
WORKDIR /app
55

6-
COPY ./package.json ./package-lock.json ./index.html ./vite.config.js ./resume-to-pdf.js ./resume-to-latex.js ./workspace.js ./
6+
COPY ./package.json ./package-lock.json ./index.html ./vite.config.js ./resume-to-pdf.js ./resume-to-latex.js ./workspace.js ./server.js ./
77
COPY ./scripts/docker/ ./scripts/docker/
88
COPY ./public/ ./public/
99

systems/resume/resume-to-latex.js

+38-33
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
export async function fetchResumeJson() {
2+
const os = await import('node:os');
3+
const fs = await import('node:fs/promises');
4+
const path = await import('node:path');
5+
const useTailoredResume =
6+
process.env['VITE_IS_TAILORED_RESUME'] === 'true' ? true : false;
7+
const resumeSource =
8+
process.env['VITE_RESUME_SOURCE'] ||
9+
'https://neviaumi.github.io/portfolio/resume.json';
10+
if (useTailoredResume) {
11+
return fs
12+
.readFile(path.join(os.tmpdir(), resumeSource), {
13+
encoding: 'utf8',
14+
})
15+
.then(JSON.parse);
16+
}
17+
return fetch(resumeSource).then(res => res.json());
18+
}
19+
120
export function resumeToLatex(resume) {
221
function escapes(source) {
322
return source.replaceAll('&', '\\&');
@@ -121,37 +140,23 @@ ${education.map(edu => ` \\item[-] ${edu.studyType} in ${edu.area}, ${edu
121140
\\end{document}`;
122141
}
123142

124-
async function fetchResumeJson() {
125-
const os = await import('node:os');
126-
const fs = await import('node:fs/promises');
127-
const path = await import('node:path');
128-
const useTailoredResume =
129-
process.env['VITE_IS_TAILORED_RESUME'] === 'true' ? true : false;
130-
const resumeSource =
131-
process.env['VITE_RESUME_SOURCE'] ||
132-
'https://neviaumi.github.io/portfolio/resume.json';
133-
if (useTailoredResume) {
134-
return fs
135-
.readFile(path.join(os.tmpdir(), resumeSource), {
136-
encoding: 'utf8',
137-
})
138-
.then(JSON.parse);
139-
}
140-
return fetch(resumeSource).then(res => res.json());
143+
const isMainExecution =
144+
import.meta.url === new URL(process.argv[1], 'file://').toString();
145+
if (isMainExecution) {
146+
fetchResumeJson()
147+
.then(resumeToLatex)
148+
.then(async latexText => {
149+
const workspace = await import('./workspace.js');
150+
const fs = await import('node:fs/promises');
151+
const fileName = await (async () => {
152+
const useTailoredResume = process.env['VITE_IS_TAILORED_RESUME']
153+
? true
154+
: false;
155+
if (!useTailoredResume) return 'resume.tex';
156+
const resumeJson = await fetchResumeJson();
157+
return resumeJson.meta.id + '.tex';
158+
})();
159+
await fs.writeFile(`${workspace.PUBLIC_FOLDER}/${fileName}`, latexText);
160+
return latexText;
161+
});
141162
}
142-
fetchResumeJson()
143-
.then(resumeToLatex)
144-
.then(async latexText => {
145-
const workspace = await import('./workspace.js');
146-
const fs = await import('node:fs/promises');
147-
const fileName = await (async () => {
148-
const useTailoredResume = process.env['VITE_IS_TAILORED_RESUME']
149-
? true
150-
: false;
151-
if (!useTailoredResume) return 'resume.tex';
152-
const resumeJson = await fetchResumeJson();
153-
return resumeJson.meta.id + '.tex';
154-
})();
155-
await fs.writeFile(`${workspace.PUBLIC_FOLDER}/${fileName}`, latexText);
156-
return latexText;
157-
});

systems/resume/resume-to-pdf.js

+26-22
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@ import path from 'node:path';
44
import playwright from 'playwright';
55
import { createServer } from 'vite';
66

7-
const WORKSPACE_ROOT = path.resolve(import.meta.dirname);
7+
import { WORKSPACE_ROOT } from './workspace.js';
88

9-
async function generateResumeToPDF(pdfPath) {
9+
export async function generateResumeToPDF(pdfPath) {
1010
const server = await createServer({
1111
server: {
12-
host: '0.0.0.0',
13-
port: 8080,
12+
port: 8081,
1413
},
1514
});
1615
await server.listen();
@@ -37,7 +36,7 @@ async function generateResumeToPDF(pdfPath) {
3736
});
3837

3938
await page.emulateMedia({ media: 'print' });
40-
await page.goto(`http://localhost:8080/`, {
39+
await page.goto(`http://localhost:8081/`, {
4140
waitUntil: 'networkidle',
4241
});
4342
// await page.waitForSelector('json-resume'); // Update selector as needed
@@ -53,21 +52,26 @@ async function generateResumeToPDF(pdfPath) {
5352
await server.close();
5453
}
5554

56-
const resumePdfFileName = await (async () => {
57-
const useTailoredResume = process.env['VITE_IS_TAILORED_RESUME']
58-
? true
59-
: false;
60-
const resumeSource = process.env['VITE_RESUME_SOURCE'];
61-
if (!useTailoredResume) return 'resume.pdf';
62-
return fs
63-
.readFile(path.join(os.tmpdir(), resumeSource), { encoding: 'utf8' })
64-
.then(JSON.parse)
65-
.then(({ meta: { id } }) => `${id}.pdf`);
66-
})();
55+
const isMainExecution =
56+
import.meta.url === new URL(process.argv[1], 'file://').toString();
6757

68-
await generateResumeToPDF(
69-
path.join(WORKSPACE_ROOT, 'public', resumePdfFileName),
70-
);
71-
console.log(
72-
`Resume PDF generated at ${path.join('public', resumePdfFileName)}`,
73-
);
58+
if (isMainExecution) {
59+
const resumePdfFileName = await (async () => {
60+
const useTailoredResume = process.env['VITE_IS_TAILORED_RESUME']
61+
? true
62+
: false;
63+
const resumeSource = process.env['VITE_RESUME_SOURCE'];
64+
if (!useTailoredResume) return 'resume.pdf';
65+
return fs
66+
.readFile(path.join(os.tmpdir(), resumeSource), { encoding: 'utf8' })
67+
.then(JSON.parse)
68+
.then(({ meta: { id } }) => `${id}.pdf`);
69+
})();
70+
71+
await generateResumeToPDF(
72+
path.join(WORKSPACE_ROOT, 'public', resumePdfFileName),
73+
);
74+
console.log(
75+
`Resume PDF generated at ${path.join('public', resumePdfFileName)}`,
76+
);
77+
}
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env bash
2+
3+
set -ex
4+
5+
node ./server.js

0 commit comments

Comments
 (0)