Add content-level language filter, recheck command, expanded keywords
- isEnglishContent() fetches each job page and drops descriptions with ≥6 German or French indicator words; called for every new job in index.ts before writing to CSV - New `bun recheck` command (recheck.ts) runs searches in raw mode, lists filtered-out jobs, and prints word-frequency suggestions for SOFTWARE_TITLE_RE - Expanded KEYWORDS and SOFTWARE_TITLE_RE to cover ML/AI, system/applications engineer, director, informatics, and more title variants - Added `raw` flag to fetchJobs() to bypass the software-role filter - Add CLAUDE.md with project workflow docs; add CV files and city temp scripts
This commit is contained in:
@@ -0,0 +1,92 @@
|
|||||||
|
# linkme — Claude skill
|
||||||
|
|
||||||
|
This project scrapes LinkedIn jobs and filters/ranks them for the user's job search.
|
||||||
|
The user's CV is at `polo.md`. Always use it when ranking opportunities.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| command | what it does |
|
||||||
|
|---|---|
|
||||||
|
| `bun fetch` | scrape Basel area (3 searches), append new jobs to `jobs.csv` |
|
||||||
|
| `bun recheck` | fetch in raw mode, list filtered-out jobs + word-frequency keyword suggestions |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Searching any city temporarily
|
||||||
|
|
||||||
|
Write a throwaway script (e.g. `bern-temp.ts`) — never touch `config.ts` unless the user explicitly asks to save the change.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { KEYWORDS } from "./config.ts";
|
||||||
|
import { fetchJobs } from "./fetch.ts";
|
||||||
|
import type { SearchConfig } from "./config.ts";
|
||||||
|
|
||||||
|
const searches: SearchConfig[] = [
|
||||||
|
{ label: "City Name", keywords: KEYWORDS, location: "City, Switzerland", workType: ["onsite", "hybrid"], timePosted: "month" },
|
||||||
|
];
|
||||||
|
// collect, deduplicate, print
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `location: "City, Switzerland"` (no geoId) when the geoId is unknown — LinkedIn resolves by name.
|
||||||
|
|
||||||
|
Known Swiss geoIds:
|
||||||
|
- Basel city: `103383283`
|
||||||
|
- Basel Metropolitan Area: `90009885`
|
||||||
|
- Basel-Country: `107117530`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Filtering job descriptions by language
|
||||||
|
|
||||||
|
Use the `bern-langcheck.ts` pattern: fetch each job page, extract the description, count German/French indicator words, drop anything with ≥ 6 matches.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const DE_CONTENT_RE = /\b(Sie|Ihre?|Ihnen|Wir|wir|und\s|für|mit\s|von\s|bei\s|das\s|ist\s|sind\s|haben\s|werden\s|können|suchen|bieten|Kenntnisse|Erfahrung|Anforderungen|Aufgaben|Stellenbeschreibung|Stellenangebot|Bewerbung|Ausbildung|Berufserfahrung|Teamarbeit|Quereinsteiger|Vollzeit|Teilzeit|unbefristet|Abschluss|gesucht|Deutsch|Englisch|Schweizer)\b/g;
|
||||||
|
const FR_CONTENT_RE = /\b(vous|nous|votre|notre|pour\s|avec\s|profil|poste|emploi|entreprise|compétences|expérience|formation|équipe|rejoindre|recherchons|offrons|missions|candidature|CDI|CDD|temps\s+plein|temps\s+partiel)\b/g;
|
||||||
|
```
|
||||||
|
|
||||||
|
Extract description from `class="show-more-less-html__markup"`, fall back to stripping all HTML. Add 350 ms delay between requests.
|
||||||
|
|
||||||
|
Title-only filtering runs during scraping via `NON_ENGLISH_RE` in `fetch.ts`. Content-level filtering via `isEnglishContent()` is now always applied in `index.ts` before writing to CSV — every new job has its description page fetched and checked.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ranking jobs against the CV
|
||||||
|
|
||||||
|
Read `polo.md` for the full profile. Key signals to match against:
|
||||||
|
|
||||||
|
- **Seniority**: Senior / Staff / Principal / Lead / Architect — weight titles accordingly
|
||||||
|
- **Core stack**: Python · TypeScript · Ruby on Rails · React · Node.js
|
||||||
|
- **Domain strengths**: platform engineering · data pipelines · AI/LLM integrations · DevOps (Docker, Kubernetes, CI/CD, AWS)
|
||||||
|
- **Background edges**: fintech/CBDC (Moedax), civic tech (Tactical Tech / GFOSS), independent consulting (ExtraPolo, 40+ projects)
|
||||||
|
- **Languages**: English (full) · German B1 TELC certified · Spanish (full) — flag roles that require C1+ German
|
||||||
|
- **Location**: based in Basel, open to Basel / Bern / Zürich; EU citizen, no permit needed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output format
|
||||||
|
|
||||||
|
Always present jobs as a markdown table with columns: `#`, `Title`, `Company`, `Why it fits`, `Link`.
|
||||||
|
|
||||||
|
Links use just the job ID as the anchor text:
|
||||||
|
```
|
||||||
|
[4392954199](https://www.linkedin.com/jobs/view/4392954199/)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow for "find jobs in X city"
|
||||||
|
|
||||||
|
1. Write a temp script for that city, run it, collect all passing jobs.
|
||||||
|
2. If the user says "English only" or "filter language": run the content-language check script against the results.
|
||||||
|
3. Rank survivors against `polo.md`, present top N as a table.
|
||||||
|
4. Delete the temp scripts after the session unless the user asks to keep them.
|
||||||
|
|
||||||
|
## Workflow for "check if new keywords should be added"
|
||||||
|
|
||||||
|
1. Run `bun recheck`.
|
||||||
|
2. Review the filtered-out list manually — identify false negatives (real software roles that got dropped).
|
||||||
|
3. Check the word-frequency suggestions but ignore generic terms (`engineer`, `development`, `senior`).
|
||||||
|
4. Propose specific additions to `SOFTWARE_TITLE_RE` in `fetch.ts` or words to `NON_ENGLISH_RE`.
|
||||||
@@ -5,7 +5,8 @@ Scrapes LinkedIn job listings into a local CSV. Run it daily, delete rows you've
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun fetch
|
bun fetch # scrape and append new jobs to jobs.csv
|
||||||
|
bun recheck # show jobs currently filtered out + keyword suggestions
|
||||||
```
|
```
|
||||||
|
|
||||||
No setup needed. Results are appended to `jobs.csv` — reruns are safe, already-seen jobs are skipped by ID.
|
No setup needed. Results are appended to `jobs.csv` — reruns are safe, already-seen jobs are skipped by ID.
|
||||||
@@ -30,24 +31,48 @@ Open in any spreadsheet app. Delete rows as you apply or dismiss.
|
|||||||
- **Paginated scraping** — fetches up to 6 pages per search (150 jobs) with a 400 ms delay between pages to avoid rate limiting
|
- **Paginated scraping** — fetches up to 6 pages per search (150 jobs) with a 400 ms delay between pages to avoid rate limiting
|
||||||
- **Card-boundary-safe parsing** — each job card is isolated before extracting fields, preventing content from adjacent cards bleeding into company or date values
|
- **Card-boundary-safe parsing** — each job card is isolated before extracting fields, preventing content from adjacent cards bleeding into company or date values
|
||||||
- **Language filter** — drops German (`m/w/d`, umlauts, gender-marked vocabulary) and French (`H/F`, accented characters) job titles automatically
|
- **Language filter** — drops German (`m/w/d`, umlauts, gender-marked vocabulary) and French (`H/F`, accented characters) job titles automatically
|
||||||
- **Software-role filter** — requires the title to contain a relevant term (software, developer, data, AI, backend, devops, platform, architect, technical, solutions, consultant, product/IT/engineering manager, etc.) — eliminates aviation, mechanical, and pharma-process roles that match "Engineer" broadly
|
- **Software-role filter** — requires the title to contain a relevant term (software, developer, data, AI, backend, frontend, devops, platform, architect, tech lead, typescript, python, rails, node.js, ML, cloud, security, SRE, solutions, consultant, applications/system engineer, director, product/IT/engineering manager, staff/principal engineer, CTO, VP engineering, etc.) — eliminates aviation, mechanical, and pharma-process roles that match "Engineer" broadly
|
||||||
- **Seniority filter** — drops junior, intern, trainee, and apprentice titles
|
- **Seniority filter** — drops junior, jr., intern, trainee, apprentice, werkstudent, and entry-level titles
|
||||||
- **Global deduplication** — job IDs are indexed from the existing CSV on startup; jobs you've already seen are never re-added, even if you delete and re-run
|
- **Global deduplication** — job IDs are indexed from the existing CSV on startup; jobs you've already seen are never re-added, even if you re-run
|
||||||
- **30-day recency** — only fetches jobs posted in the last month (`f_TPR=r2592000`)
|
- **30-day recency** — only fetches jobs posted in the last month (`f_TPR=r2592000`)
|
||||||
|
|
||||||
|
## recheck
|
||||||
|
|
||||||
|
`bun recheck` fetches all three search areas in raw mode (bypassing the software-role filter) and lists every job that would be filtered out — useful for spotting false negatives. It also prints a word-frequency table of the most common terms in filtered titles so you can decide what to add to `SOFTWARE_TITLE_RE`.
|
||||||
|
|
||||||
## Configure searches
|
## Configure searches
|
||||||
|
|
||||||
Edit `config.ts`:
|
Edit `config.ts`:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const KEYWORDS = [
|
export const KEYWORDS = [
|
||||||
"Software Engineer",
|
"Software Engineer",
|
||||||
|
"Solutions Engineer",
|
||||||
"Full Stack Engineer",
|
"Full Stack Engineer",
|
||||||
|
"Backend Engineer",
|
||||||
"Data Engineer",
|
"Data Engineer",
|
||||||
"AI Engineer",
|
"AI Engineer",
|
||||||
|
"Applications Engineer",
|
||||||
|
"Platform Engineer",
|
||||||
|
"DevOps Engineer",
|
||||||
|
"Staff Engineer",
|
||||||
|
"Principal Engineer",
|
||||||
|
"Tech Lead",
|
||||||
|
"Developer",
|
||||||
"Ruby on Rails",
|
"Ruby on Rails",
|
||||||
"Python",
|
"Python",
|
||||||
// add or remove freely
|
"Node.js",
|
||||||
|
"TypeScript",
|
||||||
|
"Artificial Intelligence",
|
||||||
|
"Machine Learning",
|
||||||
|
"Data Science",
|
||||||
|
"AI Consultant",
|
||||||
|
"ML Engineer",
|
||||||
|
"Director of Engineering",
|
||||||
|
"Engineering Manager",
|
||||||
|
"Lead Developer",
|
||||||
|
"System Engineer",
|
||||||
|
"Solution Engineer",
|
||||||
].join(" OR ");
|
].join(" OR ");
|
||||||
|
|
||||||
export const searches: SearchConfig[] = [
|
export const searches: SearchConfig[] = [
|
||||||
@@ -59,6 +84,22 @@ export const searches: SearchConfig[] = [
|
|||||||
workType: ["onsite", "hybrid"],
|
workType: ["onsite", "hybrid"],
|
||||||
timePosted: "month",
|
timePosted: "month",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Basel Metropolitan Area",
|
||||||
|
keywords: KEYWORDS,
|
||||||
|
geoId: "90009885",
|
||||||
|
location: "Basel Metropolitan Area",
|
||||||
|
workType: ["onsite", "hybrid"],
|
||||||
|
timePosted: "month",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Basel-Country",
|
||||||
|
keywords: KEYWORDS,
|
||||||
|
geoId: "107117530",
|
||||||
|
location: "Basel-Country",
|
||||||
|
workType: ["onsite", "hybrid"],
|
||||||
|
timePosted: "month",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -68,3 +109,5 @@ export const searches: SearchConfig[] = [
|
|||||||
- `107117530` — Basel-Country
|
- `107117530` — Basel-Country
|
||||||
|
|
||||||
You can also paste any LinkedIn search URL directly as `url:` instead of building from filters.
|
You can also paste any LinkedIn search URL directly as `url:` instead of building from filters.
|
||||||
|
|
||||||
|
`SearchConfig` also supports `jobType` (full-time, part-time, contract, temporary, internship) and `seniority` (internship, entry, associate, mid-senior, director, executive) to pass LinkedIn's native API filters.
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
const DE_CONTENT_RE = /\b(Sie|Ihre?|Ihnen|Wir|wir|und\s|für|mit\s|von\s|bei\s|das\s|ist\s|sind\s|haben\s|werden\s|können|suchen|bieten|Kenntnisse|Erfahrung|Anforderungen|Aufgaben|Stellenbeschreibung|Stellenangebot|Bewerbung|Ausbildung|Berufserfahrung|Teamarbeit|Quereinsteiger|Vollzeit|Teilzeit|unbefristet|Abschluss|gesucht|Deutsch|Englisch|Schweizer)\b/g;
|
||||||
|
const FR_CONTENT_RE = /\b(vous|nous|votre|notre|pour\s|avec\s|profil|poste|emploi|entreprise|compétences|expérience|formation|équipe|rejoindre|recherchons|offrons|missions|candidature|CDI|CDD|temps\s+plein|temps\s+partiel)\b/g;
|
||||||
|
|
||||||
|
const HEADERS = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
||||||
|
"Accept-Language": "en-US,en;q=0.9",
|
||||||
|
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
|
};
|
||||||
|
|
||||||
|
const jobs = [
|
||||||
|
{ id: "4412445330", title: "Senior Software and System Architect", company: "NVIDIA AI" },
|
||||||
|
{ id: "4406964785", title: "Digital Platform Engineer – EasyGov.swiss", company: "Swiss Federal Administration" },
|
||||||
|
{ id: "4405193749", title: "Technical Lead Web Portals", company: "Smart Energy Link AG" },
|
||||||
|
{ id: "4411445039", title: "Senior System Engineer (Cloud Native, Remote-First)", company: "Adfinis" },
|
||||||
|
{ id: "4411290320", title: "Automation Data Engineer", company: "Takeda Pharmaceuticals" },
|
||||||
|
{ id: "4411348405", title: "Solutions Architect", company: "RJC Group" },
|
||||||
|
{ id: "4406980033", title: "Enterprise Architect", company: "Swiss Federal Administration" },
|
||||||
|
{ id: "4367874399", title: "Software Engineer", company: "Cartier" },
|
||||||
|
{ id: "4399418573", title: "Data Science Specialist", company: "Nestlé" },
|
||||||
|
{ id: "4411369976", title: "Data Scientist 80–100%", company: "Fincons Group" },
|
||||||
|
{ id: "4412464434", title: "Solution Engineer Management Systems", company: "Siemens" },
|
||||||
|
{ id: "4401638413", title: "Fullstack Developer (Java/React)", company: "soobr" },
|
||||||
|
{ id: "4399356316", title: "Senior Java Software Engineer", company: "Smart Energy Link AG" },
|
||||||
|
{ id: "4407778229", title: "Software Engineer", company: "Softcom" },
|
||||||
|
{ id: "4402476156", title: "Software Developer", company: "SwissPeak Partners" },
|
||||||
|
// remaining from the 20
|
||||||
|
{ id: "4404649130", title: "Senior System Engineer (100%)", company: "nanoTRONIC" },
|
||||||
|
{ id: "4408770074", title: "System Engineer", company: "Infopro AG" },
|
||||||
|
{ id: "4408864023", title: "System Engineer – Workplace Engineering", company: "Bedag Informatique SA" },
|
||||||
|
{ id: "4405214433", title: "Cloud Platform Engineer (Kubernetes/Azure/DevOps)", company: "Gramian Consulting" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function detectLanguage(text: string): "de" | "fr" | "en" {
|
||||||
|
const deMatches = (text.match(DE_CONTENT_RE) ?? []).length;
|
||||||
|
const frMatches = (text.match(FR_CONTENT_RE) ?? []).length;
|
||||||
|
if (deMatches >= 6) return "de";
|
||||||
|
if (frMatches >= 6) return "fr";
|
||||||
|
return "en";
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractDescription(html: string): string {
|
||||||
|
const m = html.match(/class="show-more-less-html__markup[^"]*">([\s\S]{0,8000}?)<\/div>/);
|
||||||
|
if (m) return m[1].replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
||||||
|
// fallback: grab large text blocks
|
||||||
|
return html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").slice(0, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const passed: typeof jobs = [];
|
||||||
|
const dropped: { id: string; title: string; company: string; lang: string }[] = [];
|
||||||
|
|
||||||
|
for (const job of jobs) {
|
||||||
|
await Bun.sleep(350);
|
||||||
|
process.stdout.write(` checking ${job.company} — ${job.title} … `);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`https://www.linkedin.com/jobs/view/${job.id}/`, { headers: HEADERS });
|
||||||
|
const html = await res.text();
|
||||||
|
const desc = extractDescription(html);
|
||||||
|
const lang = detectLanguage(desc);
|
||||||
|
console.log(lang);
|
||||||
|
if (lang === "en") passed.push(job);
|
||||||
|
else dropped.push({ ...job, lang });
|
||||||
|
} catch (e) {
|
||||||
|
console.log("fetch error, keeping");
|
||||||
|
passed.push(job);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n── ${dropped.length} dropped (non-English) ──`);
|
||||||
|
for (const j of dropped) console.log(` [${j.lang}] ${j.title} @ ${j.company}`);
|
||||||
|
|
||||||
|
console.log(`\n── ${passed.length} English jobs ──`);
|
||||||
|
for (const [i, j] of passed.entries()) {
|
||||||
|
console.log(` ${i + 1}. ${j.title} — ${j.company} https://www.linkedin.com/jobs/view/${j.id}/`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
import { KEYWORDS } from "./config.ts";
|
||||||
|
import { fetchJobs, SOFTWARE_TITLE_RE } from "./fetch.ts";
|
||||||
|
import type { SearchConfig } from "./config.ts";
|
||||||
|
|
||||||
|
const JUNIOR_RE = /\b(junior|jr\.?|entry.?level|graduate|intern(?:ship)?|trainee|apprentice|werkstudent)\b/i;
|
||||||
|
|
||||||
|
const searches: SearchConfig[] = [
|
||||||
|
{ label: "Bern (city)", keywords: KEYWORDS, location: "Bern, Switzerland", workType: ["onsite", "hybrid"], timePosted: "month" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const results: { title: string; company: string; link: string; search: string }[] = [];
|
||||||
|
|
||||||
|
for (const config of searches) {
|
||||||
|
console.log(`\n[${config.label}]`);
|
||||||
|
const jobs = await fetchJobs(config, { raw: false });
|
||||||
|
for (const job of jobs) {
|
||||||
|
if (!seen.has(job.id)) {
|
||||||
|
seen.add(job.id);
|
||||||
|
results.push({ title: job.title, company: job.company, link: job.link, search: job.search });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n── ${results.length} jobs found in Bern ──\n`);
|
||||||
|
for (const j of results) {
|
||||||
|
console.log(` [${j.search}] ${j.title}`);
|
||||||
|
console.log(` ${j.company} ${j.link}`);
|
||||||
|
}
|
||||||
@@ -16,13 +16,14 @@ export interface SearchConfig {
|
|||||||
seniority?: Seniority[];
|
seniority?: Seniority[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const KEYWORDS = [
|
export const KEYWORDS = [
|
||||||
"Software Engineer",
|
"Software Engineer",
|
||||||
"Solutions Engineer",
|
"Solutions Engineer",
|
||||||
"Full Stack Engineer",
|
"Full Stack Engineer",
|
||||||
"Backend Engineer",
|
"Backend Engineer",
|
||||||
"Data Engineer",
|
"Data Engineer",
|
||||||
"AI Engineer",
|
"AI Engineer",
|
||||||
|
"Applications Engineer",
|
||||||
"Platform Engineer",
|
"Platform Engineer",
|
||||||
"DevOps Engineer",
|
"DevOps Engineer",
|
||||||
"Staff Engineer",
|
"Staff Engineer",
|
||||||
@@ -33,6 +34,16 @@ const KEYWORDS = [
|
|||||||
"Python",
|
"Python",
|
||||||
"Node.js",
|
"Node.js",
|
||||||
"TypeScript",
|
"TypeScript",
|
||||||
|
"Artificial Intelligence",
|
||||||
|
"Machine Learning",
|
||||||
|
"Data Science",
|
||||||
|
"AI Consultant",
|
||||||
|
"ML Engineer",
|
||||||
|
"Director of Engineering",
|
||||||
|
"Engineering Manager",
|
||||||
|
"Lead Developer",
|
||||||
|
"System Engineer",
|
||||||
|
"Solution Engineer",
|
||||||
].join(" OR ");
|
].join(" OR ");
|
||||||
|
|
||||||
// --- Define your searches here ---
|
// --- Define your searches here ---
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ const NON_ENGLISH_RE = new RegExp(
|
|||||||
// /in word-ending (e.g. Mitarbeiter/in) — excludes m/f, n/a, etc.
|
// /in word-ending (e.g. Mitarbeiter/in) — excludes m/f, n/a, etc.
|
||||||
String.raw`[a-zäöü]\/in\b`,
|
String.raw`[a-zäöü]\/in\b`,
|
||||||
// German-only words (case-insensitive handled by flag)
|
// German-only words (case-insensitive handled by flag)
|
||||||
String.raw`\b(Mitarbeiter|Pflegehelfer|Köchin|Koch EFZ|Kellner|Sozialpädagog|Lehrperson|Produktionsmitarbeiter|Produktionsplaner|Geschäftsführer|Spontanbewerbung|Arztsekretär|Physiotherapeut|Empfangsmitarbeiter|Automobilmechatroniker|Verkäufer|Projektleiter|Crewmitarbeiter|Crewmember|Systemgastronomie|Studienteilnehmende|Teilnehmende|Bauingenieur|Tiefbau|Hochbau|Elektroingenieur|Maschinenbau|gesucht|EFZ|Schweiz|Filiale)\b`,
|
String.raw`\b(Deine|Mitarbeiter|Pflegehelfer|Köchin|Koch EFZ|Kellner|Sozialpädagog|Lehrperson|Produktionsmitarbeiter|Produktionsplaner|Geschäftsführer|Spontanbewerbung|Arztsekretär|Physiotherapeut|Empfangsmitarbeiter|Automobilmechatroniker|Verkäufer|Projektleiter|Crewmitarbeiter|Crewmember|Systemgastronomie|Studienteilnehmende|Teilnehmende|Bauingenieur|Tiefbau|Hochbau|Elektroingenieur|Maschinenbau|gesucht|EFZ|Schweiz|Filiale)\b`,
|
||||||
// French gender markers
|
// French gender markers
|
||||||
String.raw`\(H\/F\)|\(F\/H\)|\(h\/f\)|\(f\/h\)`,
|
String.raw`\(H\/F\)|\(F\/H\)|\(h\/f\)|\(f\/h\)`,
|
||||||
// French-only accented characters
|
// French-only accented characters
|
||||||
@@ -89,7 +89,27 @@ const HEADERS = {
|
|||||||
|
|
||||||
const JUNIOR_TITLE_RE = /\b(junior|jr\.?|entry.?level|graduate|intern(?:ship)?|trainee|apprentice|werkstudent)\b/i;
|
const JUNIOR_TITLE_RE = /\b(junior|jr\.?|entry.?level|graduate|intern(?:ship)?|trainee|apprentice|werkstudent)\b/i;
|
||||||
|
|
||||||
const SOFTWARE_TITLE_RE = /\b(software|developer|architect|devops|platform|data|backend|frontend|fullstack|full.stack|tech\s*lead|technical|typescript|python|rails|node\.?js|ai\b|machine\s*learning|cloud|security|analytics|sre|reliability|solutions|consultant|product\s*manager|engineering\s*manager|it\s*manager|it\s*project|staff\s*engineer|principal\s*engineer|cto|vp\s*eng)\b/i;
|
export const SOFTWARE_TITLE_RE = /\b(software|developer|architect|devops|platform|data|backend|frontend|fullstack|full.stack|tech\s*lead|technical|typescript|python|rails|node\.?js|ai\b|machine\s*learning|cloud|security|analytics|sre|reliability|solutions?|consultant|applications?\s*engineer|system[\s-]*engineer|informatics|director|product\s*manager|engineering\s*manager|it\s*manager|it\s*project|staff\s*engineer|principal\s*engineer|cto|vp\s*eng)\b/i;
|
||||||
|
|
||||||
|
const DE_CONTENT_RE = /\b(Sie|Ihre?|Ihnen|Wir|wir|und\s|für|mit\s|von\s|bei\s|das\s|ist\s|sind\s|haben\s|werden\s|können|suchen|bieten|Kenntnisse|Erfahrung|Anforderungen|Aufgaben|Stellenbeschreibung|Stellenangebot|Bewerbung|Ausbildung|Berufserfahrung|Teamarbeit|Vollzeit|Teilzeit|unbefristet|Abschluss|gesucht|Deutsch|Englisch|Schweizer)\b/g;
|
||||||
|
const FR_CONTENT_RE = /\b(vous|nous|votre|notre|pour\s|avec\s|profil|poste|emploi|entreprise|compétences|expérience|formation|équipe|rejoindre|recherchons|offrons|missions|candidature|CDI|CDD|temps\s+plein|temps\s+partiel)\b/g;
|
||||||
|
|
||||||
|
export async function isEnglishContent(id: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await Bun.sleep(350);
|
||||||
|
const res = await fetch(`https://www.linkedin.com/jobs/view/${id}/`, { headers: HEADERS });
|
||||||
|
const html = await res.text();
|
||||||
|
const m = html.match(/class="show-more-less-html__markup[^"]*">([\s\S]{0,8000}?)<\/div>/);
|
||||||
|
const text = m
|
||||||
|
? m[1].replace(/<[^>]+>/g, " ")
|
||||||
|
: html.replace(/<[^>]+>/g, " ").slice(0, 5000);
|
||||||
|
const de = (text.match(DE_CONTENT_RE) ?? []).length;
|
||||||
|
const fr = (text.match(FR_CONTENT_RE) ?? []).length;
|
||||||
|
return de < 6 && fr < 6;
|
||||||
|
} catch {
|
||||||
|
return true; // keep on fetch error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const PAGE_SIZE = 25;
|
const PAGE_SIZE = 25;
|
||||||
const MAX_PAGES = 6;
|
const MAX_PAGES = 6;
|
||||||
@@ -156,7 +176,7 @@ function parseJobs(html: string, label: string): Job[] {
|
|||||||
return jobs;
|
return jobs;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchJobs(config: SearchConfig): Promise<Job[]> {
|
export async function fetchJobs(config: SearchConfig, { raw = false } = {}): Promise<Job[]> {
|
||||||
const baseUrl = buildUrl(config);
|
const baseUrl = buildUrl(config);
|
||||||
const allJobs: Job[] = [];
|
const allJobs: Job[] = [];
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
@@ -176,7 +196,7 @@ export async function fetchJobs(config: SearchConfig): Promise<Job[]> {
|
|||||||
const jobs = parseJobs(html, config.label)
|
const jobs = parseJobs(html, config.label)
|
||||||
.filter((j) => !NON_ENGLISH_RE.test(j.title))
|
.filter((j) => !NON_ENGLISH_RE.test(j.title))
|
||||||
.filter((j) => !JUNIOR_TITLE_RE.test(j.title))
|
.filter((j) => !JUNIOR_TITLE_RE.test(j.title))
|
||||||
.filter((j) => SOFTWARE_TITLE_RE.test(j.title));
|
.filter((j) => raw || SOFTWARE_TITLE_RE.test(j.title));
|
||||||
|
|
||||||
let newOnPage = 0;
|
let newOnPage = 0;
|
||||||
for (const job of jobs) {
|
for (const job of jobs) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
import { searches } from "./config.ts";
|
import { searches } from "./config.ts";
|
||||||
import { fetchJobs } from "./fetch.ts";
|
import { fetchJobs, isEnglishContent } from "./fetch.ts";
|
||||||
|
|
||||||
const CSV = "./jobs.csv";
|
const CSV = "./jobs.csv";
|
||||||
const HEADER = "date_found,posted_at,search,title,company,link";
|
const HEADER = "date_found,posted_at,search,title,company,link";
|
||||||
@@ -34,8 +34,17 @@ for (const config of searches) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let added = 0;
|
let added = 0;
|
||||||
|
let langDropped = 0;
|
||||||
for (const job of jobs) {
|
for (const job of jobs) {
|
||||||
if (!existingIds.has(job.id)) {
|
if (!existingIds.has(job.id)) {
|
||||||
|
process.stdout.write(` checking content: ${job.title} … `);
|
||||||
|
const english = await isEnglishContent(job.id);
|
||||||
|
if (!english) {
|
||||||
|
console.log("dropped (non-English)");
|
||||||
|
langDropped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
console.log("ok");
|
||||||
existingIds.add(job.id);
|
existingIds.add(job.id);
|
||||||
newRows.push(
|
newRows.push(
|
||||||
[date, job.postedAt, config.label, job.title, job.company, job.link]
|
[date, job.postedAt, config.label, job.title, job.company, job.link]
|
||||||
@@ -45,7 +54,7 @@ for (const config of searches) {
|
|||||||
added++;
|
added++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(` ${jobs.length} found, ${added} new`);
|
console.log(` ${jobs.length} found, ${added} new, ${langDropped} non-English dropped`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newRows.length === 0) {
|
if (newRows.length === 0) {
|
||||||
|
|||||||
+2
-1
@@ -3,6 +3,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"fetch": "bun run index.ts"
|
"fetch": "bun run index.ts",
|
||||||
|
"recheck": "bun run recheck.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Rafael Polo - CV</title>
|
||||||
|
<style>
|
||||||
|
@page {
|
||||||
|
size: 8.2681in 11.6929in;
|
||||||
|
margin: 0.5in;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: Calibri, Liberation Sans, sans-serif;
|
||||||
|
font-size: 11pt;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
display: table;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.left-col {
|
||||||
|
display: table-cell;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: 24pt;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0.05in;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
font-size: 11pt;
|
||||||
|
font-style: italic;
|
||||||
|
margin-bottom: 0.15in;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
table.personal {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 0.2in;
|
||||||
|
}
|
||||||
|
table.personal td {
|
||||||
|
padding: 0.02in 0.05in;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
table.personal .label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
width: 1in;
|
||||||
|
}
|
||||||
|
table.personal .header-row td {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 11pt;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border-bottom: 1px solid #999;
|
||||||
|
padding-bottom: 0.05in;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: 11pt;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin: 0.15in 0 0.08in 0;
|
||||||
|
border-bottom: 1px solid #999;
|
||||||
|
padding-bottom: 0.03in;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
margin: 0.05in 0;
|
||||||
|
padding-left: 0.15in;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
margin-bottom: 0.04in;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
.job-item {
|
||||||
|
margin-bottom: 0.08in;
|
||||||
|
}
|
||||||
|
.job-title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
.job-meta {
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 9pt;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
.job-desc {
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 10pt;
|
||||||
|
color: #444;
|
||||||
|
margin-left: 0.1in;
|
||||||
|
}
|
||||||
|
.edu-item {
|
||||||
|
margin-bottom: 0.05in;
|
||||||
|
}
|
||||||
|
.edu-degree {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
.edu-school {
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
.skill-item {
|
||||||
|
font-size: 10pt;
|
||||||
|
margin-bottom: 0.02in;
|
||||||
|
}
|
||||||
|
.lang-item {
|
||||||
|
font-size: 10pt;
|
||||||
|
margin-bottom: 0.02in;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
font-size: 9pt;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 0.2in;
|
||||||
|
padding-top: 0.1in;
|
||||||
|
border-top: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="title">Rafael Polo</div>
|
||||||
|
<div class="subtitle">Senior Software Engineer — Platform, Data & AI</div>
|
||||||
|
|
||||||
|
<table class="personal">
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" class="header-row">Personal</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">First name:</td>
|
||||||
|
<td>Rafael</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Last name:</td>
|
||||||
|
<td>Polo</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Citizenship:</td>
|
||||||
|
<td>Portuguese (EU) · Brazilian — no work permit required</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Address:</td>
|
||||||
|
<td>Basel, 4055, Switzerland</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Email:</td>
|
||||||
|
<td>rafael@extrapolo.com</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Web:</td>
|
||||||
|
<td>linkedin.com/in/rafaelpolo<br>extrapolo.com</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>Formal Education</h3>
|
||||||
|
<ul>
|
||||||
|
<li class="edu-item">
|
||||||
|
<span class="edu-degree"><em><strong>Postgraduate in Data Science for Finance</strong></em> – 2021</span><br>
|
||||||
|
<span class="edu-school">NOVA IMS Information Management School, Lisbon</span>
|
||||||
|
</li>
|
||||||
|
<li class="edu-item">
|
||||||
|
<span class="edu-degree"><em><strong>Master in Information Systems</strong></em> – 2010–2012</span><br>
|
||||||
|
<span class="edu-school">PPGI/UNIRIO — Knowledge Representation & Reasoning</span>
|
||||||
|
</li>
|
||||||
|
<li class="edu-item">
|
||||||
|
<span class="edu-degree"><em><strong>Bachelor in Computer Science</strong></em> – 2004–2009</span><br>
|
||||||
|
<span class="edu-school">Metodista Bennett, Rio de Janeiro</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Relevant Professional Experience</h3>
|
||||||
|
<ul>
|
||||||
|
<li class="job-item">
|
||||||
|
<span class="job-title">Swisscom</span> – <span class="job-meta">2024–2025 – Zürich, Switzerland</span><br>
|
||||||
|
<span class="job-desc"><strong>DevOps Engineer</strong> developing platforms for Telecommunication Operations.</span>
|
||||||
|
</li>
|
||||||
|
<li class="job-item">
|
||||||
|
<span class="job-title">EDGE Empower®</span> – <span class="job-meta">2024–2025 – Baar, Switzerland</span><br>
|
||||||
|
<span class="job-desc"><strong>Senior Technical Lead</strong> managing projects and external UX & Dev Teams for a SaaS Product.</span>
|
||||||
|
</li>
|
||||||
|
<li class="job-item">
|
||||||
|
<span class="job-title">ExtraPolo — Independent Consulting</span> – <span class="job-meta">2010–2022 – Rio de Janeiro · Berlin · Athens · Lisbon</span><br>
|
||||||
|
<span class="job-desc">40+ projects across civic tech, data journalism, open-source platforms and interactive installations for public and private clients.</span>
|
||||||
|
</li>
|
||||||
|
<li class="job-item">
|
||||||
|
<span class="job-title">Moedax Digital Services</span> – <span class="job-meta">2018–2021 – Lisbon · Rio de Janeiro</span><br>
|
||||||
|
<span class="job-desc"><strong>Founder & CTO</strong> – Fintech startup connecting CBDC digital currency with KYC compliance automation.</span>
|
||||||
|
</li>
|
||||||
|
<li class="job-item">
|
||||||
|
<span class="job-title">GFOSS.eu / Code + Create</span> – <span class="job-meta">2017–2018 – Athens, Greece</span><br>
|
||||||
|
<span class="job-desc"><strong>Web Programming Teacher</strong> – taught HTML, CSS, jQuery to refugee and local youth.</span>
|
||||||
|
</li>
|
||||||
|
<li class="job-item">
|
||||||
|
<span class="job-title">Tactical Tech Collective</span> – <span class="job-meta">2014–2016 – Berlin, Germany</span><br>
|
||||||
|
<span class="job-desc"><strong>Full-Stack Web Architect</strong> – investigative data tools under Exposing the Invisible team.</span>
|
||||||
|
</li>
|
||||||
|
<li class="job-item">
|
||||||
|
<span class="job-title">Trommsdorf + Drüner</span> – <span class="job-meta">2013–2014 – Berlin, Germany</span><br>
|
||||||
|
<span class="job-desc"><strong>Full-Stack Engineer</strong> – Innovation Team developing Business Intelligence dashboards.</span>
|
||||||
|
</li>
|
||||||
|
<li class="job-item">
|
||||||
|
<span class="job-title">Cortex Intelligence</span> – <span class="job-meta">2007–2008 – Rio de Janeiro, Brazil</span><br>
|
||||||
|
<span class="job-desc"><strong>Developer</strong> – Competitive Intelligence and Data Mining R&D team.</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Certifications</h3>
|
||||||
|
<ul>
|
||||||
|
<li class="skill-item">Swiss Federal Act on Data Protection (nFADP)</li>
|
||||||
|
<li class="skill-item">Gen AI Foundational Models — NLP & LLMs</li>
|
||||||
|
<li class="skill-item">AI Agents using RAG and LangChain</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Relevant Tech Skills</h3>
|
||||||
|
<ul>
|
||||||
|
<li class="skill-item"><strong>Languages & Frameworks:</strong> Python · Ruby on Rails · TypeScript · React · Java</li>
|
||||||
|
<li class="skill-item"><strong>DevOps & Infra:</strong> Docker · Kubernetes · CI/CD · AWS · Linux · Git</li>
|
||||||
|
<li class="skill-item"><strong>Data & AI:</strong> SQL · ETL · Data Engineering · RAG · LangChain · LLM integrations</li>
|
||||||
|
<li class="skill-item"><strong>Architecture:</strong> REST APIs · MVC · OOP · UML · System Design · Web Crawling</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Natural Languages</h3>
|
||||||
|
<ul>
|
||||||
|
<li class="lang-item">Native Brazilian Portuguese</li>
|
||||||
|
<li class="lang-item">Full professional English</li>
|
||||||
|
<li class="lang-item">Full professional Spanish</li>
|
||||||
|
<li class="lang-item">German B1 – TELC certified · 3 yrs Berlin + 3 yrs Basel</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
References available upon request · CV updated May 2026
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+196
@@ -0,0 +1,196 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Rafael Polo - CV</title>
|
||||||
|
<style>
|
||||||
|
@page {
|
||||||
|
size: 8.2681in 11.6929in;
|
||||||
|
margin: 0.5in;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: Liberation Sans, Carlito, Georgia, serif;
|
||||||
|
font-size: 11pt;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: 36pt;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0.1in;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
font-size: 14pt;
|
||||||
|
margin-bottom: 0.3in;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 24pt;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0.2in 0 0.1in 0;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
padding-bottom: 0.05in;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 18pt;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0.15in 0 0.08in 0;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: 14pt;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0.1in 0 0.05in 0;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
margin-bottom: 0.15in;
|
||||||
|
}
|
||||||
|
.personalinline {
|
||||||
|
font-size: 10pt;
|
||||||
|
margin-bottom: 0.2in;
|
||||||
|
}
|
||||||
|
.personalinline span {
|
||||||
|
margin-right: 0.3in;
|
||||||
|
}
|
||||||
|
.key-value {
|
||||||
|
display: flex;
|
||||||
|
margin: 0.03in 0;
|
||||||
|
}
|
||||||
|
.key {
|
||||||
|
font-weight: bold;
|
||||||
|
width: 1.5in;
|
||||||
|
}
|
||||||
|
.value {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
margin: 0.05in 0;
|
||||||
|
padding-left: 0.2in;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
margin-bottom: 0.03in;
|
||||||
|
}
|
||||||
|
.small {
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
.experience-item {
|
||||||
|
margin-bottom: 0.1in;
|
||||||
|
}
|
||||||
|
.exp-header {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.exp-meta {
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
.exp-desc {
|
||||||
|
margin-left: 0.2in;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="title">Rafael Polo</div>
|
||||||
|
<div class="subtitle">Senior Software Engineer — Platform, Data & AI</div>
|
||||||
|
|
||||||
|
<div class="personalinline">
|
||||||
|
<span>Basel, 4055, Switzerland</span>
|
||||||
|
<span>rafael@extrapolo.com</span>
|
||||||
|
<span>linkedin.com/in/rafaelpolo</span>
|
||||||
|
<span>extrapolo.com</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="personalinline">
|
||||||
|
<span>Portuguese (EU) · Brazilian — no work permit required</span>
|
||||||
|
<span>20+ years building production systems across Switzerland, Germany, Greece and Brazil</span>
|
||||||
|
<span>Fluent English · German B1 TELC (3 yrs Berlin + 3 yrs Basel) · EU citizen, immediately available</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h1>P R O F E S S I O N A L E X P E R I E N C E</h1>
|
||||||
|
|
||||||
|
<div class="experience-item">
|
||||||
|
<div class="exp-header">Swisscom</div>
|
||||||
|
<div class="exp-meta">DevOps Engineer · Zürich, Switzerland</div>
|
||||||
|
<div class="exp-desc">Modernization of mission-critical internal platforms & CI/CD pipelines for high-availability Telecom infrastructure.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="experience-item">
|
||||||
|
<div class="exp-header">EDGE Empower®</div>
|
||||||
|
<div class="exp-meta">Senior Technical Lead · Baar, Switzerland · 2024 – 2025</div>
|
||||||
|
<div class="exp-desc">Built a PayTool cutting processing time by 98%; reduced CI/CD pipeline from 14 → 4 minutes; managed 3 developers and a UX designer for a SaaS product.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="experience-item">
|
||||||
|
<div class="exp-header">ExtraPolo — Independent Consulting</div>
|
||||||
|
<div class="exp-meta">Software Engineer & Consultant · Rio de Janeiro · Berlin · Athens · Lisbon · 2010 – 2022</div>
|
||||||
|
<div class="exp-desc">40+ projects across civic tech, data journalism, open-source platforms and interactive installations for public and private clients.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="experience-item">
|
||||||
|
<div class="exp-header">Moedax Digital Services</div>
|
||||||
|
<div class="exp-meta">Founder & CTO · Lisbon · Rio de Janeiro · 2018 – 2021</div>
|
||||||
|
<div class="exp-desc">Fintech startup connecting CBDC digital currency with KYC compliance automation; built data pipelines and banking integrations in Python.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="experience-item">
|
||||||
|
<div class="exp-header">GFOSS.eu / Code + Create</div>
|
||||||
|
<div class="exp-meta">Web Programming Teacher · Athens, Greece · 2017 – 2018</div>
|
||||||
|
<div class="exp-desc">Taught web programming (HTML, CSS, jQuery) to refugee and local youth through the EU-funded Code + Create initiative.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="experience-item">
|
||||||
|
<div class="exp-header">Tactical Tech Collective</div>
|
||||||
|
<div class="exp-meta">Full-Stack Web Architect · Berlin, Germany · Dec 2014 – Jun 2016</div>
|
||||||
|
<div class="exp-desc">Built investigative data tools and platforms under the Exposing the Invisible team (Ruby on Rails, data visualisation).</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="experience-item">
|
||||||
|
<div class="exp-header">Trommsdorff + Drüner</div>
|
||||||
|
<div class="exp-meta">Full-Stack Engineer · Berlin, Germany</div>
|
||||||
|
<div class="exp-desc">Innovation team — Business Intelligence dashboards for marketing consultancy clients.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="experience-item">
|
||||||
|
<div class="exp-header">Cortex Intelligence</div>
|
||||||
|
<div class="exp-meta">Developer · Rio de Janeiro, Brazil</div>
|
||||||
|
<div class="exp-desc">Competitive Intelligence and Data Mining R&D team.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h1>E D U C A T I O N</h1>
|
||||||
|
<div class="key-value"><span class="key">Postgraduate in Data Science for Finance</span><span class="value">NOVA IMS Information Management School, Lisbon — 2021</span></div>
|
||||||
|
<div class="key-value"><span class="key">Master in Information Systems</span><span class="value">PPGI/UNIRIO — Knowledge Representation & Reasoning — 2010–2012</span></div>
|
||||||
|
<div class="key-value"><span class="key">Bachelor in Computer Science</span><span class="value">Metodista Bennett, Rio de Janeiro — 2004–2009</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h1>L A N G U A G E S</h1>
|
||||||
|
<div class="key-value"><span class="key">Portuguese</span><span class="value">native</span></div>
|
||||||
|
<div class="key-value"><span class="key">English</span><span class="value">full professional</span></div>
|
||||||
|
<div class="key-value"><span class="key">Spanish</span><span class="value">full professional</span></div>
|
||||||
|
<div class="key-value"><span class="key">German</span><span class="value">B1 — TELC certified · 3 yrs Berlin + 3 yrs Basel</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h1>C E R T I F I C A T I O N S</h1>
|
||||||
|
<ul>
|
||||||
|
<li>Swiss Federal Act on Data Protection (nFADP)</li>
|
||||||
|
<li>Gen AI Foundational Models — NLP & LLMs</li>
|
||||||
|
<li>AI Agents using RAG and LangChain</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h1>T E C H S K I L L S</h1>
|
||||||
|
<div class="key-value"><span class="key">Languages & Frameworks</span><span class="value">Python · Ruby on Rails · TypeScript · React · Java</span></div>
|
||||||
|
<div class="key-value"><span class="key">DevOps & Infra</span><span class="value">Docker · Kubernetes · CI/CD · AWS · Linux · Git</span></div>
|
||||||
|
<div class="key-value"><span class="key">Data & AI</span><span class="value">SQL · ETL · Data Engineering · RAG · LangChain · LLM integrations · Data Science</span></div>
|
||||||
|
<div class="key-value"><span class="key">Architecture</span><span class="value">REST APIs · MVC · OOP · UML · System Design · Web Crawling & Scraping</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="small">
|
||||||
|
References available upon request · CV updated May 2026
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Rafael Polo - CV</title>
|
||||||
|
<style>
|
||||||
|
@page {
|
||||||
|
size: 8.2681in 11.6929in;
|
||||||
|
margin: 0.5in;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: Liberation Sans, Carlito, Calibri, Georgia, sans-serif;
|
||||||
|
font-size: 11pt;
|
||||||
|
line-height: 1.3;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
display: table;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.left-col {
|
||||||
|
display: table-cell;
|
||||||
|
width: 65%;
|
||||||
|
padding-right: 0.2in;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.right-col {
|
||||||
|
display: table-cell;
|
||||||
|
width: 35%;
|
||||||
|
vertical-align: top;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.photo {
|
||||||
|
width: 2in;
|
||||||
|
height: auto;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: 24pt;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0.05in;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
font-size: 12pt;
|
||||||
|
font-style: italic;
|
||||||
|
margin-bottom: 0.2in;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: 12pt;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0.15in 0 0.08in 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05in;
|
||||||
|
border-bottom: 1px solid #999;
|
||||||
|
padding-bottom: 0.03in;
|
||||||
|
}
|
||||||
|
.personal-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
.personal-table td {
|
||||||
|
padding: 0.02in 0;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.personal-table .label {
|
||||||
|
font-weight: bold;
|
||||||
|
width: 1.2in;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.personal-table .header {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 12pt;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05in;
|
||||||
|
padding-bottom: 0.05in;
|
||||||
|
}
|
||||||
|
.section-header {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 11pt;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03in;
|
||||||
|
margin-top: 0.15in;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
margin: 0.05in 0;
|
||||||
|
padding-left: 0.15in;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
margin-bottom: 0.04in;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
.job-title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.job-meta {
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
.job-company {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.job-desc {
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 10pt;
|
||||||
|
margin-left: 0.1in;
|
||||||
|
}
|
||||||
|
.edu-item {
|
||||||
|
font-size: 10pt;
|
||||||
|
margin-bottom: 0.03in;
|
||||||
|
}
|
||||||
|
.edu-school {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.skill-category {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 10pt;
|
||||||
|
margin-top: 0.05in;
|
||||||
|
}
|
||||||
|
.skill-list {
|
||||||
|
font-size: 10pt;
|
||||||
|
margin-left: 0.1in;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
font-size: 9pt;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 0.3in;
|
||||||
|
border-top: 1px solid #ccc;
|
||||||
|
padding-top: 0.1in;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="left-col">
|
||||||
|
<p class="title">Rafael Polo</p>
|
||||||
|
<p class="subtitle">Senior Software Engineer — Platform, Data & AI</p>
|
||||||
|
|
||||||
|
<table class="personal-table">
|
||||||
|
<tr>
|
||||||
|
<td class="header" colspan="2">Personal</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Address:</td>
|
||||||
|
<td>Basel, 4055, Switzerland</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Email:</td>
|
||||||
|
<td>rafael@extrapolo.com</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">LinkedIn:</td>
|
||||||
|
<td>linkedin.com/in/rafaelpolo</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Web:</td>
|
||||||
|
<td>extrapolo.com</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Citizenship:</td>
|
||||||
|
<td>Portuguese (EU) · Brazilian — no work permit required</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Languages:</td>
|
||||||
|
<td>Fluent English · German B1 TELC (3 yrs Berlin + 3 yrs Basel) · EU citizen, immediately available</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>Formal Education</h3>
|
||||||
|
<ul>
|
||||||
|
<li><em><strong>Postgraduate in Data Science for Finance</strong></em> – 2021<br>
|
||||||
|
<span class="edu-school">NOVA IMS Information Management School, Lisbon</span></li>
|
||||||
|
<li><em><strong>Master in Information Systems</strong></em> – 2010–2012<br>
|
||||||
|
<span class="edu-school">Research Area: Knowledge Representation & Reasoning, PPGI/UNIRIO</span></li>
|
||||||
|
<li><em><strong>Bachelor in Computer Science</strong></em> – 2004–2009<br>
|
||||||
|
<span class="edu-school">Metodista Bennett, Rio de Janeiro</span></li>
|
||||||
|
<li>Thesis: Agile Systems Development for Web 2.0 with Ruby on Rails</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Relevant Professional Experience</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Swisscom</strong> – <em>2024–2025</em> – <em>Zürich, Switzerland</em>
|
||||||
|
<p class="job-desc"><strong>DevOps Engineer</strong> developing platforms for Telecommunication Operations.</p></li>
|
||||||
|
<li><strong>EDGE Empower®</strong> – <em>2024–2025</em> – <em>Baar, Switzerland</em>
|
||||||
|
<p class="job-desc"><strong>Senior Technical Lead</strong> managing projects and external UX & Dev Teams for a SaaS Company.</p></li>
|
||||||
|
<li><strong>ExtraPolo — Independent Consulting</strong> – <em>2010–2022</em> – <em>Rio de Janeiro · Berlin · Athens · Lisbon</em>
|
||||||
|
<p class="job-desc">For more than 10 years, worked as an independent Software Developer and IT Consultant, developing dozens of Projects for public and private contractors in Brazil, Germany, Greece and Switzerland.</p></li>
|
||||||
|
<li><strong>Moedax Digital Services</strong> – <em>2018–2021</em> – <em>Lisbon and Rio de Janeiro</em>
|
||||||
|
<p class="job-desc"><strong>Founder & CTO</strong> – connected Brazilian CBDC with KYC processes and automation</p></li>
|
||||||
|
<li><strong>GFOSS.eu / Code + Create</strong> – <em>2017–2018</em> – <em>Athens, Greece</em>
|
||||||
|
<p class="job-desc"><strong>Web Programming Teacher</strong> – taught web programming to refugee and local youth</p></li>
|
||||||
|
<li><strong>Tactical Tech Collective</strong> – <em>2014–2016</em> – <em>Berlin, Germany</em>
|
||||||
|
<p class="job-desc"><strong>Full-Stack Web Architect</strong> on an NGO under the Exposing the Invisible Team.</p></li>
|
||||||
|
<li><strong>Trommsdorf + Drüner</strong> – <em>2013–2014</em> – <em>Berlin, Germany</em>
|
||||||
|
<p class="job-desc"><strong>Full-stack Engineer</strong> – Innovation Team developing Business Intelligence dashboards.</p></li>
|
||||||
|
<li><strong>Cortex Intelligence</strong> – <em>2007–2008</em> – <em>Rio de Janeiro, Brazil</em>
|
||||||
|
<p class="job-desc"><strong>Developer</strong> in the Competitive Intelligence and Data Mining Team.</p></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Certifications</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Swiss Federal Act on Data Protection (nFADP)</li>
|
||||||
|
<li>Gen AI Foundational Models — NLP & LLMs</li>
|
||||||
|
<li>AI Agents using RAG and LangChain</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Relevant Tech Skills</h3>
|
||||||
|
<ul>
|
||||||
|
<li>System Analysis & Modeling with UML</li>
|
||||||
|
<li>Quick Proof-of-Concepts development, deploy and management.</li>
|
||||||
|
<li>Large Language Models, Gen AI, LangChain integration and Processes Automation</li>
|
||||||
|
<li>MVC System Architecture, Design Patterns & Object-Oriented Programming</li>
|
||||||
|
<li>Data Engineering, Data Science, Relational Databases & SQL</li>
|
||||||
|
<li>Experience with Blockchain, Smart Contracts and P2P systems</li>
|
||||||
|
<li>Distributing and Scaling Processes with Message Queues</li>
|
||||||
|
<li>Developing Web Crawlers & Bots for Data Mining & ETL</li>
|
||||||
|
<li>Linux Systems, VPS, DevOps, SSH & Deploy with Continuous Integration processes</li>
|
||||||
|
<li>Projects mostly developed in Python, Ruby on Rails, Django, Java</li>
|
||||||
|
<li>Full Stack Developer & Front-end Web Design with HTML, CSS and JavaScript frameworks</li>
|
||||||
|
<li>CI/CD, Docker, Kubernetes and GitHub workflows</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Natural Languages</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Native Brazilian Portuguese</li>
|
||||||
|
<li>Advanced Spoken Spanish</li>
|
||||||
|
<li>A2 level Greek – <em>3 years in Athens</em></li>
|
||||||
|
<li>B1 level German / TELC proof – <em>3 years in Berlin</em> and 3 in Switzerland</li>
|
||||||
|
<li>Fluent English – <em>13 years in between international teams</em></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
References available upon request · CV updated May 2026
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right-col">
|
||||||
|
<!-- Photo placeholder - remove if not needed -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,130 @@
|
|||||||
|
Rafael Polo
|
||||||
|
|
||||||
|
Basel, 4055, Switzerland
|
||||||
|
|
||||||
|
rafael@extrapolo.com
|
||||||
|
|
||||||
|
linkedin.com/in/rafaelpolo
|
||||||
|
|
||||||
|
extrapolo.com
|
||||||
|
|
||||||
|
Senior Software Engineer — Platform, Data & AI
|
||||||
|
|
||||||
|
Portuguese (EU) · Brazilian — no work permit required
|
||||||
|
|
||||||
|
20+ years building production systems across Switzerland, Germany, Greece and Brazil. Specialised in platform
|
||||||
|
|
||||||
|
engineering, data pipelines and AI/LLM integrations. Fluent English · German B1 TELC (3 yrs Berlin + 3 yrs Basel) · EU
|
||||||
|
|
||||||
|
citizen, immediately available.
|
||||||
|
|
||||||
|
P R O F E S S I O N A L E X P E R I E N C E
|
||||||
|
|
||||||
|
Swisscom
|
||||||
|
DevOps Engineer · Zürich, Switzerland
|
||||||
|
|
||||||
|
Modernization of mission-critical internal platforms & CI/CD pipelines for high-availability Telecom infrastructure.
|
||||||
|
|
||||||
|
EDGE Empower®
|
||||||
|
|
||||||
|
Senior Technical Lead · Baar, Switzerland
|
||||||
|
|
||||||
|
2024 – 2025
|
||||||
|
|
||||||
|
Mar 2023 – Mar 2024
|
||||||
|
|
||||||
|
Built a PayTool cutting processing time by 98%; reduced CI/CD pipeline from 14 → 4 minutes; managed 3 developers and a UX
|
||||||
|
|
||||||
|
designer for a SaaS product.
|
||||||
|
|
||||||
|
ExtraPolo — Independent Consulting
|
||||||
|
Software Engineer & Consultant · Rio de Janeiro · Berlin · Athens · Lisbon
|
||||||
|
|
||||||
|
2010 – 2022
|
||||||
|
|
||||||
|
40+ projects across civic tech, data journalism, open-source platforms and interactive installations for public and private clients.
|
||||||
|
|
||||||
|
Moedax Digital Services
|
||||||
|
|
||||||
|
Founder & CTO · Lisbon · Rio de Janeiro
|
||||||
|
|
||||||
|
2018 – 2021
|
||||||
|
|
||||||
|
Fintech startup connecting CBDC digital currency with KYC compliance automation; built data pipelines and banking integrations in
|
||||||
|
Python.
|
||||||
|
|
||||||
|
GFOSS.eu / Code + Create
|
||||||
|
Web Programming Teacher · Athens, Greece
|
||||||
|
|
||||||
|
2017 – 2018
|
||||||
|
|
||||||
|
Taught web programming (HTML, CSS, jQuery) to refugee and local youth through the EU-funded Code + Create initiative.
|
||||||
|
|
||||||
|
Tactical Tech Collective
|
||||||
|
Full-Stack Web Architect · Berlin, Germany
|
||||||
|
|
||||||
|
Dec 2014 – Jun 2016
|
||||||
|
|
||||||
|
Built investigative data tools and platforms under the Exposing the Invisible team (Ruby on Rails, data visualisation).
|
||||||
|
|
||||||
|
Trommsdorff + Drüner
|
||||||
|
|
||||||
|
Full-Stack Engineer · Berlin, Germany
|
||||||
|
|
||||||
|
Innovation team — Business Intelligence dashboards for marketing consultancy clients.
|
||||||
|
|
||||||
|
Cortex Intelligence
|
||||||
|
Developer · Rio de Janeiro, Brazil
|
||||||
|
|
||||||
|
Competitive Intelligence and Data Mining R&D team.
|
||||||
|
|
||||||
|
E D U C A T I O N
|
||||||
|
|
||||||
|
L A N G U A G E S
|
||||||
|
|
||||||
|
Dec 2013 – Jan 2014
|
||||||
|
|
||||||
|
2008 – 2010
|
||||||
|
|
||||||
|
Postgraduate in Data Science for Finance
|
||||||
|
NOVA IMS Information Management School, Lisbon — 2021
|
||||||
|
|
||||||
|
Master in Information Systems
|
||||||
|
PPGI/UNIRIO — Knowledge Representation & Reasoning —
|
||||||
|
2010–2012
|
||||||
|
|
||||||
|
Bachelor in Computer Science
|
||||||
|
Metodista Bennett, Rio de Janeiro — 2004–2009
|
||||||
|
|
||||||
|
Portuguese — native
|
||||||
|
|
||||||
|
English — full professional
|
||||||
|
|
||||||
|
Spanish — full professional
|
||||||
|
|
||||||
|
German B1 — TELC certified · 3 yrs Berlin + 3 yrs Basel
|
||||||
|
|
||||||
|
C E R T I F I C A T I O N S
|
||||||
|
|
||||||
|
Swiss Federal Act on Data Protection (nFADP)
|
||||||
|
|
||||||
|
Gen AI Foundational Models — NLP & LLMs
|
||||||
|
|
||||||
|
AI Agents using RAG and LangChain
|
||||||
|
|
||||||
|
T E C H S K I L L S
|
||||||
|
|
||||||
|
Languages & Frameworks · Python · Ruby on Rails ·
|
||||||
|
TypeScript · React · Java
|
||||||
|
|
||||||
|
DevOps & Infra · Docker · Kubernetes · CI/CD · AWS · Linux ·
|
||||||
|
Git
|
||||||
|
|
||||||
|
Data & AI · SQL · ETL · Data Engineering · RAG · LangChain ·
|
||||||
|
LLM integrations · Data Science
|
||||||
|
|
||||||
|
Architecture · REST APIs · MVC · OOP · UML · System Design ·
|
||||||
|
Web Crawling & Scraping
|
||||||
|
|
||||||
|
References available upon request · CV updated May 2026
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+65
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
import { searches } from "./config.ts";
|
||||||
|
import { fetchJobs, SOFTWARE_TITLE_RE } from "./fetch.ts";
|
||||||
|
|
||||||
|
const CSV = "./jobs.csv";
|
||||||
|
|
||||||
|
const existingIds = new Set<string>();
|
||||||
|
try {
|
||||||
|
const text = await Bun.file(CSV).text();
|
||||||
|
for (const line of text.split("\n").slice(1)) {
|
||||||
|
const id = line.match(/\/jobs\/view\/(\d+)\//)?.[1];
|
||||||
|
if (id) existingIds.add(id);
|
||||||
|
}
|
||||||
|
} catch { /* first run */ }
|
||||||
|
|
||||||
|
const missed: { title: string; company: string; link: string }[] = [];
|
||||||
|
const seenIds = new Set<string>();
|
||||||
|
|
||||||
|
for (const config of searches) {
|
||||||
|
console.log(`\n[${config.label}]`);
|
||||||
|
const all = await fetchJobs(config, { raw: true });
|
||||||
|
for (const job of all) {
|
||||||
|
if (!seenIds.has(job.id) && !existingIds.has(job.id) && !SOFTWARE_TITLE_RE.test(job.title)) {
|
||||||
|
seenIds.add(job.id);
|
||||||
|
missed.push({ title: job.title, company: job.company, link: job.link });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missed.length === 0) {
|
||||||
|
console.log("\nNo missed jobs — all new titles pass the filter.");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n── ${missed.length} new jobs currently filtered out ──\n`);
|
||||||
|
for (const j of missed) {
|
||||||
|
console.log(` ${j.title}`);
|
||||||
|
console.log(` ${j.company} ${j.link}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Word frequency analysis to suggest keywords
|
||||||
|
const STOP = new Set([
|
||||||
|
"and", "or", "of", "the", "a", "an", "in", "for", "to", "at", "by",
|
||||||
|
"with", "on", "as", "is", "its", "be", "are", "was", "were", "our",
|
||||||
|
"your", "this", "that", "we", "you", "all", "new", "use",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const wordCounts = new Map<string, number>();
|
||||||
|
for (const { title } of missed) {
|
||||||
|
const words = title.match(/\b[a-zA-Z]{3,}\b/g) ?? [];
|
||||||
|
for (const w of words) {
|
||||||
|
const key = w.toLowerCase();
|
||||||
|
if (!STOP.has(key)) wordCounts.set(key, (wordCounts.get(key) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const suggestions = [...wordCounts.entries()]
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.filter(([, n]) => n >= 1)
|
||||||
|
.slice(0, 20);
|
||||||
|
|
||||||
|
console.log("\n── Suggested keywords to consider ──\n");
|
||||||
|
for (const [word, count] of suggestions) {
|
||||||
|
console.log(` "${word}" — ${count} title${count > 1 ? "s" : ""}`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
import { KEYWORDS } from "./config.ts";
|
||||||
|
import { fetchJobs, isEnglishContent } from "./fetch.ts";
|
||||||
|
import type { SearchConfig } from "./config.ts";
|
||||||
|
|
||||||
|
const searches: SearchConfig[] = [
|
||||||
|
{ label: "Zürich", keywords: KEYWORDS, location: "Zurich, Switzerland", workType: ["onsite", "hybrid"], timePosted: "month" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const results: { id: string; title: string; company: string; search: string }[] = [];
|
||||||
|
|
||||||
|
for (const config of searches) {
|
||||||
|
console.log(`\n[${config.label}]`);
|
||||||
|
const jobs = await fetchJobs(config);
|
||||||
|
for (const job of jobs) {
|
||||||
|
if (!seen.has(job.id)) {
|
||||||
|
seen.add(job.id);
|
||||||
|
results.push({ id: job.id, title: job.title, company: job.company, search: job.search });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n── ${results.length} jobs after title filter — checking content language ──\n`);
|
||||||
|
|
||||||
|
const english: typeof results = [];
|
||||||
|
for (const job of results) {
|
||||||
|
process.stdout.write(` ${job.company} — ${job.title} … `);
|
||||||
|
const ok = await isEnglishContent(job.id);
|
||||||
|
console.log(ok ? "en" : "dropped");
|
||||||
|
if (ok) english.push(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n── ${english.length} English jobs in Zürich ──\n`);
|
||||||
|
for (const [i, j] of english.entries()) {
|
||||||
|
console.log(` ${i + 1}. [${j.search}] ${j.title} — ${j.company}`);
|
||||||
|
console.log(` https://www.linkedin.com/jobs/view/${j.id}/`);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user