jobscout 1.4.1 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -16
- package/bin/cli.mjs +179 -68
- package/package.json +4 -6
- package/chatgpt/GPT-INSTRUCTIONS.md +0 -103
- package/chatgpt/README.md +0 -72
- package/chatgpt/knowledge/job.schema.json +0 -58
- package/chatgpt/knowledge/match.schema.json +0 -48
- package/chatgpt/knowledge/profile.schema.json +0 -121
- package/chatgpt/knowledge/skill-application-assistant.md +0 -38
- package/chatgpt/knowledge/skill-bilingual-normalization.md +0 -46
- package/chatgpt/knowledge/skill-candidate-intake.md +0 -43
- package/chatgpt/knowledge/skill-find-jobs.md +0 -50
- package/chatgpt/knowledge/skill-fit-analyzer.md +0 -37
- package/chatgpt/knowledge/skill-job-collector.md +0 -21
- package/chatgpt/knowledge/skill-job-matcher.md +0 -16
- package/chatgpt/knowledge/skill-job-schema.md +0 -62
- package/chatgpt/knowledge/skill-scoring-rubric.md +0 -108
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Thu thập job thật từ ITviec, TopCV, VietnamWorks, LinkedIn… qua web sear
|
|
|
6
6
|
(skills · seniority · domain · compensation · location · culture), xuất báo cáo fit/gap chi tiết
|
|
7
7
|
với link apply cho từng job.
|
|
8
8
|
|
|
9
|
-
**Chạy được trên Claude Code
|
|
9
|
+
**Chạy được trên Claude Code và Codex CLI** — cùng một logic, một lệnh cài đặt.
|
|
10
10
|
|
|
11
11
|
## Cài đặt
|
|
12
12
|
|
|
@@ -14,12 +14,12 @@ với link apply cho từng job.
|
|
|
14
14
|
npx jobscout setup
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Hiện menu chọn nền tảng (Claude Code / Codex / All) ngay trên terminal — dùng ↑/↓ di chuyển,
|
|
18
|
+
`space` chọn, `enter` xác nhận. Cài xong sẽ hiển thị hướng dẫn bước tiếp theo. Hoặc chỉ định thẳng:
|
|
18
19
|
|
|
19
20
|
```bash
|
|
20
21
|
npx jobscout setup claude # Claude Code
|
|
21
22
|
npx jobscout setup codex # Codex CLI
|
|
22
|
-
npx jobscout setup chatgpt # ChatGPT (Custom GPT)
|
|
23
23
|
npx jobscout setup all # Tất cả
|
|
24
24
|
```
|
|
25
25
|
|
|
@@ -40,16 +40,6 @@ codex --search
|
|
|
40
40
|
# Gọi skill find-jobs với đường dẫn CV
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
### ChatGPT
|
|
44
|
-
|
|
45
|
-
CLI tạo thư mục `job-matching-chatgpt/` chứa sẵn mọi thứ:
|
|
46
|
-
|
|
47
|
-
1. Vào [chatgpt.com/gpts/editor](https://chatgpt.com/gpts/editor) → **Create**
|
|
48
|
-
2. **Instructions**: dán nội dung `job-matching-chatgpt/GPT-INSTRUCTIONS.md`
|
|
49
|
-
3. **Knowledge**: kéo-thả tất cả file trong `job-matching-chatgpt/knowledge/`
|
|
50
|
-
4. Bật **Web Search** → Save → Publish
|
|
51
|
-
5. Mở GPT, dán CV là chạy
|
|
52
|
-
|
|
53
43
|
## Pipeline
|
|
54
44
|
|
|
55
45
|
```
|
|
@@ -105,9 +95,6 @@ JSON schema trong `job-matching-plugin/schemas/`:
|
|
|
105
95
|
│ ├── schemas/*.json Data contracts
|
|
106
96
|
│ ├── agents/*.md Subagent definitions
|
|
107
97
|
│ └── commands/find-jobs.md Slash command /find-jobs
|
|
108
|
-
├── chatgpt/ ← adapter ChatGPT
|
|
109
|
-
│ ├── GPT-INSTRUCTIONS.md Orchestrator cho Custom GPT
|
|
110
|
-
│ └── knowledge/ Skills + schemas flatten sẵn
|
|
111
98
|
└── package.json
|
|
112
99
|
```
|
|
113
100
|
|
package/bin/cli.mjs
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { resolve, join, dirname } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { emitKeypressEvents } from "node:readline";
|
|
6
7
|
|
|
7
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
9
|
const PKG_ROOT = resolve(__dirname, "..");
|
|
9
10
|
const PLUGIN_SRC = join(PKG_ROOT, "job-matching-plugin");
|
|
10
|
-
const CHATGPT_SRC = join(PKG_ROOT, "chatgpt");
|
|
11
11
|
const CWD = process.cwd();
|
|
12
|
+
const REPO_URL = "https://github.com/hieutrong512/JobMatching";
|
|
12
13
|
|
|
13
14
|
const RESET = "\x1b[0m";
|
|
14
15
|
const GREEN = "\x1b[32m";
|
|
@@ -32,16 +33,15 @@ ${BOLD}Usage:${RESET}
|
|
|
32
33
|
npx jobscout ${CYAN}<command>${RESET}
|
|
33
34
|
|
|
34
35
|
${BOLD}Commands:${RESET}
|
|
35
|
-
${CYAN}setup${RESET}
|
|
36
|
-
${CYAN}setup claude${RESET}
|
|
37
|
-
${CYAN}setup codex${RESET}
|
|
38
|
-
${CYAN}setup
|
|
39
|
-
${CYAN}setup all${RESET} Setup for all platforms
|
|
36
|
+
${CYAN}setup${RESET} Chọn nền tảng (menu) rồi cài đặt
|
|
37
|
+
${CYAN}setup claude${RESET} Cài cho Claude Code
|
|
38
|
+
${CYAN}setup codex${RESET} Cài cho Codex CLI
|
|
39
|
+
${CYAN}setup all${RESET} Cài cho tất cả nền tảng
|
|
40
40
|
|
|
41
41
|
${BOLD}Examples:${RESET}
|
|
42
|
-
${DIM}npx jobscout setup ${RESET}#
|
|
43
|
-
${DIM}npx jobscout setup claude ${RESET}# Claude Code
|
|
44
|
-
${DIM}npx jobscout setup
|
|
42
|
+
${DIM}npx jobscout setup ${RESET}# hiện menu chọn Claude Code / Codex
|
|
43
|
+
${DIM}npx jobscout setup claude ${RESET}# chỉ Claude Code
|
|
44
|
+
${DIM}npx jobscout setup all ${RESET}# tất cả
|
|
45
45
|
`;
|
|
46
46
|
|
|
47
47
|
function copyDir(src, dest) {
|
|
@@ -73,6 +73,103 @@ function detectPlatforms() {
|
|
|
73
73
|
return platforms;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
77
|
+
// Interactive multi-select (zero-dep). Trả về mảng platform ("claude"/"codex").
|
|
78
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
79
|
+
function selectPlatforms(defaults = []) {
|
|
80
|
+
const items = [
|
|
81
|
+
{ value: "claude", label: "Claude Code", checked: defaults.includes("claude") },
|
|
82
|
+
{ value: "codex", label: "Codex", checked: defaults.includes("codex") }
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
return new Promise((res) => {
|
|
86
|
+
// Không phải TTY (CI, pipe) → không hỏi, dùng defaults hoặc cả hai.
|
|
87
|
+
if (!process.stdin.isTTY) {
|
|
88
|
+
const picked = items.filter((i) => i.checked).map((i) => i.value);
|
|
89
|
+
res(picked.length ? picked : ["claude", "codex"]);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let cursor = 0;
|
|
94
|
+
let rendered = 0;
|
|
95
|
+
|
|
96
|
+
const allChecked = () => items.every((i) => i.checked);
|
|
97
|
+
const anyChecked = () => items.some((i) => i.checked);
|
|
98
|
+
|
|
99
|
+
function render() {
|
|
100
|
+
// Xóa các dòng đã vẽ lần trước.
|
|
101
|
+
if (rendered > 0) process.stdout.write(`\x1b[${rendered}A`);
|
|
102
|
+
const lines = [];
|
|
103
|
+
lines.push(`${CYAN}◆${RESET} ${BOLD}Cài JobScout cho nền tảng nào?${RESET}`);
|
|
104
|
+
lines.push(`${DIM}│${RESET} ${DIM}↑/↓ di chuyển · space chọn · a chọn tất cả · enter xác nhận${RESET}`);
|
|
105
|
+
items.forEach((item, i) => {
|
|
106
|
+
const box = item.checked ? `${GREEN}[+]${RESET}` : "[ ]";
|
|
107
|
+
const pointer = i === cursor ? `${CYAN}❯${RESET}` : " ";
|
|
108
|
+
const label = i === cursor ? `${BOLD}${item.label}${RESET}` : item.label;
|
|
109
|
+
lines.push(`${DIM}│${RESET} ${pointer} ${box} ${label}`);
|
|
110
|
+
});
|
|
111
|
+
const allBox = allChecked() ? `${GREEN}[+]${RESET}` : "[ ]";
|
|
112
|
+
const allPointer = cursor === items.length ? `${CYAN}❯${RESET}` : " ";
|
|
113
|
+
const allLabel = cursor === items.length ? `${BOLD}All${RESET}` : "All";
|
|
114
|
+
lines.push(`${DIM}│${RESET} ${allPointer} ${allBox} ${allLabel}`);
|
|
115
|
+
lines.push(`${DIM}└${RESET}`);
|
|
116
|
+
// Xóa cuối mỗi dòng để không sót ký tự cũ.
|
|
117
|
+
process.stdout.write(lines.map((l) => `\x1b[2K${l}`).join("\n") + "\n");
|
|
118
|
+
rendered = lines.length;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function cleanup() {
|
|
122
|
+
process.stdin.setRawMode(false);
|
|
123
|
+
process.stdin.pause();
|
|
124
|
+
process.stdin.removeListener("keypress", onKey);
|
|
125
|
+
process.stdout.write("\x1b[?25h"); // hiện lại con trỏ
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function toggleCurrent() {
|
|
129
|
+
if (cursor === items.length) {
|
|
130
|
+
// Dòng "All": bật/tắt tất cả.
|
|
131
|
+
const next = !allChecked();
|
|
132
|
+
items.forEach((i) => (i.checked = next));
|
|
133
|
+
} else {
|
|
134
|
+
items[cursor].checked = !items[cursor].checked;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function onKey(str, key) {
|
|
139
|
+
if (!key) return;
|
|
140
|
+
if (key.name === "up") {
|
|
141
|
+
cursor = (cursor - 1 + (items.length + 1)) % (items.length + 1);
|
|
142
|
+
} else if (key.name === "down") {
|
|
143
|
+
cursor = (cursor + 1) % (items.length + 1);
|
|
144
|
+
} else if (key.name === "space") {
|
|
145
|
+
toggleCurrent();
|
|
146
|
+
} else if (str === "a" || str === "A") {
|
|
147
|
+
const next = !allChecked();
|
|
148
|
+
items.forEach((i) => (i.checked = next));
|
|
149
|
+
} else if (key.name === "return") {
|
|
150
|
+
if (!anyChecked()) return; // cần chọn ít nhất một
|
|
151
|
+
cleanup();
|
|
152
|
+
log("");
|
|
153
|
+
res(items.filter((i) => i.checked).map((i) => i.value));
|
|
154
|
+
return;
|
|
155
|
+
} else if (key.name === "escape" || (key.ctrl && key.name === "c")) {
|
|
156
|
+
cleanup();
|
|
157
|
+
log("");
|
|
158
|
+
warn("Đã hủy.");
|
|
159
|
+
process.exit(0);
|
|
160
|
+
}
|
|
161
|
+
render();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
emitKeypressEvents(process.stdin);
|
|
165
|
+
process.stdin.setRawMode(true);
|
|
166
|
+
process.stdin.resume();
|
|
167
|
+
process.stdout.write("\x1b[?25l"); // ẩn con trỏ
|
|
168
|
+
render();
|
|
169
|
+
process.stdin.on("keypress", onKey);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
76
173
|
// Skills cần cho Claude — bỏ find-jobs/job-collector/job-matcher (command + agents đã lo).
|
|
77
174
|
const CLAUDE_SKILLS = [
|
|
78
175
|
"candidate-intake",
|
|
@@ -124,10 +221,11 @@ function setupClaude() {
|
|
|
124
221
|
});
|
|
125
222
|
ok(`Copy .claude/skills/ (${CLAUDE_SKILLS.length} skills)`);
|
|
126
223
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
224
|
+
return {
|
|
225
|
+
label: "Claude Code",
|
|
226
|
+
detail: `${CLAUDE_SKILLS.length} skills + 2 agents → .claude/`,
|
|
227
|
+
steps: [`Trong Claude Code chạy: ${CYAN}/find-jobs path/to/CV.pdf${RESET}`]
|
|
228
|
+
};
|
|
131
229
|
}
|
|
132
230
|
|
|
133
231
|
// Codex chỉ auto-discover skills trong skills/ — nên flatten mọi thứ (kể cả
|
|
@@ -162,78 +260,91 @@ function setupCodex() {
|
|
|
162
260
|
});
|
|
163
261
|
ok(`Copy .agents/skills/ (${CODEX_SKILLS.length} skills)`);
|
|
164
262
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
263
|
+
return {
|
|
264
|
+
label: "Codex CLI",
|
|
265
|
+
detail: `${CODEX_SKILLS.length} skills → .agents/`,
|
|
266
|
+
steps: [
|
|
267
|
+
`Chạy: ${CYAN}codex --search${RESET} ${DIM}# bật web search${RESET}`,
|
|
268
|
+
`Gọi skill ${CYAN}find-jobs${RESET} với đường dẫn CV`
|
|
269
|
+
]
|
|
270
|
+
};
|
|
169
271
|
}
|
|
170
272
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
273
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
274
|
+
// Hộp "ready to use" — hướng dẫn bước tiếp theo sau khi cài xong.
|
|
275
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
276
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
277
|
+
function visibleLen(s) { return s.replace(ANSI_RE, "").length; }
|
|
176
278
|
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
279
|
+
function printReadyBox(results) {
|
|
280
|
+
const lines = [];
|
|
281
|
+
results.forEach((r) => {
|
|
282
|
+
lines.push(`${GREEN}✓${RESET} ${BOLD}${r.label}${RESET} ${DIM}${r.detail}${RESET}`);
|
|
283
|
+
});
|
|
284
|
+
lines.push("");
|
|
285
|
+
lines.push(`${DIM}Installed to:${RESET} ${CWD}`);
|
|
286
|
+
lines.push("");
|
|
287
|
+
lines.push(`${BOLD}Bước tiếp theo:${RESET}`);
|
|
288
|
+
lines.push(` 1. Mở project ở folder này bằng AI agent của bạn`);
|
|
289
|
+
let n = 2;
|
|
290
|
+
results.forEach((r) => {
|
|
291
|
+
r.steps.forEach((s) => {
|
|
292
|
+
lines.push(` ${n}. ${s}`);
|
|
293
|
+
n += 1;
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
lines.push("");
|
|
297
|
+
lines.push(`${DIM}Repo: ${REPO_URL}${RESET}`);
|
|
188
298
|
|
|
299
|
+
const inner = Math.max(...lines.map(visibleLen), visibleLen("JobScout đã sẵn sàng!")) + 2;
|
|
300
|
+
const top = `┌─ ${BOLD}${GREEN}JobScout đã sẵn sàng!${RESET} ${"─".repeat(Math.max(0, inner - visibleLen("JobScout đã sẵn sàng!") - 3))}┐`;
|
|
189
301
|
log("");
|
|
190
|
-
|
|
191
|
-
log(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
302
|
+
log(top);
|
|
303
|
+
log(`│${" ".repeat(inner)}│`);
|
|
304
|
+
lines.forEach((l) => {
|
|
305
|
+
const pad = inner - 1 - visibleLen(l);
|
|
306
|
+
log(`│ ${l}${" ".repeat(Math.max(0, pad))}│`);
|
|
307
|
+
});
|
|
308
|
+
log(`│${" ".repeat(inner)}│`);
|
|
309
|
+
log(`└${"─".repeat(inner)}┘`);
|
|
195
310
|
}
|
|
196
311
|
|
|
197
|
-
function
|
|
312
|
+
function runPlatforms(platforms) {
|
|
313
|
+
const results = [];
|
|
314
|
+
platforms.forEach((p) => {
|
|
315
|
+
if (p === "claude") results.push(setupClaude());
|
|
316
|
+
if (p === "codex") results.push(setupCodex());
|
|
317
|
+
});
|
|
318
|
+
printReadyBox(results);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function runSetup(platform) {
|
|
198
322
|
log(`${BOLD}🔧 JobScout Setup${RESET}`);
|
|
199
323
|
|
|
200
|
-
if (
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
setupClaude();
|
|
211
|
-
setupCodex();
|
|
212
|
-
setupChatgpt();
|
|
213
|
-
}
|
|
214
|
-
} else if (platform === "claude") {
|
|
215
|
-
setupClaude();
|
|
216
|
-
} else if (platform === "codex") {
|
|
217
|
-
setupCodex();
|
|
218
|
-
} else if (platform === "chatgpt") {
|
|
219
|
-
setupChatgpt();
|
|
220
|
-
} else if (platform === "all") {
|
|
221
|
-
setupClaude();
|
|
222
|
-
setupCodex();
|
|
223
|
-
setupChatgpt();
|
|
224
|
-
} else {
|
|
225
|
-
warn(`Platform "${platform}" không hợp lệ. Dùng: claude, codex, chatgpt, all`);
|
|
324
|
+
if (platform === "all") {
|
|
325
|
+
runPlatforms(["claude", "codex"]);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (platform === "claude" || platform === "codex") {
|
|
329
|
+
runPlatforms([platform]);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (platform) {
|
|
333
|
+
warn(`Platform "${platform}" không hợp lệ. Dùng: claude, codex, all`);
|
|
226
334
|
process.exit(1);
|
|
227
335
|
}
|
|
228
336
|
|
|
229
|
-
|
|
230
|
-
|
|
337
|
+
// Không chỉ định platform → hỏi qua menu (mặc định gợi ý theo nền tảng phát hiện được).
|
|
338
|
+
const detected = detectPlatforms();
|
|
339
|
+
if (detected.length) info(`Phát hiện sẵn: ${detected.join(", ")}`);
|
|
340
|
+
const picked = await selectPlatforms(detected);
|
|
341
|
+
runPlatforms(picked);
|
|
231
342
|
}
|
|
232
343
|
|
|
233
344
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
234
345
|
log(HELP);
|
|
235
346
|
} else if (command === "setup") {
|
|
236
|
-
runSetup(process.argv[3]);
|
|
347
|
+
await runSetup(process.argv[3]);
|
|
237
348
|
} else {
|
|
238
349
|
warn(`Lệnh "${command}" không tồn tại.`);
|
|
239
350
|
log(HELP);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jobscout",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "AI job matching — tìm và xếp hạng việc làm phù hợp CV, song ngữ Việt–Anh. Chạy trên Claude Code
|
|
3
|
+
"version": "1.5.2",
|
|
4
|
+
"description": "AI job matching — tìm và xếp hạng việc làm phù hợp CV, song ngữ Việt–Anh. Chạy trên Claude Code và Codex CLI.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"jobscout": "bin/cli.mjs"
|
|
7
7
|
},
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
"recruitment",
|
|
15
15
|
"claude",
|
|
16
16
|
"codex",
|
|
17
|
-
"chatgpt",
|
|
18
17
|
"ai-agents",
|
|
19
18
|
"vietnam"
|
|
20
19
|
],
|
|
@@ -25,7 +24,7 @@
|
|
|
25
24
|
"license": "MIT",
|
|
26
25
|
"repository": {
|
|
27
26
|
"type": "git",
|
|
28
|
-
"url": "git+https://github.com/hieutrong512/
|
|
27
|
+
"url": "git+https://github.com/hieutrong512/JobScout.git"
|
|
29
28
|
},
|
|
30
29
|
"files": [
|
|
31
30
|
"bin/",
|
|
@@ -37,8 +36,7 @@
|
|
|
37
36
|
"job-matching-plugin/commands/",
|
|
38
37
|
"job-matching-plugin/docs/",
|
|
39
38
|
"job-matching-plugin/AGENTS.md",
|
|
40
|
-
"job-matching-plugin/README.md"
|
|
41
|
-
"chatgpt/"
|
|
39
|
+
"job-matching-plugin/README.md"
|
|
42
40
|
],
|
|
43
41
|
"engines": {
|
|
44
42
|
"node": ">=18"
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
# JobMatching — hướng dẫn cho ChatGPT (Custom GPT / Project)
|
|
2
|
-
|
|
3
|
-
Bạn là **JobMatching**, trợ lý tìm và xếp hạng việc làm phù hợp nhất với **target** và **CV**
|
|
4
|
-
của ứng viên, song ngữ Việt–Anh. Bạn thu thập job qua **web browsing** (Search) — không cần API key.
|
|
5
|
-
|
|
6
|
-
> Dán toàn bộ file này vào ô **Instructions** của một Custom GPT (bật **Web Search**), hoặc dùng làm
|
|
7
|
-
> chỉ dẫn cho một ChatGPT Project. Nếu có upload các file knowledge (`*.md` skill + `*.schema.json`),
|
|
8
|
-
> hãy ưu tiên đọc chúng để lấy chi tiết; nếu không có, làm theo bản rút gọn dưới đây là đủ.
|
|
9
|
-
|
|
10
|
-
## Môi trường ChatGPT
|
|
11
|
-
|
|
12
|
-
- **Web**: dùng công cụ browsing/Search sẵn có để tìm và mở JD. Không bịa job hay URL.
|
|
13
|
-
- **File**: ChatGPT không có ổ đĩa lâu dài. Giữ dữ liệu trung gian (profile/jobs/shortlist) **dạng JSON
|
|
14
|
-
ngay trong hội thoại** (khối ```json). Nếu bật Code Interpreter, có thể ghi file tạm để tải về, nhưng
|
|
15
|
-
không bắt buộc.
|
|
16
|
-
- Không tự nộp hồ sơ / gửi email / gửi tin nhắn thay người dùng.
|
|
17
|
-
|
|
18
|
-
## Pipeline (chạy tuần tự trong 1 hội thoại)
|
|
19
|
-
|
|
20
|
-
```
|
|
21
|
-
CV + Target → [1] Intake → profile(JSON)
|
|
22
|
-
→ [2] Collect → jobs(JSON, tối đa 20)
|
|
23
|
-
→ [3] Match → shortlist(JSON đã xếp hạng)
|
|
24
|
-
→ [4] Fit report (Markdown)
|
|
25
|
-
→ [5] (tùy chọn) Application assistant
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
### [1] Intake — tạo `profile`
|
|
29
|
-
- Nhận CV (dán text, hoặc upload PDF/DOCX → trích text). Trích: thông tin cá nhân tối thiểu, skills,
|
|
30
|
-
kinh nghiệm (title/công ty/thời gian/highlights), học vấn, ngôn ngữ. Suy `total_years` + `seniority`.
|
|
31
|
-
- Hỏi target **gộp 1 lần** (bỏ qua phần người dùng đã nêu): (1) vai trò + cấp bậc; (2) địa điểm + remote;
|
|
32
|
-
(3) lương min & target (VND/USD, tháng/năm); (4) ngành + quy mô công ty; (5) dealbreakers;
|
|
33
|
-
(6) **ưu tiên** → chuyển thành trọng số `priorities` (tổng ~1) cho bước chấm điểm.
|
|
34
|
-
- Chuẩn hóa skills/chức danh về **canonical EN** (xem "Chuẩn hóa" bên dưới). Tóm tắt cho user xác nhận.
|
|
35
|
-
|
|
36
|
-
### [2] Collect — tạo `jobs`
|
|
37
|
-
- Sinh query song ngữ từ role/seniority/location/skills. Tìm trên job boards chính thống
|
|
38
|
-
(ITviec, TopCV, VietnamWorks, LinkedIn, CareerViet…).
|
|
39
|
-
- **Chỉ giữ job đọc được full JD và còn hạn.** Bỏ: snippet rời rạc, hết hạn, 404–410. Mục tiêu ≤ 20 job.
|
|
40
|
-
- Khử trùng theo URL chuẩn hóa (bỏ query tracking). Mỗi job đặt `extraction_confidence` (0–1) trung thực:
|
|
41
|
-
0.8–1.0 nếu đọc được full JD; ≤0.4 nếu chỉ có snippet.
|
|
42
|
-
- **`url` luôn là link gốc để bấm apply.** Nếu JD có contact (email/phone/Zalo/form) → ghi lại.
|
|
43
|
-
|
|
44
|
-
### [3] Match — tạo `shortlist`
|
|
45
|
-
Chấm từng job theo rubric, xếp giảm dần, để `excluded` cuối.
|
|
46
|
-
|
|
47
|
-
**Trọng số mặc định** (chuẩn hóa để tổng = 1; nếu user có `priorities` thì dùng nó, cũng chuẩn hóa):
|
|
48
|
-
skills 0.35 · seniority 0.20 · domain 0.15 · compensation 0.15 · location 0.05 · culture 0.05
|
|
49
|
-
|
|
50
|
-
**Hard filter trước:** vi phạm dealbreaker (có đủ bằng chứng) → `recommendation="excluded"`, `overall=0`.
|
|
51
|
-
Thiếu dữ liệu để khẳng định vi phạm → KHÔNG loại, chỉ hạ `confidence`.
|
|
52
|
-
|
|
53
|
-
**Điểm mỗi chiều 0–100:**
|
|
54
|
-
- **skills** = `100*(0.75*must_cover + 0.25*nice_cover)`; +≤10 nếu có skill advanced/expert cho must-have chính (cap 100). Ghi `matched_skills`, `missing_must_have`.
|
|
55
|
-
- **seniority**: khoảng cách bậc (intern<junior<mid<senior<lead<manager<director) 0→100, 1→75, 2→45, ≥3→15. Overqualified cũng bị trừ. `total_years < min_years` → nhân 0.6.
|
|
56
|
-
- **domain**: trùng ngành 100; ngành liền kề 60; không liên quan 30.
|
|
57
|
-
- **location**: remote khớp / đúng địa điểm 100; hybrid cùng thành phố 85; onsite khác TP mà user để "any" 60; ngoài vùng (chưa tới dealbreaker) 25.
|
|
58
|
-
- **compensation**: quy về VND/tháng để so ($1 = 26.100 VND, year→month chia 12). `job_mid ≥ target` → 100; trong [min,target] nội suy 70–100; ≥90% min → 50; thấp hơn → 20. **Lương unknown → 60 neutral (KHÔNG chấm 0)**, hạ confidence.
|
|
59
|
-
- **culture**: khớp `company_size` +baseline; tín hiệu tốt trong JD cộng thêm; ít dữ liệu → 60.
|
|
60
|
-
|
|
61
|
-
**Tổng hợp:** `overall = Σ(dimension_score[d] * normalized_weight[d])`. Map:
|
|
62
|
-
≥80 strong · 65–79 good · 50–64 maybe · <50 weak · (dealbreaker) excluded.
|
|
63
|
-
**Không bao giờ chấm 0 vì "thiếu thông tin"** — dùng neutral 55–60 và hạ `confidence`.
|
|
64
|
-
Luôn ghi `weights_used`, `rationale` (1–2 lý do mạnh + 1 gap lớn nhất), `confidence`.
|
|
65
|
-
|
|
66
|
-
### [4] Fit report (Markdown) — GỬI CHO USER
|
|
67
|
-
- **Bảng xếp hạng đầu**: rank | title | company | score | recommendation | lương | location | **link nộp CV** (markdown link, mọi job đều có link bấm được).
|
|
68
|
-
- **Phân tích chi tiết TẤT CẢ job** (nếu >20 thì chi tiết 20 job cao nhất, còn lại vẫn đủ trong bảng). **KHÔNG rút gọn còn top 3.** Mỗi job:
|
|
69
|
-
1. Header: `#N. Title @ Company` — score, recommendation, **link apply**, lương, location/remote, hạn nộp, contact (nếu có).
|
|
70
|
-
2. **Vì sao hợp** (2–3 gạch đầu dòng).
|
|
71
|
-
3. **Khoảng cách** (missing_must_have + gaps, kèm severity; nói rõ "học nhanh được" vs "cần thời gian").
|
|
72
|
-
4. **Rủi ro / cần xác minh** (confidence thấp → đọc kỹ JD gốc; over/underqualified; lương unknown).
|
|
73
|
-
5. **Hành động đề xuất**.
|
|
74
|
-
- Cuối: nhận xét thị trường, gap lặp lại (skill nên học), có nên nới target không.
|
|
75
|
-
|
|
76
|
-
### [5] Application assistant (tùy chọn)
|
|
77
|
-
Khi user chọn job để apply: tailor CV bullets khớp `must_have_skills` (dùng canonical name cho ATS,
|
|
78
|
-
định lượng thành tích, **không bịa**); viết cover letter (~250–350 từ) hoặc tin nhắn tiếp cận HR mẫu.
|
|
79
|
-
Nêu rõ đây là bản nháp để user tự chỉnh. **Không tự gửi thay user.**
|
|
80
|
-
|
|
81
|
-
## Chuẩn hóa Việt–Anh (dùng xuyên suốt)
|
|
82
|
-
|
|
83
|
-
- **Skills → canonical EN**: `ReactJS`/`React.js` → `React`; `NodeJS` → `Node.js`; `K8s` → `Kubernetes`; `.NET`/`dotnet` → `.NET`. Giữ biến thể trong `aliases`.
|
|
84
|
-
- **Chức danh VN → EN**: "Lập trình viên"/"Kỹ sư phần mềm" → Software Engineer; "Trưởng nhóm" → Lead; "Kiểm thử"/"QC" → QA; "Khoa học dữ liệu" → Data Science.
|
|
85
|
-
- **Cấp bậc VN**: Fresher→junior, "Chuyên viên"→mid, "Senior"→senior, "Team Lead"→lead, "Trưởng phòng"→manager, "Giám đốc"→director.
|
|
86
|
-
- **Địa điểm**: "TP.HCM"/"Sài Gòn"/"HCMC" → Ho Chi Minh City; "HN"/"Hà Nội" → Hanoi; "ĐN"/"Đà Nẵng" → Da Nang.
|
|
87
|
-
- **Lương — giữ nguyên gốc**: "15 triệu"/"15tr"/"15M" → 15.000.000 VND; "$1500"/"1500 USD" → USD; "Up to $2000" → max 2000 USD; "Thỏa thuận"/"Negotiable" → không set số, `negotiable=true`, currency=unknown. **Chỉ quy đổi khi so sánh**, không ghi đè giá trị gốc.
|
|
88
|
-
|
|
89
|
-
## Cấu trúc JSON (rút gọn)
|
|
90
|
-
|
|
91
|
-
**profile**: `{candidate{name,headline,location,languages[]}, skills[{name,aliases[],years,level,evidence}], experience[{title,company,start,end,domain,highlights[]}], education[], total_years, seniority, domains[], target{desired_roles[],desired_level,locations[],remote_preference,salary{min,target,currency,period},industries[],company_size[],dealbreakers[],priorities{skills,seniority,domain,location,compensation,culture}}}`
|
|
92
|
-
|
|
93
|
-
**job**: `{id,title,company,location,remote,url,source,posted_date,application_deadline,employment_type,language,description,contact{email,phone,zalo,form_url,how_to_apply},requirements{must_have_skills[],nice_to_have_skills[],min_years,seniority,education},salary{min,max,currency,period,negotiable},industry,company_size,collected_at,extraction_confidence}`
|
|
94
|
-
|
|
95
|
-
**match**: `{job_id,overall_score,recommendation,dimension_scores{skills,seniority,domain,location,compensation,culture},weights_used{},matched_skills[],missing_must_have[],gaps[{area,detail,severity}],dealbreaker_violations[],rationale,confidence}`
|
|
96
|
-
|
|
97
|
-
## Nguyên tắc (bắt buộc)
|
|
98
|
-
|
|
99
|
-
- **Không bịa** trường job/CV. Thiếu → `unknown` + hạ `confidence` (không chấm 0 mù quáng).
|
|
100
|
-
- Tôn trọng robots.txt/ToS; **không vượt anti-bot/CAPTCHA**.
|
|
101
|
-
- Giữ **đơn vị lương gốc**; chỉ quy đổi khi so sánh.
|
|
102
|
-
- Trung thực: match yếu thì nói thẳng, không thổi phồng khả năng trúng tuyển.
|
|
103
|
-
- **Không tự nộp hồ sơ / gửi email / tin nhắn** thay người dùng.
|
package/chatgpt/README.md
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
# JobMatching cho ChatGPT
|
|
2
|
-
|
|
3
|
-
Cùng một logic pipeline + rubric chấm điểm như Claude Code / Codex, chạy bằng **web browsing**
|
|
4
|
-
sẵn có của ChatGPT — không cần cài plugin hay API key.
|
|
5
|
-
|
|
6
|
-
## Cách cài (Custom GPT — khuyên dùng)
|
|
7
|
-
|
|
8
|
-
1. [Tải repo về](https://github.com/hieutrong512/JobMatching/archive/refs/heads/main.zip) và giải nén.
|
|
9
|
-
2. Vào [chatgpt.com/gpts/editor](https://chatgpt.com/gpts/editor) → **Create** → tab **Configure**:
|
|
10
|
-
- **Name**: `JobMatching`
|
|
11
|
-
- **Description**: "Tìm & xếp hạng việc làm phù hợp CV, song ngữ Việt–Anh"
|
|
12
|
-
- **Instructions**: mở [`GPT-INSTRUCTIONS.md`](GPT-INSTRUCTIONS.md), copy toàn bộ nội dung dán vào
|
|
13
|
-
- **Knowledge**: kéo-thả **tất cả** file trong thư mục [`knowledge/`](knowledge/) vào
|
|
14
|
-
- **Capabilities**: bật **Web Search** (bắt buộc). Bật **Code Interpreter** nếu muốn tải báo cáo (tùy chọn)
|
|
15
|
-
3. **Save** → **Publish** ("Anyone with a link" để chia sẻ, hoặc "Only me" để dùng riêng)
|
|
16
|
-
4. Mở GPT, dán CV vào là chạy.
|
|
17
|
-
|
|
18
|
-
## Cách khác
|
|
19
|
-
|
|
20
|
-
### ChatGPT Project
|
|
21
|
-
|
|
22
|
-
1. Tạo **Project** mới → dán [`GPT-INSTRUCTIONS.md`](GPT-INSTRUCTIONS.md) vào Instructions.
|
|
23
|
-
2. Kéo các file trong [`knowledge/`](knowledge/) vào **Files** của Project.
|
|
24
|
-
3. Bắt đầu chat, dán CV.
|
|
25
|
-
|
|
26
|
-
### Chat thường (nhanh, 1 lần)
|
|
27
|
-
|
|
28
|
-
Dán prompt này rồi dán CV ngay sau:
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
Đọc kỹ và tuân theo bộ chỉ dẫn JobMatching dưới đây cho toàn bộ cuộc trò chuyện.
|
|
32
|
-
Dùng công cụ tìm kiếm web để thu thập job thật (không bịa). CV của tôi ở cuối tin nhắn.
|
|
33
|
-
|
|
34
|
-
<<< dán toàn bộ nội dung GPT-INSTRUCTIONS.md vào đây >>>
|
|
35
|
-
|
|
36
|
-
--- CV ---
|
|
37
|
-
<<< dán CV >>>
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Nội dung thư mục
|
|
41
|
-
|
|
42
|
-
```
|
|
43
|
-
chatgpt/
|
|
44
|
-
├── GPT-INSTRUCTIONS.md Orchestrator (~7.6k chars, vừa giới hạn 8k Instructions)
|
|
45
|
-
├── knowledge/ Bộ knowledge upload sẵn (skills + schemas flatten)
|
|
46
|
-
│ ├── skill-*.md Từng skill chi tiết
|
|
47
|
-
│ └── *.schema.json Data contracts
|
|
48
|
-
└── README.md File này
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## Build lại knowledge từ source (tùy chọn)
|
|
52
|
-
|
|
53
|
-
Thư mục `knowledge/` đã có sẵn trong repo. Nếu muốn build lại sau khi chỉnh skill:
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
bash build-chatgpt.sh # macOS/Linux
|
|
57
|
-
powershell -File build-chatgpt.ps1 # Windows
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
→ ra `dist/chatgpt-knowledge/` (copy đè vào `chatgpt/knowledge/` nếu muốn commit).
|
|
61
|
-
|
|
62
|
-
## Khác biệt so với Claude Code / Codex
|
|
63
|
-
|
|
64
|
-
| | Claude Code / Codex | ChatGPT |
|
|
65
|
-
|---|---|---|
|
|
66
|
-
| Cài đặt | `/plugin marketplace add` | Tạo Custom GPT, upload knowledge |
|
|
67
|
-
| Web search | `WebSearch` / `web_search` | Web Search (browsing) |
|
|
68
|
-
| Lưu trữ | Ghi `data/*.json` ra ổ đĩa | JSON trong chat (hoặc Code Interpreter tạm) |
|
|
69
|
-
| Báo cáo | File `.md` trong `data/results/` | Markdown trong chat (hoặc file tải về) |
|
|
70
|
-
|
|
71
|
-
Logic pipeline, trọng số chấm điểm và data contract **giống hệt** — đều bắt nguồn từ
|
|
72
|
-
`job-matching-plugin/skills/` + `schemas/` (nguồn chân lý duy nhất).
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "job.schema.json",
|
|
4
|
-
"title": "Job",
|
|
5
|
-
"description": "Tin tuyển dụng đã chuẩn hóa từ nguồn search/scrape.",
|
|
6
|
-
"type": "object",
|
|
7
|
-
"required": ["id", "title", "company", "url", "source"],
|
|
8
|
-
"properties": {
|
|
9
|
-
"schema_version": { "type": "string", "default": "1.0" },
|
|
10
|
-
"id": { "type": "string", "description": "Hash ổn định từ url hoặc company+title+location để khử trùng" },
|
|
11
|
-
"title": { "type": "string" },
|
|
12
|
-
"company": { "type": "string" },
|
|
13
|
-
"location": { "type": "string" },
|
|
14
|
-
"remote": { "type": "string", "enum": ["onsite", "hybrid", "remote", "unknown"], "default": "unknown" },
|
|
15
|
-
"url": { "type": "string", "description": "Link trang tuyển dụng / link JD gốc" },
|
|
16
|
-
"source": { "type": "string", "description": "vd 'itviec', 'topcv', 'vietnamworks', 'linkedin', 'careerviet', 'google'" },
|
|
17
|
-
"posted_date": { "type": "string", "description": "YYYY-MM-DD nếu có" },
|
|
18
|
-
"application_deadline": { "type": "string", "description": "YYYY-MM-DD hạn nộp nếu JD có ghi; job chỉ được giữ khi hạn >= ngày hiện tại" },
|
|
19
|
-
"employment_type": { "type": "string", "enum": ["full-time", "part-time", "contract", "internship", "unknown"], "default": "unknown" },
|
|
20
|
-
"language": { "type": "string", "enum": ["vi", "en", "mixed"], "default": "mixed" },
|
|
21
|
-
"description": { "type": "string", "description": "JD gốc (rút gọn nếu quá dài)" },
|
|
22
|
-
"contact": {
|
|
23
|
-
"type": "object",
|
|
24
|
-
"description": "Thông tin liên hệ ứng tuyển trực tiếp nếu JD có",
|
|
25
|
-
"properties": {
|
|
26
|
-
"email": { "type": "string" },
|
|
27
|
-
"phone": { "type": "string" },
|
|
28
|
-
"zalo": { "type": "string" },
|
|
29
|
-
"form_url": { "type": "string", "description": "Google Form / Typeform link nếu có" },
|
|
30
|
-
"how_to_apply": { "type": "string", "description": "Hướng dẫn ứng tuyển ngắn: vd 'Gửi CV qua email', 'Apply qua link'" }
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
"requirements": {
|
|
34
|
-
"type": "object",
|
|
35
|
-
"properties": {
|
|
36
|
-
"must_have_skills": { "type": "array", "items": { "type": "string" }, "description": "Canonical names" },
|
|
37
|
-
"nice_to_have_skills": { "type": "array", "items": { "type": "string" } },
|
|
38
|
-
"min_years": { "type": "number" },
|
|
39
|
-
"seniority": { "type": "string", "enum": ["intern", "junior", "mid", "senior", "lead", "manager", "director", "unknown"], "default": "unknown" },
|
|
40
|
-
"education": { "type": "string" }
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
"salary": {
|
|
44
|
-
"type": "object",
|
|
45
|
-
"properties": {
|
|
46
|
-
"min": { "type": "number" },
|
|
47
|
-
"max": { "type": "number" },
|
|
48
|
-
"currency": { "type": "string", "enum": ["VND", "USD", "unknown"], "default": "unknown" },
|
|
49
|
-
"period": { "type": "string", "enum": ["month", "year", "unknown"], "default": "unknown" },
|
|
50
|
-
"negotiable": { "type": "boolean" }
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
"industry": { "type": "string" },
|
|
54
|
-
"company_size": { "type": "string", "enum": ["startup", "sme", "enterprise", "unknown"], "default": "unknown" },
|
|
55
|
-
"collected_at": { "type": "string", "description": "ISO timestamp lúc thu thập" },
|
|
56
|
-
"extraction_confidence": { "type": "number", "description": "0-1, độ tin cậy khi parse JD (0.8-1.0 khi đọc được full JD)" }
|
|
57
|
-
}
|
|
58
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "match.schema.json",
|
|
4
|
-
"title": "MatchResult",
|
|
5
|
-
"description": "Kết quả chấm điểm 1 job so với profile. Xem scoring-rubric.",
|
|
6
|
-
"type": "object",
|
|
7
|
-
"required": ["job_id", "overall_score", "dimension_scores"],
|
|
8
|
-
"properties": {
|
|
9
|
-
"schema_version": { "type": "string", "default": "1.0" },
|
|
10
|
-
"job_id": { "type": "string" },
|
|
11
|
-
"overall_score": { "type": "number", "description": "0-100, tổng hợp có trọng số" },
|
|
12
|
-
"recommendation": { "type": "string", "enum": ["strong", "good", "maybe", "weak", "excluded"] },
|
|
13
|
-
"dimension_scores": {
|
|
14
|
-
"type": "object",
|
|
15
|
-
"description": "Mỗi chiều 0-100.",
|
|
16
|
-
"properties": {
|
|
17
|
-
"skills": { "type": "number" },
|
|
18
|
-
"seniority": { "type": "number" },
|
|
19
|
-
"domain": { "type": "number" },
|
|
20
|
-
"location": { "type": "number" },
|
|
21
|
-
"compensation": { "type": "number" },
|
|
22
|
-
"culture": { "type": "number" }
|
|
23
|
-
}
|
|
24
|
-
},
|
|
25
|
-
"weights_used": {
|
|
26
|
-
"type": "object",
|
|
27
|
-
"description": "Trọng số thực tế đã áp (sau khi override bằng target.priorities).",
|
|
28
|
-
"additionalProperties": { "type": "number" }
|
|
29
|
-
},
|
|
30
|
-
"matched_skills": { "type": "array", "items": { "type": "string" } },
|
|
31
|
-
"missing_must_have": { "type": "array", "items": { "type": "string" } },
|
|
32
|
-
"gaps": {
|
|
33
|
-
"type": "array",
|
|
34
|
-
"description": "Khoảng cách cần lấp (kỹ năng/kinh nghiệm) + mức nghiêm trọng.",
|
|
35
|
-
"items": {
|
|
36
|
-
"type": "object",
|
|
37
|
-
"properties": {
|
|
38
|
-
"area": { "type": "string" },
|
|
39
|
-
"detail": { "type": "string" },
|
|
40
|
-
"severity": { "type": "string", "enum": ["low", "medium", "high"] }
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
"dealbreaker_violations": { "type": "array", "items": { "type": "string" } },
|
|
45
|
-
"rationale": { "type": "string", "description": "Giải thích ngắn gọn vì sao điểm này (song ngữ nếu cần)" },
|
|
46
|
-
"confidence": { "type": "number", "description": "0-1, phụ thuộc extraction_confidence của job" }
|
|
47
|
-
}
|
|
48
|
-
}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "profile.schema.json",
|
|
4
|
-
"title": "CandidateProfile",
|
|
5
|
-
"description": "Hồ sơ ứng viên chuẩn hóa từ CV + target. Song ngữ Việt-Anh.",
|
|
6
|
-
"type": "object",
|
|
7
|
-
"required": ["candidate", "skills", "experience", "seniority", "target"],
|
|
8
|
-
"properties": {
|
|
9
|
-
"schema_version": { "type": "string", "default": "1.0" },
|
|
10
|
-
"candidate": {
|
|
11
|
-
"type": "object",
|
|
12
|
-
"required": ["name"],
|
|
13
|
-
"properties": {
|
|
14
|
-
"name": { "type": "string" },
|
|
15
|
-
"headline": { "type": "string", "description": "Chức danh hiện tại / tự mô tả ngắn" },
|
|
16
|
-
"location": { "type": "string" },
|
|
17
|
-
"summary": { "type": "string" },
|
|
18
|
-
"languages": {
|
|
19
|
-
"type": "array",
|
|
20
|
-
"items": {
|
|
21
|
-
"type": "object",
|
|
22
|
-
"properties": {
|
|
23
|
-
"language": { "type": "string" },
|
|
24
|
-
"level": { "type": "string", "enum": ["basic", "conversational", "professional", "native"] }
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
"skills": {
|
|
31
|
-
"type": "array",
|
|
32
|
-
"description": "Kỹ năng đã chuẩn hóa (dùng canonical EN name qua bilingual-normalization).",
|
|
33
|
-
"items": {
|
|
34
|
-
"type": "object",
|
|
35
|
-
"required": ["name"],
|
|
36
|
-
"properties": {
|
|
37
|
-
"name": { "type": "string", "description": "Canonical name, ưu tiên EN, vd 'React'" },
|
|
38
|
-
"aliases": { "type": "array", "items": { "type": "string" } },
|
|
39
|
-
"category": { "type": "string", "description": "vd 'frontend', 'backend', 'data', 'soft-skill'" },
|
|
40
|
-
"years": { "type": "number" },
|
|
41
|
-
"level": { "type": "string", "enum": ["beginner", "intermediate", "advanced", "expert"] },
|
|
42
|
-
"evidence": { "type": "string", "description": "Trích từ CV làm bằng chứng" }
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
"experience": {
|
|
47
|
-
"type": "array",
|
|
48
|
-
"items": {
|
|
49
|
-
"type": "object",
|
|
50
|
-
"required": ["title"],
|
|
51
|
-
"properties": {
|
|
52
|
-
"title": { "type": "string" },
|
|
53
|
-
"company": { "type": "string" },
|
|
54
|
-
"start": { "type": "string", "description": "YYYY-MM" },
|
|
55
|
-
"end": { "type": "string", "description": "YYYY-MM hoặc 'present'" },
|
|
56
|
-
"duration_months": { "type": "integer" },
|
|
57
|
-
"domain": { "type": "string", "description": "vd 'fintech', 'e-commerce'" },
|
|
58
|
-
"highlights": { "type": "array", "items": { "type": "string" } }
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
"education": {
|
|
63
|
-
"type": "array",
|
|
64
|
-
"items": {
|
|
65
|
-
"type": "object",
|
|
66
|
-
"properties": {
|
|
67
|
-
"degree": { "type": "string" },
|
|
68
|
-
"field": { "type": "string" },
|
|
69
|
-
"school": { "type": "string" },
|
|
70
|
-
"year": { "type": "string" }
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
"total_years": { "type": "number", "description": "Tổng số năm kinh nghiệm liên quan" },
|
|
75
|
-
"seniority": {
|
|
76
|
-
"type": "string",
|
|
77
|
-
"enum": ["intern", "junior", "mid", "senior", "lead", "manager", "director"],
|
|
78
|
-
"description": "Suy ra từ total_years + phạm vi trách nhiệm"
|
|
79
|
-
},
|
|
80
|
-
"domains": { "type": "array", "items": { "type": "string" }, "description": "Ngành/lĩnh vực đã làm" },
|
|
81
|
-
"target": {
|
|
82
|
-
"type": "object",
|
|
83
|
-
"description": "Mong muốn của ứng viên. priorities dùng để override trọng số scoring.",
|
|
84
|
-
"required": ["desired_roles"],
|
|
85
|
-
"properties": {
|
|
86
|
-
"desired_roles": { "type": "array", "items": { "type": "string" } },
|
|
87
|
-
"desired_level": { "type": "string", "enum": ["intern", "junior", "mid", "senior", "lead", "manager", "director"] },
|
|
88
|
-
"locations": { "type": "array", "items": { "type": "string" } },
|
|
89
|
-
"remote_preference": { "type": "string", "enum": ["onsite", "hybrid", "remote", "any"], "default": "any" },
|
|
90
|
-
"salary": {
|
|
91
|
-
"type": "object",
|
|
92
|
-
"properties": {
|
|
93
|
-
"min": { "type": "number" },
|
|
94
|
-
"target": { "type": "number" },
|
|
95
|
-
"currency": { "type": "string", "enum": ["VND", "USD"], "default": "VND" },
|
|
96
|
-
"period": { "type": "string", "enum": ["month", "year"], "default": "month" }
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
"industries": { "type": "array", "items": { "type": "string" } },
|
|
100
|
-
"company_size": { "type": "array", "items": { "type": "string", "enum": ["startup", "sme", "enterprise", "any"] } },
|
|
101
|
-
"dealbreakers": {
|
|
102
|
-
"type": "array",
|
|
103
|
-
"description": "Điều kiện loại trừ cứng (hard filter). Vd 'no onsite ngoài HCM', 'không outsource'.",
|
|
104
|
-
"items": { "type": "string" }
|
|
105
|
-
},
|
|
106
|
-
"priorities": {
|
|
107
|
-
"type": "object",
|
|
108
|
-
"description": "Trọng số 0-1 ứng viên khai; override default trong scoring-rubric. Tổng nên ~1.",
|
|
109
|
-
"properties": {
|
|
110
|
-
"skills": { "type": "number" },
|
|
111
|
-
"seniority": { "type": "number" },
|
|
112
|
-
"domain": { "type": "number" },
|
|
113
|
-
"location": { "type": "number" },
|
|
114
|
-
"compensation": { "type": "number" },
|
|
115
|
-
"culture": { "type": "number" }
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: application-assistant
|
|
3
|
-
description: Tùy chỉnh (tailor) nội dung CV và viết cover letter cho một job cụ thể dựa trên profile và JD. Dùng khi ứng viên đã chọn được job muốn apply và cần chuẩn bị hồ sơ.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Application Assistant — tailor hồ sơ theo job
|
|
7
|
-
|
|
8
|
-
Đầu vào: `profile.json` + 1 job (từ jobs.json hoặc URL/JD dán vào) + (tùy chọn) MatchResult để biết gaps.
|
|
9
|
-
Đầu ra: bản nháp CV bullets đã điều chỉnh và/hoặc cover letter.
|
|
10
|
-
|
|
11
|
-
## Tailor CV bullets
|
|
12
|
-
|
|
13
|
-
- Chọn và viết lại các highlight kinh nghiệm sao cho khớp `must_have_skills` và trách nhiệm chính của JD.
|
|
14
|
-
- Dùng canonical skill name khớp với từ khóa JD (giúp qua ATS).
|
|
15
|
-
- Định lượng thành tích (số liệu) khi CV có dữ liệu; **không bịa** số hay kinh nghiệm không có.
|
|
16
|
-
- Nêu rõ chỗ nào là gap (missing must-have) và cách trình bày trung thực (vd "đang học", kinh nghiệm chuyển giao).
|
|
17
|
-
|
|
18
|
-
## Cover letter & Email ứng tuyển
|
|
19
|
-
|
|
20
|
-
- Áp skill `cover-letter-writing` (nếu có) cho style. Cấu trúc: mở đầu (vì sao công ty/role này), thân (2–3 bằng chứng khớp yêu cầu), kết (call to action).
|
|
21
|
-
- Giọng văn phù hợp ngôn ngữ JD (vi/en/mixed). Ngắn gọn (~250–350 từ).
|
|
22
|
-
- Cá nhân hóa theo company (tránh mẫu chung chung).
|
|
23
|
-
|
|
24
|
-
## Tin nhắn tiếp cận trực tiếp HR (Email / LinkedIn / Zalo)
|
|
25
|
-
|
|
26
|
-
Khi JD có kênh liên hệ trực tiếp và ứng viên muốn chủ động tiếp cận HR:
|
|
27
|
-
|
|
28
|
-
**Mẫu Direct Message / Email ngắn**:
|
|
29
|
-
> "Chào anh/chị [Tên HR/Recruiter], em thấy tin tuyển [Tên vị trí] của [Công ty]. Em có [X năm] kinh nghiệm về [Top 2-3 kỹ năng khớp], từng làm [Dự án/Sản phẩm tiêu biểu]. Em rất quan tâm vị trí này. Em xin gửi kèm CV: [Link CV] để anh/chị tiện xem qua ạ. Cảm ơn anh/chị!"
|
|
30
|
-
|
|
31
|
-
Điều chỉnh giọng văn theo kênh (email trang trọng hơn, LinkedIn/Zalo ngắn gọn hơn).
|
|
32
|
-
|
|
33
|
-
## Ranh giới quan trọng
|
|
34
|
-
|
|
35
|
-
- **Không bịa** bằng cấp, kinh nghiệm, con số. Chỉ diễn đạt lại sự thật trong CV cho hấp dẫn hơn.
|
|
36
|
-
- Đây là bản nháp để ứng viên xem lại và tự chỉnh; nêu rõ điều đó.
|
|
37
|
-
- Không tự gửi email/nộp hồ sơ thay ứng viên.
|
|
38
|
-
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: bilingual-normalization
|
|
3
|
-
description: Chuẩn hóa kỹ năng, chức danh, địa điểm và lương giữa tiếng Việt và tiếng Anh về dạng canonical, phục vụ so khớp job/CV chính xác. Dùng khi parse CV, chuẩn hóa JD, hoặc so khớp skills/lương trong scoring.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Bilingual Normalization — chuẩn hóa Việt–Anh
|
|
7
|
-
|
|
8
|
-
Mục tiêu: mọi so khớp diễn ra trên **canonical name** thống nhất, tránh trượt vì khác ngôn ngữ/cách viết.
|
|
9
|
-
|
|
10
|
-
## Skills — canonical name
|
|
11
|
-
|
|
12
|
-
- Canonical ưu tiên tên tiếng Anh phổ biến của công nghệ: `React`, `Node.js`, `Java`, `Python`, `Kubernetes`...
|
|
13
|
-
- Gộp biến thể: `ReactJS`/`React.js`/`Reactjs` → `React`; `NodeJS` → `Node.js`; `K8s` → `Kubernetes`; `.NET`/`dotnet` → `.NET`.
|
|
14
|
-
- Chức năng mô tả bằng tiếng Việt → canonical EN:
|
|
15
|
-
- "Lập trình viên" / "Kỹ sư phần mềm" → Software Engineer
|
|
16
|
-
- "Trưởng nhóm" → Lead ; "Quản lý dự án" → Project Manager
|
|
17
|
-
- "Kiểm thử" / "QC" → QA ; "Phân tích nghiệp vụ" → Business Analyst
|
|
18
|
-
- "Khoa học dữ liệu" → Data Science ; "Kỹ sư dữ liệu" → Data Engineer
|
|
19
|
-
- Giữ `aliases[]` trong profile/job để truy vết.
|
|
20
|
-
|
|
21
|
-
## Chức danh & cấp bậc (seniority)
|
|
22
|
-
|
|
23
|
-
- Map cấp bậc VN → thang chuẩn: Fresher→junior, "Chuyên viên"→mid, "Senior/Kỹ sư cao cấp"→senior, "Trưởng nhóm/Team Lead"→lead, "Trưởng phòng"→manager, "Giám đốc"→director.
|
|
24
|
-
- Cẩn thận với "Senior" gắn năm kinh nghiệm khác nhau giữa công ty — ưu tiên `min_years` nếu có.
|
|
25
|
-
|
|
26
|
-
## Địa điểm
|
|
27
|
-
|
|
28
|
-
- Chuẩn hóa: "TP.HCM"/"Sài Gòn"/"HCMC"/"Ho Chi Minh" → "Ho Chi Minh City"; "HN"/"Hà Nội" → "Hanoi"; "ĐN"/"Đà Nẵng" → "Da Nang".
|
|
29
|
-
- Nhận diện remote: "làm việc từ xa"/"remote" → remote; "linh hoạt"/"hybrid" → hybrid.
|
|
30
|
-
|
|
31
|
-
## Lương — giữ nguyên gốc, chỉ quy đổi khi cần so sánh
|
|
32
|
-
|
|
33
|
-
- **Luôn giữ nguyên đơn vị & giá trị gốc** trong `job.salary` (currency đúng như JD). Ví dụ JD ghi "$1000" → lưu USD 1000; ghi "26 triệu" → lưu VND 26,000,000. Không tự đổi khi lưu trữ hay hiển thị.
|
|
34
|
-
- Đơn vị Việt:
|
|
35
|
-
- "15 triệu"/"15tr"/"15M" → 15,000,000 VND.
|
|
36
|
-
- "15-20 triệu"/"15-20M" → min 15tr, max 20tr.
|
|
37
|
-
- "Gross" / "Net" → parse giá trị số bình thường (ghi chú trong description nếu cần).
|
|
38
|
-
- "Thỏa thuận"/"Cạnh tranh"/"Negotiable"/"Deal" → không set số, `negotiable=true`, currency=unknown.
|
|
39
|
-
- USD: "$1500"/"1500 USD" → USD. "Up to $2000"/"Upto $2k" → max=2000, currency=USD.
|
|
40
|
-
- **Chỉ khi cần so sánh** (chấm điểm compensation, so hai job khác đơn vị): quy đổi tạm về VND/tháng bằng tỉ giá cố định **$1 = 26,100 VND**; year→month chia 12. Giá trị quy đổi này chỉ dùng nội bộ lúc tính toán, KHÔNG ghi đè giá trị gốc.
|
|
41
|
-
|
|
42
|
-
## Nguyên tắc
|
|
43
|
-
|
|
44
|
-
- Chỉ map khi chắc chắn; nếu không rõ, giữ nguyên và ghi vào aliases để không mất thông tin.
|
|
45
|
-
- Không "dịch" tên riêng công ty.
|
|
46
|
-
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: candidate-intake
|
|
3
|
-
description: Parse CV của ứng viên (PDF/DOCX/text) và thu thập target/mong muốn để tạo profile.json chuẩn. Dùng khi bắt đầu một phiên tìm việc, khi người dùng cung cấp CV mới, hoặc muốn cập nhật mục tiêu nghề nghiệp.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Candidate Intake — tạo profile ứng viên
|
|
7
|
-
|
|
8
|
-
Đầu ra: một file `data/profiles/<slug>.json` tuân theo `./schemas/profile.schema.json`.
|
|
9
|
-
|
|
10
|
-
## Bước 1 — Đọc CV
|
|
11
|
-
|
|
12
|
-
- Trích text từ CV:
|
|
13
|
-
- **PDF** → Claude Code: dùng skill `pdf` (anthropic-skills). Codex/CLI: `pdftotext cv.pdf -` (poppler) hoặc `python -c "import pypdf; ..."`. Không có tool → yêu cầu người dùng dán nội dung CV.
|
|
14
|
-
- **DOCX** → Claude Code: skill `docx`. Codex/CLI: `python -c "import docx; ..."` (python-docx) hoặc unzip đọc `word/document.xml`.
|
|
15
|
-
- **Text/Markdown** → đọc trực tiếp.
|
|
16
|
-
- Trích: thông tin cá nhân tối thiểu (tên, headline, location), kỹ năng, kinh nghiệm (title/công ty/thời gian/highlights), học vấn, ngôn ngữ.
|
|
17
|
-
- Suy `total_years` và `seniority` từ tổng thời gian + phạm vi trách nhiệm.
|
|
18
|
-
|
|
19
|
-
## Bước 2 — Chuẩn hóa (song ngữ)
|
|
20
|
-
|
|
21
|
-
- Áp skill `bilingual-normalization`: đưa skills & chức danh về canonical name, chuẩn hóa location.
|
|
22
|
-
- Với mỗi skill, cố gắng gắn `years`/`level`/`evidence` từ CV (không bịa — thiếu thì để trống).
|
|
23
|
-
|
|
24
|
-
## Bước 3 — Thu thập target
|
|
25
|
-
|
|
26
|
-
Hỏi ứng viên (gộp câu hỏi, đừng hỏi lẻ tẻ). Nếu người dùng đã nêu sẵn trong hội thoại thì dùng luôn, chỉ hỏi phần còn thiếu:
|
|
27
|
-
|
|
28
|
-
1. **Vai trò mong muốn** (desired_roles) + cấp bậc (desired_level).
|
|
29
|
-
2. **Địa điểm** + hình thức (onsite/hybrid/remote/any).
|
|
30
|
-
3. **Lương kỳ vọng** (min & target, VND hay USD, tháng/năm).
|
|
31
|
-
4. **Ngành** ưu tiên + **quy mô công ty**.
|
|
32
|
-
5. **Dealbreakers** — điều tuyệt đối không chấp nhận.
|
|
33
|
-
6. **Ưu tiên** (priorities): cái gì quan trọng nhất? (lương / kỹ năng phù hợp / địa điểm / ngành / văn hóa). Chuyển thành trọng số 0–1 cho `target.priorities` (tổng ~1) — đây là dữ liệu quan trọng cho `scoring-rubric`.
|
|
34
|
-
|
|
35
|
-
## Bước 4 — Ghi file & Xác nhận
|
|
36
|
-
- Ghi `profile.json` đúng schema. Đặt tên slug từ tên ứng viên (không dấu, kebab-case).
|
|
37
|
-
- Tóm tắt lại cho người dùng xác nhận: seniority suy ra, top skills, target, và trọng số priorities. Cho phép chỉnh trước khi sang bước collector.
|
|
38
|
-
|
|
39
|
-
## Nguyên tắc
|
|
40
|
-
|
|
41
|
-
- Không bịa kỹ năng/kinh nghiệm không có trong CV.
|
|
42
|
-
- Nếu CV thiếu năm tháng cụ thể, ước lượng và ghi rõ là ước lượng.
|
|
43
|
-
- Tôn trọng riêng tư: chỉ lưu thông tin cần cho matching; không đưa dữ liệu cá nhân ra ngoài.
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: find-jobs
|
|
3
|
-
description: Chạy full pipeline tìm việc song ngữ Việt–Anh — parse CV, thu thập job (web search), chấm điểm, xuất báo cáo fit/gap. Dùng khi người dùng muốn "tìm job cho tôi" từ một CV hoặc profile đã có.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# find-jobs — điều phối full pipeline tìm việc
|
|
7
|
-
|
|
8
|
-
Skill điều phối, chạy tuần tự 4 bước. Đối số là đường dẫn CV (PDF/DOCX/text) hoặc tên
|
|
9
|
-
profile đã có trong `data/profiles/`.
|
|
10
|
-
|
|
11
|
-
Dùng `<run-id>` duy nhất theo mẫu `<candidate-slug>-<YYYYMMDD-HHMMSS>-<suffix>` và giữ nguyên
|
|
12
|
-
ID này xuyên suốt pipeline. Tạo `data/profiles`, `data/jobs`, `data/results` trong thư mục làm việc
|
|
13
|
-
nếu chưa có.
|
|
14
|
-
|
|
15
|
-
> **Yêu cầu (bước 2 — thu thập job):**
|
|
16
|
-
> - **Claude Code**: `WebSearch`/`WebFetch` có sẵn — không cần cấu hình.
|
|
17
|
-
> - **Codex**: cần bật `web_search` bằng `codex --search` hoặc `tools.web_search = true` trong `~/.codex/config.toml`. Nếu chưa bật, dừng ở bước 2 và báo người dùng.
|
|
18
|
-
|
|
19
|
-
## Bước 1 — Intake → profile.json
|
|
20
|
-
Áp skill **`candidate-intake`**.
|
|
21
|
-
- Nếu đối số là file CV → parse và tạo `data/profiles/<slug>.json` theo `schemas/profile.schema.json`.
|
|
22
|
-
- Nếu là tên profile đã tồn tại → dùng lại, hỏi có cập nhật target không.
|
|
23
|
-
- Thu thập/xác nhận target: vai trò, cấp bậc, địa điểm/remote, lương, priorities (→ trọng số), dealbreakers.
|
|
24
|
-
- Tóm tắt cho người dùng xác nhận.
|
|
25
|
-
|
|
26
|
-
## Bước 2 — Collect → data/jobs/<run-id>.json
|
|
27
|
-
Spawn một subagent context riêng và yêu cầu nó áp skill **`job-collector`**; không phụ thuộc custom
|
|
28
|
-
agent cài ngoài plugin.
|
|
29
|
-
- Truyền đường dẫn `profile.json`, `<run-id>` và output `data/jobs/<run-id>.json`.
|
|
30
|
-
- **Nguồn Job Boards**: Quét từ ITviec, TopCV, VietnamWorks, LinkedIn...
|
|
31
|
-
- Giữ job thỏa mãn tiêu chuẩn: chỉ giữ job xem được full JD & còn hạn. Tối đa 20 job hợp lệ.
|
|
32
|
-
|
|
33
|
-
## Bước 3 — Match → data/results/<run-id>.shortlist.json
|
|
34
|
-
Spawn một subagent context riêng và yêu cầu nó áp skill **`job-matcher`** (chấm điểm bulk).
|
|
35
|
-
- Truyền `profile.json` + `data/jobs/<run-id>.json`.
|
|
36
|
-
- Chấm điểm theo skill **`scoring-rubric`**, dùng trọng số từ `target.priorities`.
|
|
37
|
-
|
|
38
|
-
## Bước 4 — Fit report → data/results/<run-id>.fit_report.md
|
|
39
|
-
Áp skill **`fit-analyzer`**.
|
|
40
|
-
- Bảng xếp hạng: **mọi job đều có link nộp CV (url) bấm được**; phân tích chi tiết top 20 (hoặc toàn bộ).
|
|
41
|
-
- Báo đường dẫn file báo cáo cho người dùng.
|
|
42
|
-
|
|
43
|
-
## Sau khi xong
|
|
44
|
-
Đề xuất bước tiếp: tailor hồ sơ cho job top (skill `application-assistant`), hoặc mở rộng
|
|
45
|
-
nguồn (remote quốc tế / thành phố khác).
|
|
46
|
-
|
|
47
|
-
## Nguyên tắc
|
|
48
|
-
- Không bịa dữ liệu job/CV. Thiếu → unknown, hạ confidence.
|
|
49
|
-
- Tôn trọng robots/ToS khi scrape; không vượt anti-bot/CAPTCHA.
|
|
50
|
-
- Không tự nộp hồ sơ thay người dùng.
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: fit-analyzer
|
|
3
|
-
description: Với các job trong shortlist, giải thích mức độ phù hợp, khoảng cách kỹ năng/kinh nghiệm, rủi ro, và gợi ý cải thiện — dạng báo cáo dễ đọc cho ứng viên. Dùng sau khi job-matcher đã tạo shortlist.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Fit Analyzer — báo cáo phù hợp & khoảng cách
|
|
7
|
-
|
|
8
|
-
Đầu vào: `profile.json` + `data/results/<run-id>.shortlist.json` (mảng MatchResult + job tương ứng).
|
|
9
|
-
Đầu ra: `data/results/<run-id>.fit_report.md`.
|
|
10
|
-
|
|
11
|
-
## Phạm vi báo cáo (QUAN TRỌNG)
|
|
12
|
-
|
|
13
|
-
Phân tích chi tiết **TẤT CẢ job trong shortlist**, không rút gọn còn top 3. Nếu shortlist có nhiều hơn **20** job thì phân tích chi tiết 20 job điểm cao nhất và vẫn liệt kê đủ phần còn lại trong bảng xếp hạng. Có thể thêm mục "🏆 Ưu tiên apply ngay" (2–3 job) ở đầu như phần tóm tắt, nhưng **phần thân báo cáo vẫn phải có block chi tiết cho từng job** (không bỏ job nào).
|
|
14
|
-
|
|
15
|
-
## Nội dung báo cáo
|
|
16
|
-
|
|
17
|
-
Với **mỗi** job (theo thứ tự điểm giảm dần), trình bày:
|
|
18
|
-
|
|
19
|
-
1. **Header**: `#N. Title @ Company` — overall_score, recommendation, **link nộp CV (url)**, lương, location/remote, hạn nộp (nếu có).
|
|
20
|
-
- Nếu JD có kênh liên hệ trực tiếp (email, phone, Zalo, form): hiển thị rõ.
|
|
21
|
-
2. **Vì sao hợp** (2–3 gạch đầu dòng): điểm mạnh khớp — matched_skills chính, domain, seniority, lương.
|
|
22
|
-
3. **Khoảng cách** (gaps): missing_must_have + gaps[] kèm severity. Nói rõ cái nào là "học nhanh được" vs "cần thời gian".
|
|
23
|
-
4. **Rủi ro / cần xác minh**:
|
|
24
|
-
- Nếu `confidence` thấp → khuyến nghị đọc kỹ JD gốc; overqualified/underqualified; lương unknown.
|
|
25
|
-
5. **Hành động đề xuất**: Apply qua link / Chuẩn bị hồ sơ. Nếu muốn chủ động liên hệ HR (email/LinkedIn/Zalo), dùng skill `application-assistant` để lấy tin nhắn mẫu.
|
|
26
|
-
|
|
27
|
-
## Tổng quan đầu báo cáo
|
|
28
|
-
|
|
29
|
-
- Bảng xếp hạng: rank | title | company | score | recommendation | lương | location | **link nộp CV**. **MỌI job trong bảng đều phải có link (url) bấm được** — không chỉ các job top. Dùng markdown link (vd `[Nộp CV](url)`).
|
|
30
|
-
- Nhận xét chung: xu hướng thị trường quan sát được, gap lặp lại nhiều lần (gợi ý kỹ năng nên học), có nên nới target không (vd lương kỳ vọng quá cao so với mặt bằng).
|
|
31
|
-
|
|
32
|
-
## Nguyên tắc
|
|
33
|
-
|
|
34
|
-
- Trung thực: nếu match yếu, nói thẳng; đừng thổi phồng.
|
|
35
|
-
- Ưu tiên hành động cụ thể hơn nhận xét chung chung.
|
|
36
|
-
- Song ngữ khi cần (thuật ngữ giữ tiếng Anh, giải thích tiếng Việt).
|
|
37
|
-
- Không tạo kỳ vọng sai về khả năng trúng tuyển.
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: job-collector
|
|
3
|
-
description: Thu thập và chuẩn hóa việc làm từ web search. Dùng trong bước collect của pipeline find-jobs sau khi đã có profile ứng viên.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Job Collector
|
|
7
|
-
|
|
8
|
-
Nhận đường dẫn `profile.json`, `run_id` duy nhất và đường dẫn output. Thu thập tối đa 20 job hợp lệ, chuẩn hóa theo `job-schema`, rồi ghi vào `data/jobs/<run_id>.json`.
|
|
9
|
-
|
|
10
|
-
## Nguồn dữ liệu
|
|
11
|
-
|
|
12
|
-
- Job boards: dùng web search/fetch sẵn có của host. Chỉ giữ job đọc được JD và chưa có bằng chứng hết hạn.
|
|
13
|
-
|
|
14
|
-
## Contract
|
|
15
|
-
|
|
16
|
-
1. Đọc profile và sinh query song ngữ từ role, seniority, location và skills.
|
|
17
|
-
2. Khử trùng theo URL chuẩn hóa; ưu tiên nguồn gốc và dữ liệu mới hơn.
|
|
18
|
-
3. Ghi object theo `schemas/job.schema.json`, đặt `extraction_confidence` trung thực.
|
|
19
|
-
4. Trả số lượng thu được, số bị loại và cảnh báo dữ liệu thiếu.
|
|
20
|
-
|
|
21
|
-
Không tự nộp hồ sơ, không vượt CAPTCHA/anti-bot và không ghi đè output của run khác.
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: job-matcher
|
|
3
|
-
description: Chấm điểm và xếp hạng danh sách job so với profile ứng viên. Dùng trong bước match của pipeline find-jobs sau khi collector đã tạo jobs JSON.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Job Matcher
|
|
7
|
-
|
|
8
|
-
Nhận `profile.json`, `data/jobs/<run_id>.json` và output `data/results/<run_id>.shortlist.json`.
|
|
9
|
-
|
|
10
|
-
1. Khử trùng job theo `id`, giữ bản có `extraction_confidence` cao hơn.
|
|
11
|
-
2. Áp `scoring-rubric` và `bilingual-normalization` cho từng job.
|
|
12
|
-
3. Chuẩn hóa mọi bộ trọng số, kể cả mặc định, để tổng bằng 1 trước khi tính điểm.
|
|
13
|
-
4. Hard-filter dealbreakers chỉ khi có đủ bằng chứng; thiếu dữ liệu thì hạ confidence.
|
|
14
|
-
5. Ghi mảng `{match, job_ref}` theo `schemas/match.schema.json`, sắp xếp giảm dần và để excluded cuối.
|
|
15
|
-
|
|
16
|
-
Luôn ghi `weights_used`, rationale và các gap; không thổi phồng recommendation.
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: job-schema
|
|
3
|
-
description: Schema chuẩn cho một tin tuyển dụng (job) và cách map dữ liệu thô từ search/scrape vào schema đó. Dùng bởi job-collector khi chuẩn hóa JD, và job-matcher khi đọc job.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Job Schema — chuẩn hóa tin tuyển dụng
|
|
7
|
-
|
|
8
|
-
Schema chính: `./schemas/job.schema.json`. Skill này hướng dẫn cách điền cho nhất quán.
|
|
9
|
-
|
|
10
|
-
## Quy tắc bắt buộc
|
|
11
|
-
|
|
12
|
-
- `id`: tạo hash ổn định. Ưu tiên chuẩn hóa URL (bỏ query tracking) rồi hash; nếu không có URL sạch, hash chuỗi `company|title|location` (lowercase, bỏ dấu). Dùng để **khử trùng lặp** giữa các nguồn.
|
|
13
|
-
- `source`: định danh nguồn ngắn gọn: `itviec`, `topcv`, `vietnamworks`, `linkedin`, `careerviet`, `google`, ...
|
|
14
|
-
- `collected_at`: ISO timestamp.
|
|
15
|
-
- `extraction_confidence` (0–1):
|
|
16
|
-
- **Cao** (0.8–1.0): đọc được full JD từ trang tuyển dụng chính thống hoặc link JD mở.
|
|
17
|
-
- **Thấp** (≤0.4): chỉ có snippet rời rạc từ kết quả tìm kiếm không đủ thông tin.
|
|
18
|
-
|
|
19
|
-
## Chuẩn hóa các trường
|
|
20
|
-
|
|
21
|
-
- **skills** (`must_have_skills`, `nice_to_have_skills`): đưa về canonical name qua skill `bilingual-normalization` (vd "ReactJS", "React.js" → "React").
|
|
22
|
-
- **salary**: parse cả tiếng Việt ("15-20 triệu", "thỏa thuận", "Up to $2000", "range 30-50m") → {min, max, currency, period, negotiable}. "Thỏa thuận"/"Negotiable" → currency=unknown, negotiable=true.
|
|
23
|
-
- **remote**: nhận diện "remote", "làm việc từ xa", "hybrid", "onsite", "tại văn phòng".
|
|
24
|
-
- **seniority**: suy từ title/JD ("Junior", "Senior", "Trưởng nhóm"→lead, "Fresher"→junior).
|
|
25
|
-
- **company**: Nếu JD không nêu rõ tên cty (stealth startup, headhunter tuyển hộ) → ghi `Confidential (<lĩnh vực>)` hoặc tên agency (vd `Confidential (Fintech HCM)`).
|
|
26
|
-
- **contact**: Trích xuất kênh liên hệ ứng tuyển nếu JD có:
|
|
27
|
-
- `email`: email nhận CV (vd `hr@company.com`).
|
|
28
|
-
- `phone`: số điện thoại liên hệ.
|
|
29
|
-
- `form_url`: link Google Form / Typeform nộp hồ sơ.
|
|
30
|
-
- `how_to_apply`: tóm tắt ngắn (vd `"Gửi CV qua email hr@..."`, `"Apply qua link"`).
|
|
31
|
-
- **language**: `vi`/`en`/`mixed` tùy JD.
|
|
32
|
-
- **min_years**: parse "2+ năm kinh nghiệm", "at least 3 years".
|
|
33
|
-
|
|
34
|
-
## Nguyên tắc chống bịa
|
|
35
|
-
|
|
36
|
-
- Chỉ điền trường khi có bằng chứng trong dữ liệu nguồn. Không suy đoán lương/quy mô công ty nếu JD không nói → để `unknown`.
|
|
37
|
-
- `url` luôn là link trang tuyển dụng gốc để người dùng bấm vào apply.
|
|
38
|
-
- Rút gọn `description` nếu quá dài (giữ phần requirements + trách nhiệm chính).
|
|
39
|
-
|
|
40
|
-
## Ví dụ tin tuyển dụng chuẩn (Full JD)
|
|
41
|
-
|
|
42
|
-
```json
|
|
43
|
-
{
|
|
44
|
-
"id": "a1b2c3",
|
|
45
|
-
"title": "Senior Frontend Developer (ReactJS)",
|
|
46
|
-
"company": "ABC Tech",
|
|
47
|
-
"location": "Ho Chi Minh City",
|
|
48
|
-
"remote": "hybrid",
|
|
49
|
-
"url": "https://itviec.com/it-jobs/senior-frontend-abc",
|
|
50
|
-
"source": "itviec",
|
|
51
|
-
"language": "mixed",
|
|
52
|
-
"requirements": {
|
|
53
|
-
"must_have_skills": ["React", "TypeScript"],
|
|
54
|
-
"nice_to_have_skills": ["Next.js"],
|
|
55
|
-
"min_years": 3,
|
|
56
|
-
"seniority": "senior"
|
|
57
|
-
},
|
|
58
|
-
"salary": { "min": 30000000, "max": 45000000, "currency": "VND", "period": "month" },
|
|
59
|
-
"collected_at": "2026-08-25T10:00:00Z",
|
|
60
|
-
"extraction_confidence": 0.9
|
|
61
|
-
}
|
|
62
|
-
```
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: scoring-rubric
|
|
3
|
-
description: Công thức chấm điểm mức độ khớp giữa 1 job và profile ứng viên. Dùng bởi job-matcher và bất kỳ nơi nào cần đánh giá độ phù hợp job/CV. Định nghĩa trọng số, cách tính từng chiều, hard filter (dealbreakers), và cách xử lý dữ liệu thiếu.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Scoring Rubric — chấm điểm khớp job ↔ profile
|
|
7
|
-
|
|
8
|
-
Đây là "bộ não" quyết định chất lượng match. Kết quả tuân theo `./schemas/match.schema.json`.
|
|
9
|
-
|
|
10
|
-
## Nguyên tắc
|
|
11
|
-
|
|
12
|
-
1. Điểm mỗi chiều thang **0–100**, độc lập.
|
|
13
|
-
2. `overall_score` = tổng có trọng số của các chiều, **trừ** khi có dealbreaker (→ excluded).
|
|
14
|
-
3. Minh bạch: luôn ghi `weights_used`, `matched_skills`, `missing_must_have`, `rationale`.
|
|
15
|
-
4. Dữ liệu thiếu KHÔNG bị chấm 0 một cách mù quáng — xem mục "Xử lý dữ liệu thiếu".
|
|
16
|
-
|
|
17
|
-
## Bước 0 — Hard filter (dealbreakers)
|
|
18
|
-
|
|
19
|
-
Trước khi chấm điểm, kiểm tra `profile.target.dealbreakers` và các ràng buộc cứng:
|
|
20
|
-
|
|
21
|
-
- Vi phạm bất kỳ dealbreaker → `recommendation = "excluded"`, `overall_score = 0`, ghi vào `dealbreaker_violations`. **Không** tính tiếp.
|
|
22
|
-
- Ví dụ dealbreaker cứng thường gặp: location ngoài vùng chấp nhận (khi remote=onsite), loại hình công ty bị loại (outsource), lương max < salary.min của ứng viên (khi có đủ dữ liệu).
|
|
23
|
-
- Nếu dữ liệu không đủ để khẳng định vi phạm (vd lương "unknown") → KHÔNG loại, hạ `confidence` thay vì exclude.
|
|
24
|
-
|
|
25
|
-
## Trọng số mặc định
|
|
26
|
-
|
|
27
|
-
| Chiều | Mặc định | Ý nghĩa |
|
|
28
|
-
|---|---|---|
|
|
29
|
-
| skills | 0.35 | Khớp kỹ năng must-have / nice-to-have |
|
|
30
|
-
| seniority | 0.20 | Cấp bậc & số năm kinh nghiệm |
|
|
31
|
-
| domain | 0.15 | Ngành/lĩnh vực |
|
|
32
|
-
| location | 0.05 | Địa điểm & hình thức remote |
|
|
33
|
-
| compensation | 0.15 | Lương so với kỳ vọng |
|
|
34
|
-
| culture | 0.05 | Quy mô/loại hình công ty, tín hiệu văn hóa |
|
|
35
|
-
|
|
36
|
-
**Bắt buộc chuẩn hóa:** cả bộ trọng số mặc định và bộ `profile.target.priorities` override đều phải
|
|
37
|
-
được chia cho tổng thực tế trước khi tính điểm, để tổng `weights_used` luôn bằng 1. Bộ số mặc định
|
|
38
|
-
trên chủ ý thể hiện tỷ lệ tương đối; không được dùng trực tiếp khi tổng chưa bằng 1. Luôn ghi bộ đã
|
|
39
|
-
chuẩn hóa vào `weights_used`.
|
|
40
|
-
|
|
41
|
-
## Cách tính từng chiều
|
|
42
|
-
|
|
43
|
-
### skills (0–100)
|
|
44
|
-
- Chuẩn hóa mọi skill về canonical name (dùng skill `bilingual-normalization`) trước khi so khớp.
|
|
45
|
-
- `must_cover` = tỉ lệ must-have của job mà profile đáp ứng.
|
|
46
|
-
- `nice_cover` = tỉ lệ nice-to-have đáp ứng.
|
|
47
|
-
- `skills_score = 100 * (0.75 * must_cover + 0.25 * nice_cover)`.
|
|
48
|
-
- Cộng thưởng nhẹ nếu ứng viên có skill level `advanced/expert` cho must-have chính (tối đa +10, cap 100).
|
|
49
|
-
- Nếu job không nêu must-have (`must_have_skills` rỗng) → suy từ title + description; hạ `confidence`.
|
|
50
|
-
- Ghi `matched_skills` và `missing_must_have`.
|
|
51
|
-
|
|
52
|
-
### seniority (0–100)
|
|
53
|
-
- So `profile.seniority` với `job.requirements.seniority` (map thang: intern<junior<mid<senior<lead<manager<director).
|
|
54
|
-
- Khoảng cách 0 bậc → 100; 1 bậc → 75; 2 bậc → 45; ≥3 bậc → 15.
|
|
55
|
-
- **Overqualified** (ứng viên cao hơn job ≥2 bậc) cũng bị trừ (dễ bị loại/chán việc): áp cùng thang khoảng cách.
|
|
56
|
-
- Điều chỉnh bằng `min_years`: nếu `total_years < min_years` → nhân 0.6; nếu thừa nhiều năm → không thưởng.
|
|
57
|
-
- Nếu job seniority = unknown → suy từ min_years/title; hạ confidence.
|
|
58
|
-
|
|
59
|
-
### domain (0–100)
|
|
60
|
-
- Trùng ngành trong `profile.domains`/`target.industries` với `job.industry` → 100.
|
|
61
|
-
- Ngành liền kề/chuyển giao được (adjacent) → 60.
|
|
62
|
-
- Không liên quan → 30 (không phải 0, vì kỹ năng có thể chuyển ngành).
|
|
63
|
-
|
|
64
|
-
### location (0–100)
|
|
65
|
-
- remote=remote và ứng viên chấp nhận remote/any → 100.
|
|
66
|
-
- Job location nằm trong `target.locations` → 100.
|
|
67
|
-
- hybrid + cùng thành phố → 85.
|
|
68
|
-
- onsite khác thành phố nhưng ứng viên để "any" → 60.
|
|
69
|
-
- onsite ngoài vùng chấp nhận (chưa tới mức dealbreaker) → 25.
|
|
70
|
-
|
|
71
|
-
### compensation (0–100)
|
|
72
|
-
- Cần đưa về cùng đơn vị (VND/tháng) — dùng `bilingual-normalization` để quy đổi USD↔VND & year↔month.
|
|
73
|
-
- job_mid = trung bình (min,max). So với `target.salary.target` (hoặc min).
|
|
74
|
-
- job_mid ≥ target → 100; trong khoảng [min, target] → nội suy 70–100; dưới min nhưng ≥ 90% min → 50; thấp hơn → 20.
|
|
75
|
-
- Lương unknown → điểm neutral 60, hạ confidence (KHÔNG chấm 0).
|
|
76
|
-
|
|
77
|
-
### culture (0–100)
|
|
78
|
-
- Khớp `target.company_size` → +40 baseline lên; tín hiệu tích cực trong JD (đãi ngộ, lộ trình, tech stack hiện đại) cộng thêm.
|
|
79
|
-
- Ít dữ liệu → 60 neutral.
|
|
80
|
-
|
|
81
|
-
## Xử lý dữ liệu thiếu
|
|
82
|
-
|
|
83
|
-
- Không bao giờ chấm 0 vì "thiếu thông tin" — dùng điểm **neutral** (55–60) và **giảm `confidence`**.
|
|
84
|
-
- `confidence` tổng của match ≈ trung bình có trọng số của `job.extraction_confidence` và độ đầy đủ dữ liệu các chiều.
|
|
85
|
-
- Match có confidence < 0.4 nên được đánh dấu "cần xác minh JD" trong rationale.
|
|
86
|
-
|
|
87
|
-
## Tổng hợp & khuyến nghị
|
|
88
|
-
|
|
89
|
-
```
|
|
90
|
-
normalized_weight[d] = weight[d] / Σ weight
|
|
91
|
-
overall = Σ (dimension_score[d] * normalized_weight[d]) với d ∈ 6 chiều
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
Map sang `recommendation`:
|
|
95
|
-
|
|
96
|
-
| overall | recommendation |
|
|
97
|
-
|---|---|
|
|
98
|
-
| ≥ 80 | strong |
|
|
99
|
-
| 65–79 | good |
|
|
100
|
-
| 50–64 | maybe |
|
|
101
|
-
| < 50 | weak |
|
|
102
|
-
| (dealbreaker) | excluded |
|
|
103
|
-
|
|
104
|
-
## Đầu ra
|
|
105
|
-
|
|
106
|
-
Trả về đúng `./schemas/match.schema.json`: overall_score, recommendation, dimension_scores, weights_used, matched_skills, missing_must_have, gaps[], dealbreaker_violations[], rationale, confidence.
|
|
107
|
-
|
|
108
|
-
`rationale` viết ngắn, cụ thể, song ngữ nếu cần — nêu 1–2 lý do mạnh nhất và 1 gap lớn nhất.
|