diff --git a/clawedin-openclawjobfindagent/.env.example b/clawedin-openclawjobfindagent/.env.example
new file mode 100644
index 0000000..18181bb
--- /dev/null
+++ b/clawedin-openclawjobfindagent/.env.example
@@ -0,0 +1,4 @@
+# HrFlow.ai credentials (get from hackathon organizers)
+HRFLOW_SECRET_KEY=your-hrflow-secret-key
+HRFLOW_API_KEY=your-api-key-here
+HRFLOW_SOURCE_KEY=your-source-key-here
diff --git a/clawedin-openclawjobfindagent/README.md b/clawedin-openclawjobfindagent/README.md
new file mode 100644
index 0000000..c86e2e5
--- /dev/null
+++ b/clawedin-openclawjobfindagent/README.md
@@ -0,0 +1,152 @@
+# ClawedIn — AI Job Finding Agent
+
+> Upload your CV. Let AI find your next job.
+
+ClawedIn is an AI-powered job matching agent built for the HrFlow.ai Hackathon. It parses a candidate's CV using HrFlow.ai, generates relevant job roles using OpenClaw (local LLM), searches real job listings via Brave Search, and ranks them by profile match.
+
+---
+
+## What It Does
+
+1. **CV Parsing** — Upload a PDF resume. HrFlow.ai extracts skills, experience, and profile data.
+2. **Job Role Generation** — OpenClaw LLM analyses extracted skills and suggests 10 relevant job titles.
+3. **Job Search** — Brave Search API finds real job listings from LinkedIn, Indeed, Welcome to the Jungle, APEC, Glassdoor, and Talent.io.
+4. **AI Ranking** — OpenClaw ranks the listings by match quality and explains why each job fits the candidate.
+5. **Results Dashboard** — Candidates see ranked jobs with match scores, reasons, and direct apply links.
+
+---
+
+## Architecture
+```
+Streamlit UI (port 8501)
+ ↓ Upload CV
+FastAPI Skill (port 8001)
+ ↓ HrFlow Parsing API → extract skills
+ ↓ Brave Search API → find real job listings
+ ↓ OpenClaw LLM → rank jobs by relevance
+ ↓ return ranked results
+OpenClaw Gateway (port 18789)
+ ↓ qwen3:1.7b via Ollama (local LLM)
+```
+
+---
+
+## HrFlow.ai APIs Used
+
+| API | Purpose |
+|-----|---------|
+| `profile/parsing/add_file` | Parse uploaded CV PDF and extract skills, experience, profile data |
+| `job/searching` | Search indexed job listings |
+| `job/scoring` | Score jobs against candidate profile |
+
+---
+
+## Prerequisites
+
+- Python 3.12+
+- [Ollama](https://ollama.ai) with `qwen3:1.7b` model
+- [OpenClaw](https://openclaw.ai) gateway
+- Brave Search API key (free tier available at [brave.com/search/api](https://brave.com/search/api))
+- HrFlow.ai API credentials
+
+---
+
+## Installation
+
+### 1. Clone the repo
+```bash
+git clone https://github.com/YOUR_USERNAME/app-store-public.git
+cd app-store-public/clawedin
+```
+
+### 2. Install FastAPI dependencies
+```bash
+cd hrflow-skill
+pip install poetry
+poetry install
+```
+
+### 3. Install Streamlit dependencies
+```bash
+cd ../Streamlit
+pip install -r requirements.txt
+```
+
+### 4. Install Ollama and pull model
+```bash
+# Install Ollama from https://ollama.ai
+ollama pull qwen3:1.7b
+```
+
+### 5. Install and configure OpenClaw
+```bash
+npm install -g openclaw
+openclaw configure
+openclaw config set plugins.entries.brave.enabled true
+openclaw config set plugins.entries.brave.config.webSearch.apiKey "YOUR_BRAVE_KEY"
+openclaw config set tools.web.search.provider "brave"
+```
+
+---
+
+## Configuration
+
+Copy `.env.example` to `.env` in the `hrflow-skill` folder:
+```bash
+cp hrflow-skill/.env.example hrflow-skill/.env
+```
+
+Fill in your credentials:
+```env
+HRFLOW_API_KEY=your_hrflow_api_key
+HRFLOW_USER_EMAIL=your_hrflow_email
+HRFLOW_SOURCE_KEY=your_source_key
+HRFLOW_BOARD_KEY=your_board_key
+BRAVE_API_KEY=your_brave_api_key
+GATEWAY_TOKEN=your_openclaw_gateway_token
+```
+
+---
+
+## Running the App
+
+Open 3 terminals:
+
+**Terminal 1 — OpenClaw Gateway**
+```bash
+openclaw gateway --port 18789
+```
+
+**Terminal 2 — FastAPI Skill**
+```bash
+cd hrflow-skill
+poetry run uvicorn main:app --host 0.0.0.0 --port 8001 --reload
+```
+
+**Terminal 3 — Streamlit UI**
+```bash
+cd Streamlit
+streamlit run app.py --server.address 0.0.0.0 --server.port 8501
+```
+
+Open your browser at `http://localhost:8501`
+
+---
+
+## Stack
+
+| Layer | Technology |
+|-------|-----------|
+| Frontend | Streamlit |
+| Backend | FastAPI (Python) |
+| LLM | qwen3:1.7b via Ollama |
+| AI Agent | OpenClaw 2026.3.24 |
+| Job Search | Brave Search API |
+| CV Parsing | HrFlow.ai Parsing API |
+
+---
+
+## Team
+
+**Team 7 — ClawedIn**
+Built at the HrFlow.ai Hackathon 2026.
diff --git a/clawedin-openclawjobfindagent/Streamlit/app.py b/clawedin-openclawjobfindagent/Streamlit/app.py
new file mode 100644
index 0000000..bc78c07
--- /dev/null
+++ b/clawedin-openclawjobfindagent/Streamlit/app.py
@@ -0,0 +1,89 @@
+import streamlit as st
+from core.styles import apply_design_system
+from core.session import init_session
+from modules import auth, upload, engine, dashboard
+from modules import keywords as keywords_module
+from datetime import datetime
+
+st.set_page_config(
+ page_title="ClawedIn",
+ page_icon="🦞",
+ layout="wide",
+ initial_sidebar_state="expanded"
+)
+apply_design_system()
+init_session()
+
+# Sidebar
+with st.sidebar:
+ st.markdown("""
+
+
🤖
+
ClawedIn
+
Powered by OpenClaw + HrFlow
+
+ """, unsafe_allow_html=True)
+
+ st.divider()
+
+ if st.session_state.logged_in:
+ name = st.session_state.user_data.get("email", "Candidate")
+ st.markdown(f"""
+
+
Logged in as
+
{name}
+
{datetime.now().strftime('%d %b %Y')}
+
+ """, unsafe_allow_html=True)
+
+ st.divider()
+
+ # Progress tracker
+ step = st.session_state.step
+ steps = ["upload", "keywords", "processing", "results"]
+ step_names = ["Upload CV", "Review Profile", "AI Search", "Results"]
+ current = steps.index(step) if step in steps else 0
+
+ st.markdown("PROGRESS
", unsafe_allow_html=True)
+ for i, (s, n) in enumerate(zip(steps, step_names)):
+ if i < current:
+ icon = "✅"
+ color = "#16a34a"
+ elif i == current:
+ icon = "▶️"
+ color = "#2563eb"
+ else:
+ icon = "○"
+ color = "#475569"
+ st.markdown(f"{icon} {n}
", unsafe_allow_html=True)
+
+ st.divider()
+
+ if st.button("🔄 New Search", use_container_width=True):
+ st.session_state.step = "upload"
+ st.session_state.results = []
+ st.session_state.parsed_cv = None
+ st.session_state.uploaded_file = None
+ st.session_state.job_roles = []
+ st.rerun()
+
+ if st.button("Logout", use_container_width=True):
+ st.session_state.logged_in = False
+ st.session_state.step = "upload"
+ st.rerun()
+ else:
+ st.markdown("Log in to activate your AI agent.
", unsafe_allow_html=True)
+
+# Router
+if not st.session_state.logged_in:
+ auth.render()
+else:
+ step = st.session_state.step
+ if step == "upload":
+ upload.render()
+ elif step == "keywords":
+ keywords_module.render()
+ elif step == "processing":
+ engine.render()
+ elif step == "results":
+ dashboard.render()
diff --git a/clawedin-openclawjobfindagent/Streamlit/assets/log.svg b/clawedin-openclawjobfindagent/Streamlit/assets/log.svg
new file mode 100644
index 0000000..d078023
--- /dev/null
+++ b/clawedin-openclawjobfindagent/Streamlit/assets/log.svg
@@ -0,0 +1,31 @@
+
\ No newline at end of file
diff --git a/clawedin-openclawjobfindagent/Streamlit/assets/logo.svg b/clawedin-openclawjobfindagent/Streamlit/assets/logo.svg
new file mode 100644
index 0000000..e1e18c9
--- /dev/null
+++ b/clawedin-openclawjobfindagent/Streamlit/assets/logo.svg
@@ -0,0 +1,34 @@
+
\ No newline at end of file
diff --git a/clawedin-openclawjobfindagent/Streamlit/core/__init__.py b/clawedin-openclawjobfindagent/Streamlit/core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/clawedin-openclawjobfindagent/Streamlit/core/api.py b/clawedin-openclawjobfindagent/Streamlit/core/api.py
new file mode 100644
index 0000000..f6fa915
--- /dev/null
+++ b/clawedin-openclawjobfindagent/Streamlit/core/api.py
@@ -0,0 +1,33 @@
+import requests
+
+FASTAPI_URL = "http://localhost:8001"
+GATEWAY_URL = "http://localhost:18789"
+GATEWAY_TOKEN = "ea28e9b03545f00a79e3d3c857e18fb0df7e83dc8f211b75"
+
+
+def health_check():
+ status = {}
+ try:
+ requests.get(f"{FASTAPI_URL}/health", timeout=2)
+ status["fastapi"] = True
+ except:
+ status["fastapi"] = False
+ try:
+ requests.get(GATEWAY_URL, timeout=2)
+ status["openclaw"] = True
+ except:
+ status["openclaw"] = False
+ return status
+
+
+def parse_and_score(file_bytes, filename, location="Paris"):
+ try:
+ resp = requests.post(
+ f"{FASTAPI_URL}/full-pipeline",
+ files={"file": (filename, file_bytes, "application/pdf")},
+ params={"location": location},
+ timeout=180
+ )
+ return resp.json()
+ except Exception as e:
+ return {"error": str(e), "profile": {}, "results": []}
diff --git a/clawedin-openclawjobfindagent/Streamlit/core/app.py b/clawedin-openclawjobfindagent/Streamlit/core/app.py
new file mode 100644
index 0000000..a6a2303
--- /dev/null
+++ b/clawedin-openclawjobfindagent/Streamlit/core/app.py
@@ -0,0 +1,263 @@
+import streamlit as st
+import time
+import requests
+import base64
+from datetime import datetime
+
+# --- CONFIGURATION & STYLING ---
+st.set_page_config(
+ page_title="AgentJob.ai | AI Career Scout",
+ page_icon="🤖",
+ layout="wide",
+ initial_sidebar_state="expanded"
+)
+
+# Custom CSS for a high-end AI dashboard feel
+st.markdown("""
+
+ """, unsafe_allow_html=True)
+
+# --- BACKEND INTEGRATION HELPERS ---
+API_BASE = "http://localhost:8000" # Your FastAPI URL
+
+def call_backend_api(endpoint, method="GET", data=None, files=None):
+ """Generic helper to talk to FastAPI with error handling."""
+ try:
+ if method == "POST":
+ if files:
+ response = requests.post(f"{API_BASE}{endpoint}", files=files)
+ else:
+ response = requests.post(f"{API_BASE}{endpoint}", json=data)
+ else:
+ response = requests.get(f"{API_BASE}{endpoint}", params=data)
+
+ if response.status_code == 200:
+ return response.json()
+ else:
+ st.error(f"Backend Error: {response.status_code}")
+ return None
+ except Exception as e:
+ # In a real app, don't show raw exception to user, but good for dev
+ return None
+
+# --- SESSION STATE MANAGEMENT ---
+def init_session():
+ if "logged_in" not in st.session_state:
+ st.session_state.logged_in = False
+ if "step" not in st.session_state:
+ st.session_state.step = "welcome"
+ if "user_data" not in st.session_state:
+ st.session_state.user_data = {}
+ if "parsed_cv" not in st.session_state:
+ st.session_state.parsed_cv = None
+ if "results" not in st.session_state:
+ st.session_state.results = []
+
+# --- UI COMPONENTS ---
+
+def sidebar_navigation():
+ with st.sidebar:
+ st.image("https://cdn-icons-png.flaticon.com/512/2103/2103811.png", width=80)
+ st.title("AgentJob.ai")
+ st.markdown("---")
+
+ if st.session_state.logged_in:
+ st.write(f"👤 **User:** {st.session_state.user_data.get('email', 'Candidate')}")
+ st.write(f"📅 **Session:** {datetime.now().strftime('%Y-%m-%d')}")
+
+ if st.button("🔄 New Search"):
+ st.session_state.step = "upload"
+ st.rerun()
+
+ if st.button("🚪 Logout"):
+ st.session_state.logged_in = False
+ st.session_state.step = "welcome"
+ st.rerun()
+ else:
+ st.info("Log in to activate your AI agent.")
+
+def login_screen():
+ col1, col2 = st.columns([1, 1])
+ with col1:
+ st.title("Access your AI Agent")
+ st.write("Log in to securely upload your CV and begin your automated search.")
+ email = st.text_input("Email Address", placeholder="alex@example.com")
+ password = st.text_input("Password", type="password")
+ if st.button("Launch Agent", use_container_width=True):
+ # Simulation of FastAPI Auth
+ st.session_state.logged_in = True
+ st.session_state.user_data = {"email": email}
+ st.session_state.step = "upload"
+ st.rerun()
+ with col2:
+ st.image("https://images.unsplash.com/photo-1551434678-e076c223a692?auto=format&fit=crop&q=80&w=1000", use_container_width=True)
+
+def upload_screen():
+ st.header("Step 1: Profile Initialization")
+ st.write("Upload your resume. Our **Quen-based NLP** will extract your core competencies.")
+
+ uploaded_file = st.file_uploader("Drop your PDF or DOCX here", type=['pdf', 'docx'])
+
+ if uploaded_file:
+ st.success("File received. Ready to parse.")
+ if st.button("Start AI Pipeline", use_container_width=True):
+ st.session_state.step = "processing"
+ st.rerun()
+
+def processing_screen():
+ st.header("Step 2: AI Execution Pipeline")
+
+ # 1. Quen Parsing
+ with st.status("🧠 **Quen AI:** Parsing CV Structure...", expanded=True) as status:
+ st.write("Extracting technical stack...")
+ time.sleep(1.2)
+ st.write("Identifying seniority level: *Senior Full Stack* detected.")
+ time.sleep(1.0)
+ st.write("Mapping location preferences: *Europe/Remote*.")
+ st.session_state.parsed_cv = {"skills": ["Python", "React", "FastAPI"], "level": "Senior"}
+ status.update(label="✅ CV Parsing Complete", state="complete", expanded=False)
+
+ # 2. Live Web Scraper Agent
+ st.subheader("🌐 Active Web Searching")
+ cols = st.columns(3)
+ sources = [
+ {"name": "LinkedIn", "icon": "🔵", "count": 18},
+ {"name": "Glassdoor", "icon": "🟢", "count": 12},
+ {"name": "Welcome to the Jungle", "icon": "🟡", "count": 9}
+ ]
+
+ for i, source in enumerate(sources):
+ with cols[i]:
+ with st.container(border=True):
+ st.write(f"{source['icon']} **{source['name']}**")
+ progress_bar = st.progress(0)
+ for percent in range(101):
+ time.sleep(0.01)
+ progress_bar.progress(percent)
+ st.caption(f"Scanned {source['count']} relevant openings")
+
+ # 3. HR Flow Ranking
+ with st.status("⚖️ **HR Flow:** Ranking Match Relevance...") as status:
+ st.write("Running semantic similarity algorithm...")
+ time.sleep(1.5)
+ st.write("Calculating match scores for 39 jobs...")
+ status.update(label="🎯 Pipeline Execution Complete!", state="complete")
+
+ if st.button("Review My Ranked Matches", type="primary", use_container_width=True):
+ # Mock Results from HR Flow
+ st.session_state.results = [
+ {"title": "Senior AI Software Engineer", "company": "NeuralPath", "score": 98, "loc": "Remote", "source": "LinkedIn", "salary": "€80k - €110k"},
+ {"title": "Full Stack Architect", "company": "GrowthOps", "score": 94, "loc": "Paris / Hybrid", "source": "Welcome to the Jungle", "salary": "€75k - €95k"},
+ {"title": "Engineering Manager (AI)", "company": "Vortex Labs", "score": 87, "loc": "London / Remote", "source": "Glassdoor", "salary": "£90k - £120k"},
+ {"title": "Lead Python Developer", "company": "EcoTech", "score": 82, "loc": "Berlin", "source": "LinkedIn", "salary": "€70k - €85k"},
+ ]
+ st.session_state.step = "results"
+ st.rerun()
+
+def results_screen():
+ st.header("Step 3: Intelligence Insights")
+ st.write(f"We found **{len(st.session_state.results)}** high-probability matches for your profile.")
+
+ # Filter/Sort UI
+ col_f1, col_f2, _ = st.columns([1, 1, 2])
+ with col_f1:
+ st.selectbox("Sort by", ["Match Score (High-Low)", "Newest", "Salary"])
+ with col_f2:
+ st.multiselect("Source Filter", ["LinkedIn", "Glassdoor", "Welcome to the Jungle"])
+
+ st.divider()
+
+ # Job Cards
+ for job in st.session_state.results:
+ source_class = "tag-linkedin" if job['source'] == "LinkedIn" else "tag-jungle" if job['source'] == "Welcome to the Jungle" else "tag-glassdoor"
+
+ st.markdown(f"""
+
+
+
{job['source']}
+
{job['title']}
+
{job['company']} • {job['loc']}
+
{job['salary']}
+
+
+
{job['score']}%
+
Match Score
+
+
+ """, unsafe_allow_html=True)
+
+ with st.expander("View Agent Analysis"):
+ st.write(f"**Why this match?** HR Flow API detected a high semantic overlap between your *{st.session_state.parsed_cv['skills'][0]}* experience and this role's requirements.")
+ st.button(f"Apply on {job['source']}", key=job['title'])
+
+# --- APP ROUTING ---
+init_session()
+sidebar_navigation()
+
+if not st.session_state.logged_in:
+ login_screen()
+else:
+ if st.session_state.step == "upload":
+ upload_screen()
+ elif st.session_state.step == "processing":
+ processing_screen()
+ elif st.session_state.step == "results":
+ results_screen()
\ No newline at end of file
diff --git a/clawedin-openclawjobfindagent/Streamlit/core/session.py b/clawedin-openclawjobfindagent/Streamlit/core/session.py
new file mode 100644
index 0000000..0628cdd
--- /dev/null
+++ b/clawedin-openclawjobfindagent/Streamlit/core/session.py
@@ -0,0 +1,18 @@
+import streamlit as st
+
+def init_session():
+ defaults = {
+ "logged_in": False,
+ "step": "upload",
+ "user_data": {},
+ "parsed_cv": None,
+ "uploaded_file": None,
+ "results": [],
+ "profile_key": "",
+ "search_keywords": "",
+ "search_location": "Paris",
+ "search_freshness": "pm"
+ }
+ for key, val in defaults.items():
+ if key not in st.session_state:
+ st.session_state[key] = val
diff --git a/clawedin-openclawjobfindagent/Streamlit/core/styles.py b/clawedin-openclawjobfindagent/Streamlit/core/styles.py
new file mode 100644
index 0000000..a58c4cf
--- /dev/null
+++ b/clawedin-openclawjobfindagent/Streamlit/core/styles.py
@@ -0,0 +1,140 @@
+import streamlit as st
+
+def apply_design_system():
+ st.markdown("""
+
+ """, unsafe_allow_html=True)
diff --git a/clawedin-openclawjobfindagent/Streamlit/modules/__init__.py b/clawedin-openclawjobfindagent/Streamlit/modules/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/clawedin-openclawjobfindagent/Streamlit/modules/auth.py b/clawedin-openclawjobfindagent/Streamlit/modules/auth.py
new file mode 100644
index 0000000..d3e21e0
--- /dev/null
+++ b/clawedin-openclawjobfindagent/Streamlit/modules/auth.py
@@ -0,0 +1,39 @@
+import streamlit as st
+from pathlib import Path
+
+def render():
+ col1, col2 = st.columns([1, 1])
+
+ with col1:
+ st.markdown("", unsafe_allow_html=True)
+ st.title("Welcome to ClawedIn")
+ st.write("Upload your CV and let our AI agent find the best jobs for you.")
+ email = st.text_input("Email Address", placeholder="alex@example.com")
+ password = st.text_input("Password", type="password")
+ if st.button("🚀 Launch Agent", type="primary", use_container_width=True):
+ if email:
+ st.session_state.logged_in = True
+ st.session_state.user_data = {"email": email}
+ st.session_state.step = "upload"
+ st.rerun()
+ else:
+ st.error("Please enter your email")
+ st.markdown("
", unsafe_allow_html=True)
+
+ with col2:
+ logo_path = Path(__file__).parent.parent / "assets" / "log.svg"
+ if logo_path.exists():
+ svg_content = logo_path.read_text()
+ st.markdown(
+ f"""""",
+ unsafe_allow_html=True
+ )
diff --git a/clawedin-openclawjobfindagent/Streamlit/modules/dashboard.py b/clawedin-openclawjobfindagent/Streamlit/modules/dashboard.py
new file mode 100644
index 0000000..59db50b
--- /dev/null
+++ b/clawedin-openclawjobfindagent/Streamlit/modules/dashboard.py
@@ -0,0 +1,98 @@
+import streamlit as st
+import re
+
+def clean_title(title: str) -> str:
+ title = re.sub(r'^\d+[-—–]\s*', '', title)
+ title = re.sub(r'\([^)]*\)', '', title)
+ title = re.sub(r'\b(20\d{2})\b', '', title)
+ title = re.sub(r'\b(NOW HIRING|URGENT|HIRING|APPLY NOW)\b', '', title, flags=re.IGNORECASE)
+ return " ".join(title.split()).strip()
+
+def render():
+ parsed = st.session_state.get("parsed_cv", {})
+ results = st.session_state.get("results", [])
+
+ # Progress indicator
+ st.markdown("""
+
+ """, unsafe_allow_html=True)
+
+ st.markdown(f"## 🎯 {len(results)} Job Matches Found")
+ st.caption(f"Ranked by OpenClaw AI for **{parsed.get('name','Candidate')}**")
+ st.divider()
+
+ for i, job in enumerate(results):
+ score = job.get("score", 0)
+ title = clean_title(job.get("title", "N/A"))
+ company = job.get("company", "N/A")
+ loc = job.get("loc", "N/A")
+ reason = job.get("reason", "")
+ url = job.get("url", "")
+
+ # Color based on score
+ if score >= 80:
+ badge_color = "#16a34a"
+ elif score >= 60:
+ badge_color = "#2563eb"
+ else:
+ badge_color = "#64748b"
+
+ with st.container(border=True):
+ col1, col2 = st.columns([0.82, 0.18])
+
+ with col1:
+ st.markdown(
+ "OPENCLAW",
+ unsafe_allow_html=True
+ )
+ st.markdown(f"### {title}")
+ st.caption(f"🏢 {company} • 📍 {loc}")
+
+ if reason and reason not in ["Relevant to your profile", "N/A", ""]:
+ st.markdown(
+ f"💡 {reason}
",
+ unsafe_allow_html=True
+ )
+
+ with col2:
+ st.markdown(
+ f""
+ f"
{score}%
"
+ f"
MATCH
"
+ f"
",
+ unsafe_allow_html=True
+ )
+
+ if url and url not in ["N/A", "", "None"]:
+ st.link_button(f"🔗 Apply Now → {company}", url)
+
+ st.divider()
+
+ col1, col2 = st.columns(2)
+ with col1:
+ if st.button("🔄 Search Again", use_container_width=True):
+ st.session_state.step = "keywords"
+ st.session_state.results = []
+ st.session_state.job_roles = []
+ st.rerun()
+ with col2:
+ if st.button("📄 New CV Upload", use_container_width=True):
+ st.session_state.step = "upload"
+ st.session_state.results = []
+ st.session_state.parsed_cv = None
+ st.session_state.uploaded_file = None
+ st.session_state.job_roles = []
+ st.rerun()
diff --git a/clawedin-openclawjobfindagent/Streamlit/modules/engine.py b/clawedin-openclawjobfindagent/Streamlit/modules/engine.py
new file mode 100644
index 0000000..7cede7d
--- /dev/null
+++ b/clawedin-openclawjobfindagent/Streamlit/modules/engine.py
@@ -0,0 +1,305 @@
+import streamlit as st
+import requests
+import json
+import urllib.parse
+from core.api import FASTAPI_URL, GATEWAY_URL, GATEWAY_TOKEN
+
+BRAVE_KEY = "BSAuGOZdZUYfp9N3TT9MxiomoRZbVRI"
+
+
+def generate_job_roles(skills: list) -> list:
+ """Ask OpenClaw to suggest relevant job roles from skills."""
+ skills_str = ", ".join(skills[:20])
+ message = f"""/no_think
+Based on these skills: {skills_str}
+
+Generate exactly 5 relevant job role titles this person should apply for.
+Return ONLY a JSON array of 5 strings. Example:
+["Full-Stack Developer", "Data Engineer", "AI Engineer", "Backend Developer", "DevOps Engineer"]
+JSON array only. Nothing else.
+"""
+ try:
+ resp = requests.post(
+ f"{GATEWAY_URL}/v1/chat/completions",
+ headers={
+ "Authorization": f"Bearer {GATEWAY_TOKEN}",
+ "Content-Type": "application/json"
+ },
+ json={
+ "model": "openclaw/default",
+ "messages": [{"role": "user", "content": message}]
+ },
+ timeout=30
+ )
+ content_raw = resp.json()["choices"][0]["message"]["content"]
+ if "" in content_raw:
+ content_raw = content_raw.split("")[-1].strip()
+ content_raw = content_raw.strip()
+ if content_raw.startswith("```"):
+ content_raw = content_raw.split("```")[1]
+ if content_raw.startswith("json"):
+ content_raw = content_raw[4:]
+ roles = json.loads(content_raw.strip())
+ if isinstance(roles, list):
+ print(f"JOB ROLES: {roles}", flush=True)
+ return roles[:5]
+ except Exception as e:
+ print(f"Role generation failed: {e}", flush=True)
+ # Fallback roles
+ return ["Full-Stack Developer", "Backend Developer", "Data Engineer",
+ "AI Engineer", "Software Engineer"]
+
+def search_jobs_brave(keywords: str, location: str = "Paris", freshness: str = "pm") -> list:
+ """Search real jobs via Brave API — fast and reliable."""
+ SKIP = ["reddit", "github", "stackoverflow", "medium", "youtube",
+ "wikipedia", "twitter", "formation", "tutorial", "blog",
+ "forum", "profile", "resume", "course", "reval.site",
+ "internshala", "shine.com", "hireitpeople", "in/",
+ "fastapi.tiangolo", "docs.", "documentation", "pypi.org",
+ "npmjs.com", "readthedocs", "dev.to", "hashnode"]
+
+ all_jobs = []
+
+ # Site-specific queries — tailored for each job platform
+ # Search by skills — "apply now" forces job listings not articles
+ site_queries = [
+ (f"{keywords} offre emploi CDI {location} postuler", "welcometothejungle.com"),
+ (f"{keywords} CDI {location} apply now", "linkedin.com"),
+ (f"{keywords} emploi CDI {location} postuler", "indeed.fr"),
+ (f"{keywords} {location} postuler candidature", "apec.fr"),
+ (f"{keywords} apply now job {location}", "glassdoor.fr"),
+ (f"{keywords} hiring {location}", "talent.io"),
+ ]
+ print(f"SEARCH SKILLS: {keywords} in {location}", flush=True)
+
+ # Freshness only applies when not "any time"
+ freshness_param = f"&freshness={freshness}" if freshness != "py" else ""
+
+ for query, site in site_queries:
+ encoded = urllib.parse.quote(query)
+ print(f"BRAVE: searching {site}", flush=True)
+ try:
+ resp = requests.get(
+ f"https://api.search.brave.com/res/v1/web/search?q={encoded}&count=10{freshness_param}",
+ headers={
+ "Accept": "application/json",
+ "X-Subscription-Token": BRAVE_KEY
+ },
+ timeout=8
+ )
+ data = resp.json()
+ for r in data.get("web", {}).get("results", []):
+ url = r.get("url", "")
+ # Skip LinkedIn profiles — only keep /jobs/ URLs
+ if "linkedin.com" in url and "/jobs/" not in url and "/job/" not in url:
+ continue
+ if any(s in url.lower() for s in SKIP):
+ continue
+ all_jobs.append({
+ "title": r.get("title", "N/A"),
+ "company": r.get("meta_url", {}).get("hostname", site),
+ "url": url,
+ "location": location,
+ "description": r.get("description", "")[:150]
+ })
+ except Exception as e:
+ print(f"Brave {site} failed: {e}", flush=True)
+ continue
+
+ # Deduplicate
+ seen = set()
+ unique = []
+ for j in all_jobs:
+ if j["url"] not in seen:
+ seen.add(j["url"])
+ unique.append(j)
+
+ print(f"BRAVE TOTAL: {len(unique)} jobs found", flush=True)
+ for j in unique[:5]:
+ print(f" - {j['title']} | {j['url'][:70]}", flush=True)
+ return unique[:50]
+
+
+def rank_jobs_openclaw(jobs: list, skills: list, name: str) -> list:
+ """OpenClaw ranks jobs — with fallback scoring."""
+ if not jobs:
+ return []
+
+ jobs_text = "\n".join(
+ f"{i+1}. {j.get('title')} | {j.get('company')} | {j.get('url')}"
+ for i, j in enumerate(jobs[:20])
+ )
+
+ message = f"""/no_think
+Candidate skills: {', '.join(skills[:6])}
+
+Rank ALL these {min(len(jobs),20)} jobs by match quality:
+{jobs_text}
+
+Return ALL jobs as JSON array ranked best to worst:
+[{{"rank":1,"title":"exact","company":"exact","score":85,"reason":"why matches","url":"exact","loc":"Paris","salary":"N/A"}}]
+Every job must appear. JSON array only.
+"""
+ try:
+ resp = requests.post(
+ f"{GATEWAY_URL}/v1/chat/completions",
+ headers={{
+ "Authorization": f"Bearer {{GATEWAY_TOKEN}}",
+ "Content-Type": "application/json"
+ }},
+ json={{
+ "model": "openclaw/default",
+ "messages": [{{"role": "user", "content": message}}]
+ }},
+ timeout=60
+ )
+ content_raw = resp.json()["choices"][0]["message"]["content"]
+ print(f"OPENCLAW RANK RAW: {{content_raw[:300]}}", flush=True)
+
+ if "" in content_raw:
+ content_raw = content_raw.split("")[-1].strip()
+ content_raw = content_raw.strip()
+ if content_raw.startswith("```"):
+ content_raw = content_raw.split("```")[1]
+ if content_raw.startswith("json"):
+ content_raw = content_raw[4:]
+ content_raw = content_raw.strip()
+
+ ranked = json.loads(content_raw)
+ if isinstance(ranked, dict) and "results" in ranked:
+ ranked = ranked["results"]
+ if not isinstance(ranked, list) or len(ranked) < 2:
+ raise ValueError(f"Only {len(ranked) if isinstance(ranked,list) else 0} results")
+
+ # Merge exact URLs + fix scores
+ url_map = {{j.get("title","").lower(): j for j in jobs}}
+ for item in ranked:
+ orig = url_map.get(item.get("title","").lower(), {{}})
+ if not item.get("url") or item["url"] in ["N/A",""]:
+ item["url"] = orig.get("url","")
+ item.setdefault("loc", "Paris")
+ item.setdefault("salary","N/A")
+ if not item.get("score") or item["score"] == 0:
+ item["score"] = max(60, 90 - (item.get("rank",1) * 3))
+ return ranked
+
+ except Exception as e:
+ print(f"OpenClaw rank failed: {{e}} — using keyword fallback", flush=True)
+
+ # Fallback — keyword-based scoring
+ def simple_score(job, skills):
+ title = job.get("title","").lower()
+ desc = job.get("description","").lower()
+ hits = sum(1 for s in skills if s.lower().replace("-"," ") in title or
+ s.lower().replace("-"," ") in desc)
+ return min(95, 60 + (hits * 8))
+
+ scored = []
+ for i, j in enumerate(jobs[:50]):
+ score = simple_score(j, skills)
+ matching = [s for s in skills[:3]
+ if s.lower().replace("-"," ") in j.get("title","").lower()
+ or s.lower().replace("-"," ") in j.get("description","").lower()]
+ scored.append({
+ "rank": i+1,
+ "title": j.get("title","N/A"),
+ "company": j.get("company","N/A"),
+ "score": score,
+ "reason": f"Matches: {', '.join(matching)}" if matching else "Relevant to your profile",
+ "url": j.get("url",""),
+ "loc": j.get("location","Paris"),
+ "salary": "N/A"
+ })
+
+ scored.sort(key=lambda x: x["score"], reverse=True)
+ for i, s in enumerate(scored):
+ s["rank"] = i + 1
+ return scored
+
+
+def render():
+ st.header("Step 3: AI Job Search")
+
+ keywords = st.session_state.get("search_keywords", "")
+ location = st.session_state.get("search_location", "Paris")
+ parsed = st.session_state.get("parsed_cv", {})
+ skills = parsed.get("skills", [])
+ name = parsed.get("name", "Candidate")
+
+ # If results cached — just show button
+ if st.session_state.get("results"):
+ st.success(f"🎯 {len(st.session_state.results)} jobs found and ranked!")
+ with st.container(border=True):
+ st.caption("🔍 Searched for")
+ st.markdown(f"**{keywords}** in **{location}**")
+ if st.button("Review My Ranked Matches →", type="primary", use_container_width=True):
+ st.session_state.step = "results"
+ st.rerun()
+ return
+
+ with st.container(border=True):
+ st.caption("🔍 Searching for")
+ st.markdown(f"**{keywords}** in **{location}**")
+ with st.status("🦞 Searching jobs...", expanded=True) as status_box:
+ import re as _re
+
+ # Step 1 — OpenClaw generates job roles from skills
+ st.write("🤖 OpenClaw analysing your skills...")
+ job_roles = st.session_state.get("job_roles", [])
+ if not job_roles:
+ job_roles = generate_job_roles(skills)
+ st.session_state.job_roles = job_roles
+ clean_roles = [_re.sub(r"\(.*?\)", "", r).strip() for r in job_roles[:5]]
+ st.write(f"✅ Roles: {", ".join(clean_roles)}")
+
+ # Step 2 — Brave searches each role
+ st.write("🌐 Brave Search scanning job sites...")
+ all_jobs = []
+ freshness = st.session_state.get("search_freshness", "py")
+ for role in clean_roles:
+ role_jobs = search_jobs_brave(f"{role} jobs apply now", location, freshness=freshness)
+ all_jobs.extend(role_jobs)
+ seen = set()
+ jobs = []
+ for j in all_jobs:
+ if j["url"] not in seen:
+ seen.add(j["url"])
+ jobs.append(j)
+ print(f"TOTAL JOBS: {len(jobs)}", flush=True)
+
+ if not jobs:
+ status_box.update(label="❌ No jobs found", state="error")
+ st.error("No jobs found. Try different keywords.")
+ if st.button("← Try different keywords"):
+ st.session_state.step = "keywords"
+ st.rerun()
+ return
+
+ st.write(f"✅ Found {len(jobs)} listings")
+ st.write("🦞 OpenClaw ranking by relevance...")
+ ranked = rank_jobs_openclaw(jobs, skills, name)
+ status_box.update(
+ label=f"✅ {len(ranked)} jobs ranked by OpenClaw!",
+ state="complete",
+ expanded=False
+ )
+
+ normalised = []
+ for job in ranked:
+ normalised.append({
+ "title": job.get("title","N/A"),
+ "company": job.get("company","N/A"),
+ "score": job.get("score",0),
+ "loc": job.get("loc","N/A"),
+ "salary": job.get("salary","N/A"),
+ "source": "OpenClaw",
+ "reason": job.get("reason",""),
+ "url": job.get("url","")
+ })
+ st.session_state.results = normalised
+
+ if normalised:
+ st.success(f"🎯 {len(normalised)} jobs found!")
+ if st.button("Review My Ranked Matches →", type="primary", use_container_width=True):
+ st.session_state.step = "results"
+ st.rerun()
diff --git a/clawedin-openclawjobfindagent/Streamlit/modules/keywords.py b/clawedin-openclawjobfindagent/Streamlit/modules/keywords.py
new file mode 100644
index 0000000..b5d6644
--- /dev/null
+++ b/clawedin-openclawjobfindagent/Streamlit/modules/keywords.py
@@ -0,0 +1,96 @@
+import streamlit as st
+
+def render():
+ parsed = st.session_state.get("parsed_cv", {})
+ name = parsed.get("name", "Candidate")
+ skills = parsed.get("skills", [])
+ summary = parsed.get("summary", "")
+
+ st.header(f"Step 2: Review Your Profile")
+
+ # Profile card
+ with st.container(border=True):
+ col1, col2 = st.columns([0.3, 0.7])
+ with col1:
+ st.markdown(f"**👤 {name}**")
+ with col2:
+ if summary:
+ st.caption(summary[:200])
+
+ st.divider()
+
+ # All skills from HrFlow — show as chips
+ st.subheader("🧠 Skills extracted by HrFlow AI")
+ if skills:
+ chips_html = " ".join([
+ f"{s}"
+ for s in skills
+ ])
+ st.markdown(chips_html, unsafe_allow_html=True)
+ else:
+ st.warning("No skills extracted — try a different CV format")
+
+ st.divider()
+
+ # Search keyword editor — use top skills directly from HrFlow
+ st.subheader("🔎 Search keywords")
+ st.caption("Generated from your parsed CV. Edit to refine.")
+
+ # Use job roles if available, otherwise fall back to skills
+ job_roles = st.session_state.get("job_roles", [])
+ if job_roles:
+ default_keywords = ", ".join(job_roles[:5])
+ else:
+ default_keywords = " ".join(skills[:5]) if skills else ""
+
+ keywords = st.text_input(
+ "Search keywords",
+ value=default_keywords,
+ placeholder="e.g. Python Data Engineer FastAPI",
+ help="Edit these keywords to refine your job search"
+ )
+
+ location = st.text_input(
+ "Location",
+ value="Paris",
+ placeholder="e.g. Paris, London, Remote"
+ )
+
+ st.divider()
+
+ # Timeline filter
+ st.subheader("📅 Job posting recency")
+ freshness_map = {
+ "24 hours": "pd",
+ "1 week": "pw",
+ "1 month": "pm",
+ "Any time": "py"
+ }
+ selected = st.radio(
+ "Recency",
+ options=list(freshness_map.keys()),
+ index=3,
+ horizontal=True,
+ label_visibility="collapsed"
+ )
+
+ st.divider()
+
+ col1, col2 = st.columns(2)
+ with col1:
+ if st.button("← Back", use_container_width=True):
+ st.session_state.step = "upload"
+ st.session_state.results = []
+ st.rerun()
+ with col2:
+ if st.button("🚀 Start Searching", type="primary", use_container_width=True):
+ if not keywords.strip():
+ st.error("Please enter keywords")
+ return
+ st.session_state.search_keywords = keywords
+ st.session_state.search_location = location
+ st.session_state.search_freshness = freshness_map[selected]
+ st.session_state.results = []
+ st.session_state.step = "processing"
+ st.rerun()
diff --git a/clawedin-openclawjobfindagent/Streamlit/modules/upload.py b/clawedin-openclawjobfindagent/Streamlit/modules/upload.py
new file mode 100644
index 0000000..f0abcaf
--- /dev/null
+++ b/clawedin-openclawjobfindagent/Streamlit/modules/upload.py
@@ -0,0 +1,83 @@
+import streamlit as st
+from core.api import FASTAPI_URL
+import requests
+
+def render():
+ st.header("Step 1: Upload Your CV")
+ st.write("Upload your resume. AI will extract your skills and suggest search keywords.")
+
+ uploaded_file = st.file_uploader("Drop your PDF here", type=["pdf"])
+
+ if uploaded_file:
+ st.success(f"✅ {uploaded_file.name} received")
+
+ if st.button("🔍 Parse CV", use_container_width=True, type="primary"):
+ with st.spinner("Parsing CV with HrFlow AI..."):
+ try:
+ resp = requests.post(
+ f"{FASTAPI_URL}/parse-cv",
+ files={"file": (uploaded_file.name, uploaded_file.getvalue(), "application/pdf")},
+ timeout=60
+ )
+ parsed = resp.json()
+ except Exception as e:
+ st.error(f"Parse failed: {e}")
+ return
+
+ if parsed.get("code") == 400 or parsed.get("error"):
+ st.error(f"HrFlow error: {parsed}")
+ return
+
+ # Store in session
+ skills = parsed.get("skills", [])
+
+ # Ask OpenClaw to generate job roles from skills
+ job_roles = []
+ try:
+ import json
+ skills_str = ", ".join(skills[:30])
+ message = f"""/no_think
+A candidate has these skills: {skills_str}
+Suggest 10 specific job titles they should apply for.
+Return ONLY a JSON array of 10 strings. JSON only.
+"""
+ resp = requests.post(
+ "http://localhost:18789/v1/chat/completions",
+ headers={
+ "Authorization": "Bearer ea28e9b03545f00a79e3d3c857e18fb0df7e83dc8f211b75",
+ "Content-Type": "application/json"
+ },
+ json={
+ "model": "openclaw/default",
+ "messages": [{"role": "user", "content": message}]
+ },
+ timeout=30
+ )
+ raw = resp.json()["choices"][0]["message"]["content"]
+ if "" in raw:
+ raw = raw.split("")[-1].strip()
+ raw = raw.strip()
+ if raw.startswith("```"):
+ raw = raw.split("```")[1]
+ if raw.startswith("json"):
+ raw = raw[4:]
+ job_roles = json.loads(raw.strip())
+ if not isinstance(job_roles, list):
+ job_roles = []
+ except Exception as e:
+ print(f"Role generation failed: {e}")
+ job_roles = []
+
+ st.session_state.uploaded_file = {
+ "bytes": uploaded_file.getvalue(),
+ "name": uploaded_file.name
+ }
+ st.session_state.parsed_cv = {
+ "name": parsed.get("name", "Candidate"),
+ "skills": skills,
+ "summary": parsed.get("summary", "")
+ }
+ st.session_state.profile_key = parsed.get("profile_key", "")
+ st.session_state.job_roles = job_roles
+ st.session_state.step = "keywords"
+ st.rerun()
diff --git a/clawedin-openclawjobfindagent/Streamlit/requirements.txt b/clawedin-openclawjobfindagent/Streamlit/requirements.txt
new file mode 100644
index 0000000..af874b5
--- /dev/null
+++ b/clawedin-openclawjobfindagent/Streamlit/requirements.txt
@@ -0,0 +1,7 @@
+streamlit
+pandas
+numpy
+plotly
+spacy
+scikit-learn
+pypdf
\ No newline at end of file
diff --git a/clawedin-openclawjobfindagent/Streamlit/views.py/login.py b/clawedin-openclawjobfindagent/Streamlit/views.py/login.py
new file mode 100644
index 0000000..e69de29
diff --git a/clawedin-openclawjobfindagent/Streamlit/views.py/processing.py b/clawedin-openclawjobfindagent/Streamlit/views.py/processing.py
new file mode 100644
index 0000000..e69de29
diff --git a/clawedin-openclawjobfindagent/Streamlit/views.py/results.py b/clawedin-openclawjobfindagent/Streamlit/views.py/results.py
new file mode 100644
index 0000000..e69de29
diff --git a/clawedin-openclawjobfindagent/Streamlit/views.py/upload.py b/clawedin-openclawjobfindagent/Streamlit/views.py/upload.py
new file mode 100644
index 0000000..e69de29
diff --git a/clawedin-openclawjobfindagent/app.json b/clawedin-openclawjobfindagent/app.json
new file mode 100644
index 0000000..dde3d5b
--- /dev/null
+++ b/clawedin-openclawjobfindagent/app.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "../../schemas/app.schema.json",
+ "name": "OpenClaw Job Finding Agent",
+ "description": "AI-powered job matching agent that parses a candidate's CV and uses HrFlow.ai APIs to search, score, and rank the most relevant job opportunities, orchestrated by OpenClaw with a local \n LLM.",
+ "credentials": {
+ "source_keys": [],
+ "board_keys": [],
+ "algorithm_key": ""
+ },
+ "settings": {
+ "team_name": "7",
+ "theme_color": "#5B5BE1",
+ "custom_filters": [],
+ "filters": []
+ }
+}
diff --git a/clawedin-openclawjobfindagent/assets/.gitkeep b/clawedin-openclawjobfindagent/assets/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/clawedin-openclawjobfindagent/assets/logo.svg b/clawedin-openclawjobfindagent/assets/logo.svg
new file mode 100644
index 0000000..e1e18c9
--- /dev/null
+++ b/clawedin-openclawjobfindagent/assets/logo.svg
@@ -0,0 +1,34 @@
+
\ No newline at end of file
diff --git a/clawedin-openclawjobfindagent/hrflow-skill/README.md b/clawedin-openclawjobfindagent/hrflow-skill/README.md
new file mode 100644
index 0000000..e69de29
diff --git a/clawedin-openclawjobfindagent/hrflow-skill/main.py b/clawedin-openclawjobfindagent/hrflow-skill/main.py
new file mode 100644
index 0000000..c0d10fa
--- /dev/null
+++ b/clawedin-openclawjobfindagent/hrflow-skill/main.py
@@ -0,0 +1,358 @@
+import os
+import requests
+import json
+from fastapi import FastAPI, UploadFile, File, HTTPException
+from fastapi.middleware.cors import CORSMiddleware
+from hrflow import Hrflow
+from dotenv import load_dotenv
+
+load_dotenv()
+
+app = FastAPI(title="HrFlow Skill")
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_methods=["*"],
+ allow_headers=["*"]
+)
+
+client = Hrflow(
+ api_secret=os.getenv("HRFLOW_API_KEY"),
+ api_user=os.getenv("HRFLOW_USER_EMAIL")
+)
+
+SOURCE_KEY = os.getenv("HRFLOW_SOURCE_KEY")
+BOARD_KEY = os.getenv("HRFLOW_BOARD_KEY")
+GATEWAY_URL = "http://localhost:18789"
+GATEWAY_TOKEN = "ea28e9b03545f00a79e3d3c857e18fb0df7e83dc8f211b75"
+ASSETS_DIR = os.path.expanduser("~/7-openclaw-job-finding-agent/assets")
+
+os.makedirs(ASSETS_DIR, exist_ok=True)
+
+
+def call_openclaw(message: str, timeout: int = 60) -> str:
+ resp = requests.post(
+ f"{GATEWAY_URL}/v1/chat/completions",
+ headers={
+ "Authorization": f"Bearer {GATEWAY_TOKEN}",
+ "Content-Type": "application/json"
+ },
+ json={
+ "model": "openclaw/default",
+ "messages": [{"role": "user", "content": message}]
+ },
+ timeout=timeout
+ )
+ content = resp.json()["choices"][0]["message"]["content"]
+ if "" in content:
+ content = content.split("")[-1].strip()
+ content = content.strip()
+ if content.startswith("```"):
+ content = content.split("```")[1]
+ if content.startswith("json"):
+ content = content[4:]
+ return content.strip()
+
+
+def parse_json_safe(text: str):
+ try:
+ return json.loads(text)
+ except:
+ return None
+
+
+@app.get("/health")
+def health():
+ return {"status": "ok"}
+
+
+@app.post("/parse-cv")
+async def parse_cv(file: UploadFile = File(...)):
+ """Save PDF to assets folder then parse via HrFlow."""
+
+ # Save file to assets
+ save_path = os.path.join(ASSETS_DIR, file.filename)
+ content = await file.read()
+ with open(save_path, "wb") as f:
+ f.write(content)
+ print(f"CV saved to: {save_path}")
+
+ # Parse via HrFlow
+ response = client.profile.parsing.add_file(
+ source_key=SOURCE_KEY,
+ profile_file=open(save_path, "rb"),
+ profile_content_type=file.content_type,
+ sync_parsing=1
+ )
+
+ print(f"HrFlow parse response code: {response.get('code')}")
+ print(f"HrFlow parse response: {json.dumps(response, indent=2)[:500]}")
+
+ if response.get("code") not in [200, 201]:
+ raise HTTPException(status_code=400, detail=response)
+
+ profile = response["data"]["profile"]
+
+ # Filter skills — keep only meaningful technical skills
+ GENERIC = {"advanced","basic","intermediate","general","good","strong",
+ "various","multiple","excellent","solid","proven","experience",
+ "ai","api","aws","skills","ability","knowledge","understanding",
+ "working","using","including","within","across","through"}
+ raw_skills = [s["name"] for s in profile.get("skills", [])]
+
+ # Step 1 — remove generic words
+ filtered = [s for s in raw_skills
+ if s.lower() not in GENERIC and len(s) > 3]
+
+ # Step 2 — remove subsets (e.g. remove "airflow" if "apache airflow" exists)
+ deduped = []
+ for skill in filtered:
+ is_subset = any(
+ skill.lower() != other.lower() and skill.lower() in other.lower()
+ for other in filtered
+ )
+ if not is_subset:
+ deduped.append(skill)
+
+ # Step 3 — sort by length (more specific first)
+ deduped.sort(key=lambda x: len(x), reverse=True)
+
+ # Step 4 — hyphenate multi-word skills
+ tech_skills = [s.replace(" ", "-") for s in deduped]
+
+ print(f"RAW SKILLS: {raw_skills[:10]}")
+ print(f"TECH SKILLS: {tech_skills[:10]}")
+
+ return {
+ "profile_key": profile["key"],
+ "name": profile.get("info", {}).get("full_name", "Candidate"),
+ "skills": tech_skills,
+ "summary": profile.get("info", {}).get("summary", ""),
+ "saved_path": save_path
+ }
+
+
+def search_jobs_brave(keywords: str, location: str = "Paris") -> list:
+ """Search jobs directly via Brave API — reliable, no LLM needed."""
+ import urllib.parse
+ # Simple targeted query — Brave handles job results well
+ query = f"{keywords} emploi CDI offre {location} 2026"
+ print(f"BRAVE QUERY: {query}")
+ encoded = urllib.parse.quote(query)
+
+ brave_key = os.getenv("BRAVE_API_KEY", "BSAuGOZdZUYfp9N3TT9MxiomoRZbVRI")
+
+ resp = requests.get(
+ f"https://api.search.brave.com/res/v1/web/search?q={encoded}&count=10",
+ headers={
+ "Accept": "application/json",
+ "X-Subscription-Token": brave_key
+ },
+ timeout=10
+ )
+
+ data = resp.json()
+ jobs = []
+
+ # Extract from web results
+ for r in data.get("web", {}).get("results", []):
+ jobs.append({
+ "title": r.get("title", "N/A"),
+ "company": r.get("meta_url", {}).get("hostname", "N/A"),
+ "url": r.get("url", ""),
+ "location": location,
+ "description": r.get("description", "")[:100]
+ })
+
+ # Also extract from FAQ results
+ for r in data.get("faq", {}).get("results", []):
+ if r.get("url") and r.get("url") not in [j["url"] for j in jobs]:
+ jobs.append({
+ "title": r.get("title", "N/A"),
+ "company": r.get("meta_url", {}).get("hostname", "N/A"),
+ "url": r.get("url", ""),
+ "location": location,
+ "description": r.get("answer", "")[:100]
+ })
+
+ print(f"BRAVE SEARCH: found {len(jobs)} results for '{query}'")
+ return jobs[:10]
+
+
+@app.post("/search-jobs-ai")
+def search_jobs_ai(payload: dict):
+ keywords = payload.get("keywords", "")
+ location = payload.get("location", "Paris")
+
+ message = f"""/no_think
+Use web_search to find job listings for: {keywords} in {location} France.
+
+Search ONLY these job sites:
+- welcometothejungle.com
+- linkedin.com/jobs
+- indeed.fr
+- glassdoor.fr
+- apec.fr
+
+Return ONLY a JSON array of 10 real job listings.
+Each item must have:
+- title: exact job title
+- company: hiring company name (not the job site)
+- url: direct link to the job posting
+- location: city in France
+- description: one sentence about the role
+
+Rules:
+- ONLY include real job postings with direct apply links
+- NO blog posts, NO tutorials, NO Reddit, NO GitHub
+- JSON array only, nothing else
+"""
+ print(f"\n=== OPENCLAW SEARCH REQUEST: {message[:200]}")
+ raw = call_openclaw(message, timeout=120)
+ print(f"=== OPENCLAW SEARCH RESPONSE: {raw[:300]}")
+ jobs = parse_json_safe(raw)
+
+ if not isinstance(jobs, list):
+ if isinstance(jobs, dict) and "results" in jobs:
+ jobs = jobs["results"]
+ else:
+ jobs = []
+
+ # Post-filter — remove non-job URLs
+ SKIP = ["reddit.com", "github.com", "stackoverflow.com", "medium.com",
+ "youtube.com", "wikipedia.org", "twitter.com", "formation",
+ "tutorial", "blog", "forum", "profile", "resume", "course"]
+
+ filtered = [
+ j for j in jobs
+ if j.get("url")
+ and not any(s in j.get("url","").lower() for s in SKIP)
+ and not any(s in j.get("title","").lower() for s in ["tutorial","course","formation","training"])
+ ]
+
+ print(f"Jobs after filter: {len(filtered)}/{len(jobs)}")
+ return {"jobs": filtered}
+
+
+@app.post("/rank-jobs-ai")
+def rank_jobs_ai(payload: dict):
+ jobs = payload.get("jobs", [])
+ skills = payload.get("skills", [])
+ name = payload.get("name", "Candidate")
+
+ if not jobs:
+ return {"results": []}
+
+ jobs_text = "\n".join(
+ f"{i+1}. {j.get('title')} at {j.get('company')} — {j.get('description','')}"
+ for i, j in enumerate(jobs[:10])
+ )
+
+ message = f"""/no_think
+Candidate: {name}
+Skills: {', '.join(skills[:8])}
+
+Jobs found:
+{jobs_text}
+
+Rank these jobs by match quality for the candidate.
+Return ONLY a JSON array ordered best-to-worst.
+Each item must have EXACTLY these fields:
+- rank: number 1-10
+- title: EXACT title from the list above
+- company: EXACT company from the list above
+- score: match percentage 0-100
+- reason: one sentence why it matches (max 12 words)
+- url: COPY EXACT url from the original list - do not change it
+- loc: COPY EXACT location from original list
+- salary: N/A
+
+IMPORTANT: Copy title, company, url, loc EXACTLY as given. Do not invent new values.
+Return only the JSON array, nothing else.
+"""
+ print(f"\n=== OPENCLAW RANK REQUEST sent for {len(jobs)} jobs")
+ raw = call_openclaw(message, timeout=120)
+ print(f"=== OPENCLAW RANK RESPONSE: {raw[:300]}")
+ ranked = parse_json_safe(raw)
+
+ if not isinstance(ranked, list):
+ if isinstance(ranked, dict) and "results" in ranked:
+ ranked = ranked["results"]
+ else:
+ ranked = [
+ {
+ "rank": i + 1,
+ "title": j.get("title", "N/A"),
+ "company": j.get("company", "N/A"),
+ "score": 80 - (i * 5),
+ "reason": f"Matches your {skills[0] if skills else 'background'}",
+ "url": j.get("url", ""),
+ "loc": j.get("location", "N/A"),
+ "salary": "N/A"
+ }
+ for i, j in enumerate(jobs[:10])
+ ]
+
+ # Merge url/location from original if missing
+ url_map = {j.get("title", ""): j for j in jobs}
+ for item in ranked:
+ orig = url_map.get(item.get("title", ""), {})
+ if not item.get("url"):
+ item["url"] = orig.get("url", "")
+ if not item.get("loc"):
+ item["loc"] = orig.get("location", "N/A")
+ item.setdefault("salary", "N/A")
+
+ return {"results": ranked}
+
+
+@app.post("/full-pipeline")
+async def full_pipeline(file: UploadFile = File(...), location: str = "Paris"):
+ """
+ Full pipeline:
+ 1. Save CV to assets/
+ 2. Parse CV via HrFlow → extract skills
+ 3. OpenClaw searches web for matching jobs
+ 4. OpenClaw ranks jobs by relevance
+ 5. Return ranked results
+ """
+
+ # Step 1 + 2 — Save and parse CV
+ parsed = await parse_cv(file)
+ skills = parsed["skills"][:8]
+ # Use meaningful technical skills only
+ GENERIC = {"advanced","basic","intermediate","general","good","strong",
+ "various","multiple","excellent","solid","proven","experience"}
+ job_skills = [s for s in skills if s.lower() not in GENERIC and len(s) > 3]
+ # Take top 3 skills max — shorter query = better results
+ keywords = " ".join(job_skills[:3]) + " developer"
+ print(f"FILTERED KEYWORDS: {keywords}")
+ print(f"\n=== PARSED SKILLS: {skills}")
+ print(f"=== SEARCH KEYWORDS: {keywords}")
+
+ # Step 3 — OpenClaw searches web using web_search tool
+ print(f"\n=== USER SEARCH KEYWORDS: {keywords}")
+ print(f"=== LOCATION: {location}")
+ search_resp = search_jobs_ai({"keywords": keywords, "location": location})
+ jobs = search_resp.get("jobs", [])
+ print(f"=== Jobs from OpenClaw: {len(jobs)}")
+ for j in jobs[:3]:
+ print(f" - {j.get('title')} | {j.get('url','')[:60]}")
+
+ # Step 4 — OpenClaw ranks by relevance
+ rank_resp = rank_jobs_ai({
+ "jobs": jobs,
+ "skills": skills,
+ "name": parsed["name"]
+ })
+ results = rank_resp.get("results", [])
+
+ return {
+ "profile": {
+ "name": parsed["name"],
+ "skills": skills,
+ "summary": parsed.get("summary", "")
+ },
+ "results": results,
+ "_source": "hrflow-parse + openclaw-search + openclaw-rank"
+ }
diff --git a/clawedin-openclawjobfindagent/hrflow-skill/poetry.lock b/clawedin-openclawjobfindagent/hrflow-skill/poetry.lock
new file mode 100644
index 0000000..b6c1a2f
--- /dev/null
+++ b/clawedin-openclawjobfindagent/hrflow-skill/poetry.lock
@@ -0,0 +1,504 @@
+# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand.
+
+[[package]]
+name = "anyio"
+version = "3.7.1"
+description = "High level compatibility layer for multiple asynchronous event loop implementations"
+optional = false
+python-versions = ">=3.7"
+groups = ["main"]
+files = [
+ {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"},
+ {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"},
+]
+
+[package.dependencies]
+idna = ">=2.8"
+sniffio = ">=1.1"
+
+[package.extras]
+doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"]
+test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4) ; python_version < \"3.8\"", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17) ; python_version < \"3.12\" and platform_python_implementation == \"CPython\" and platform_system != \"Windows\""]
+trio = ["trio (<0.22)"]
+
+[[package]]
+name = "certifi"
+version = "2026.2.25"
+description = "Python package for providing Mozilla's CA Bundle."
+optional = false
+python-versions = ">=3.7"
+groups = ["main"]
+files = [
+ {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"},
+ {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"},
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.6"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+optional = false
+python-versions = ">=3.7"
+groups = ["main"]
+files = [
+ {file = "charset_normalizer-3.4.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-win32.whl", hash = "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:659a1e1b500fac8f2779dd9e1570464e012f43e580371470b45277a27baa7532"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f61aa92e4aad0be58eb6eb4e0c21acf32cf8065f4b2cae5665da756c4ceef982"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f50498891691e0864dc3da965f340fada0771f6142a378083dc4608f4ea513e2"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf625105bb9eef28a56a943fec8c8a98aeb80e7d7db99bd3c388137e6eb2d237"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2bd9d128ef93637a5d7a6af25363cf5dec3fa21cf80e68055aad627f280e8afa"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:d08ec48f0a1c48d75d0356cea971921848fb620fdeba805b28f937e90691209f"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ed80ff870ca6de33f4d953fda4d55654b9a2b340ff39ab32fa3adbcd718f264"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f98059e4fcd3e3e4e2d632b7cf81c2faae96c43c60b569e9c621468082f1d104"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:ab30e5e3e706e3063bc6de96b118688cb10396b70bb9864a430f67df98c61ecc"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:d5f5d1e9def3405f60e3ca8232d56f35c98fb7bf581efcc60051ebf53cb8b611"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:461598cd852bfa5a61b09cae2b1c02e2efcd166ee5516e243d540ac24bfa68a7"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:71be7e0e01753a89cf024abf7ecb6bca2c81738ead80d43004d9b5e3f1244e64"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df01808ee470038c3f8dc4f48620df7225c49c2d6639e38f96e6d6ac6e6f7b0e"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-win32.whl", hash = "sha256:69dd852c2f0ad631b8b60cfbe25a28c0058a894de5abb566619c205ce0550eae"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-win_amd64.whl", hash = "sha256:517ad0e93394ac532745129ceabdf2696b609ec9f87863d337140317ebce1c14"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31215157227939b4fb3d740cd23fe27be0439afef67b785a1eb78a3ae69cba9e"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecbbd45615a6885fe3240eb9db73b9e62518b611850fdf8ab08bd56de7ad2b17"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c45a03a4c69820a399f1dda9e1d8fbf3562eda46e7720458180302021b08f778"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e8aeb10fcbe92767f0fa69ad5a72deca50d0dca07fbde97848997d778a50c9fe"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54fae94be3d75f3e573c9a1b5402dc593de19377013c9a0e4285e3d402dd3a2a"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:2f7fdd9b6e6c529d6a2501a2d36b240109e78a8ceaef5687cfcfa2bbe671d297"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1d02209e06550bdaef34af58e041ad71b88e624f5d825519da3a3308e22687"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8bc5f0687d796c05b1e28ab0d38a50e6309906ee09375dd3aff6a9c09dd6e8f4"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ee4ec14bc1680d6b0afab9aea2ef27e26d2024f18b24a2d7155a52b60da7e833"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d1a2ee9c1499fc8f86f4521f27a973c914b211ffa87322f4ee33bb35392da2c5"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:48696db7f18afb80a068821504296eb0787d9ce239b91ca15059d1d3eaacf13b"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4f41da960b196ea355357285ad1316a00099f22d0929fe168343b99b254729c9"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:802168e03fba8bbc5ce0d866d589e4b1ca751d06edee69f7f3a19c5a9fe6b597"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-win32.whl", hash = "sha256:8761ac29b6c81574724322a554605608a9960769ea83d2c73e396f3df896ad54"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-win_amd64.whl", hash = "sha256:1cf0a70018692f85172348fe06d3a4b63f94ecb055e13a00c644d368eb82e5b8"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-win_arm64.whl", hash = "sha256:3516bbb8d42169de9e61b8520cbeeeb716f12f4ecfe3fd30a9919aa16c806ca8"},
+ {file = "charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69"},
+ {file = "charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6"},
+]
+
+[[package]]
+name = "click"
+version = "8.3.1"
+description = "Composable command line interface toolkit"
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"},
+ {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+groups = ["main"]
+markers = "platform_system == \"Windows\""
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "et-xmlfile"
+version = "2.0.0"
+description = "An implementation of lxml.xmlfile for the standard library"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa"},
+ {file = "et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54"},
+]
+
+[[package]]
+name = "fastapi"
+version = "0.103.2"
+description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
+optional = false
+python-versions = ">=3.7"
+groups = ["main"]
+files = [
+ {file = "fastapi-0.103.2-py3-none-any.whl", hash = "sha256:3270de872f0fe9ec809d4bd3d4d890c6d5cc7b9611d721d6438f9dacc8c4ef2e"},
+ {file = "fastapi-0.103.2.tar.gz", hash = "sha256:75a11f6bfb8fc4d2bec0bd710c2d5f2829659c0e8c0afd5560fdda6ce25ec653"},
+]
+
+[package.dependencies]
+anyio = ">=3.7.1,<4.0.0"
+pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
+starlette = ">=0.27.0,<0.28.0"
+typing-extensions = ">=4.5.0"
+
+[package.extras]
+all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
+ {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
+]
+
+[[package]]
+name = "hrflow"
+version = "4.2.0"
+description = "Python hrflow.ai API package"
+optional = false
+python-versions = "<4.0.0,>=3.8.1"
+groups = ["main"]
+files = [
+ {file = "hrflow-4.2.0-py3-none-any.whl", hash = "sha256:339a76acaead17b8b4ca7cf4572ad0efb28c40441bceb667a6fe2800686ece33"},
+ {file = "hrflow-4.2.0.tar.gz", hash = "sha256:cdd8d643c818e3f5db2adcd67783ac10e7bc343699be0814627dd4bcf49760fc"},
+]
+
+[package.dependencies]
+openpyxl = ">=3.1.2,<4.0.0"
+pydantic = ">=1.10.8,<2.0.0"
+requests = ">=2.31.0,<3.0.0"
+tqdm = ">=4.66.2,<5.0.0"
+
+[[package]]
+name = "idna"
+version = "3.11"
+description = "Internationalized Domain Names in Applications (IDNA)"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"},
+ {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"},
+]
+
+[package.extras]
+all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
+
+[[package]]
+name = "openpyxl"
+version = "3.1.5"
+description = "A Python library to read/write Excel 2010 xlsx/xlsm files"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2"},
+ {file = "openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050"},
+]
+
+[package.dependencies]
+et-xmlfile = "*"
+
+[[package]]
+name = "pydantic"
+version = "1.10.26"
+description = "Data validation and settings management using python type hints"
+optional = false
+python-versions = ">=3.7"
+groups = ["main"]
+files = [
+ {file = "pydantic-1.10.26-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f7ae36fa0ecef8d39884120f212e16c06bb096a38f523421278e2f39c1784546"},
+ {file = "pydantic-1.10.26-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d95a76cf503f0f72ed7812a91de948440b2bf564269975738a4751e4fadeb572"},
+ {file = "pydantic-1.10.26-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a943ce8e00ad708ed06a1d9df5b4fd28f5635a003b82a4908ece6f24c0b18464"},
+ {file = "pydantic-1.10.26-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:465ad8edb29b15c10b779b16431fe8e77c380098badf6db367b7a1d3e572cf53"},
+ {file = "pydantic-1.10.26-cp310-cp310-win_amd64.whl", hash = "sha256:80e6be6272839c8a7641d26ad569ab77772809dd78f91d0068dc0fc97f071945"},
+ {file = "pydantic-1.10.26-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:116233e53889bcc536f617e38c1b8337d7fa9c280f0fd7a4045947515a785637"},
+ {file = "pydantic-1.10.26-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c3cfdd361addb6eb64ccd26ac356ad6514cee06a61ab26b27e16b5ed53108f77"},
+ {file = "pydantic-1.10.26-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e4451951a9a93bf9a90576f3e25240b47ee49ab5236adccb8eff6ac943adf0f"},
+ {file = "pydantic-1.10.26-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9858ed44c6bea5f29ffe95308db9e62060791c877766c67dd5f55d072c8612b5"},
+ {file = "pydantic-1.10.26-cp311-cp311-win_amd64.whl", hash = "sha256:ac1089f723e2106ebde434377d31239e00870a7563245072968e5af5cc4d33df"},
+ {file = "pydantic-1.10.26-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:468d5b9cacfcaadc76ed0a4645354ab6f263ec01a63fb6d05630ea1df6ae453f"},
+ {file = "pydantic-1.10.26-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2c1b0b914be31671000ca25cf7ea17fcaaa68cfeadf6924529c5c5aa24b7ab1f"},
+ {file = "pydantic-1.10.26-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15b13b9f8ba8867095769e1156e0d7fbafa1f65b898dd40fd1c02e34430973cb"},
+ {file = "pydantic-1.10.26-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad7025ca324ae263d4313998e25078dcaec5f9ed0392c06dedb57e053cc8086b"},
+ {file = "pydantic-1.10.26-cp312-cp312-win_amd64.whl", hash = "sha256:4482b299874dabb88a6c3759e3d85c6557c407c3b586891f7d808d8a38b66b9c"},
+ {file = "pydantic-1.10.26-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ae7913bb40a96c87e3d3f6fe4e918ef53bf181583de4e71824360a9b11aef1c"},
+ {file = "pydantic-1.10.26-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8154c13f58d4de5d3a856bb6c909c7370f41fb876a5952a503af6b975265f4ba"},
+ {file = "pydantic-1.10.26-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f8af0507bf6118b054a9765fb2e402f18a8b70c964f420d95b525eb711122d62"},
+ {file = "pydantic-1.10.26-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dcb5a7318fb43189fde6af6f21ac7149c4bcbcfffc54bc87b5becddc46084847"},
+ {file = "pydantic-1.10.26-cp313-cp313-win_amd64.whl", hash = "sha256:71cde228bc0600cf8619f0ee62db050d1880dcc477eba0e90b23011b4ee0f314"},
+ {file = "pydantic-1.10.26-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6b40730cc81d53d515dc0b8bb5c9b43fadb9bed46de4a3c03bd95e8571616dba"},
+ {file = "pydantic-1.10.26-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c3bbb9c0eecdf599e4db9b372fa9cc55be12e80a0d9c6d307950a39050cb0e37"},
+ {file = "pydantic-1.10.26-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc2e3fe7bc4993626ef6b6fa855defafa1d6f8996aa1caef2deb83c5ac4d043a"},
+ {file = "pydantic-1.10.26-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:36d9e46b588aaeb1dcd2409fa4c467fe0b331f3cc9f227b03a7a00643704e962"},
+ {file = "pydantic-1.10.26-cp314-cp314-win_amd64.whl", hash = "sha256:81ce3c8616d12a7be31b4aadfd3434f78f6b44b75adbfaec2fe1ad4f7f999b8c"},
+ {file = "pydantic-1.10.26-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc5c91a3b3106caf07ac6735ec6efad8ba37b860b9eb569923386debe65039ad"},
+ {file = "pydantic-1.10.26-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dde599e0388e04778480d57f49355c9cc7916de818bf674de5d5429f2feebfb6"},
+ {file = "pydantic-1.10.26-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8be08b5cfe88e58198722861c7aab737c978423c3a27300911767931e5311d0d"},
+ {file = "pydantic-1.10.26-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0141f4bafe5eda539d98c9755128a9ea933654c6ca4306b5059fc87a01a38573"},
+ {file = "pydantic-1.10.26-cp38-cp38-win_amd64.whl", hash = "sha256:eb664305ffca8a9766a8629303bb596607d77eae35bb5f32ff9245984881b638"},
+ {file = "pydantic-1.10.26-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:502b9d30d18a2dfaf81b7302f6ba0e5853474b1c96212449eb4db912cb604b7d"},
+ {file = "pydantic-1.10.26-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0d8f6087bf697dec3bf7ffcd7fe8362674f16519f3151789f33cbe8f1d19fc15"},
+ {file = "pydantic-1.10.26-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dd40a99c358419910c85e6f5d22f9c56684c25b5e7abc40879b3b4a52f34ae90"},
+ {file = "pydantic-1.10.26-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ce3293b86ca9f4125df02ff0a70be91bc7946522467cbd98e7f1493f340616ba"},
+ {file = "pydantic-1.10.26-cp39-cp39-win_amd64.whl", hash = "sha256:1a4e3062b71ab1d5df339ba12c48f9ed5817c5de6cb92a961dd5c64bb32e7b96"},
+ {file = "pydantic-1.10.26-py3-none-any.whl", hash = "sha256:c43ad70dc3ce7787543d563792426a16fd7895e14be4b194b5665e36459dd917"},
+ {file = "pydantic-1.10.26.tar.gz", hash = "sha256:8c6aa39b494c5af092e690127c283d84f363ac36017106a9e66cb33a22ac412e"},
+]
+
+[package.dependencies]
+typing-extensions = ">=4.2.0"
+
+[package.extras]
+dotenv = ["python-dotenv (>=0.10.4)"]
+email = ["email-validator (>=1.0.3)"]
+
+[[package]]
+name = "python-dotenv"
+version = "1.2.2"
+description = "Read key-value pairs from a .env file and set them as environment variables"
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"},
+ {file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"},
+]
+
+[package.extras]
+cli = ["click (>=5.0)"]
+
+[[package]]
+name = "python-multipart"
+version = "0.0.22"
+description = "A streaming multipart parser for Python"
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155"},
+ {file = "python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58"},
+]
+
+[[package]]
+name = "requests"
+version = "2.33.0"
+description = "Python HTTP for Humans."
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b"},
+ {file = "requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652"},
+]
+
+[package.dependencies]
+certifi = ">=2023.5.7"
+charset_normalizer = ">=2,<4"
+idna = ">=2.5,<4"
+urllib3 = ">=1.26,<3"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)"]
+test = ["PySocks (>=1.5.6,!=1.5.7)", "pytest (>=3)", "pytest-cov", "pytest-httpbin (==2.1.0)", "pytest-mock", "pytest-xdist"]
+use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+description = "Sniff out which async library your code is running under"
+optional = false
+python-versions = ">=3.7"
+groups = ["main"]
+files = [
+ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
+ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
+]
+
+[[package]]
+name = "starlette"
+version = "0.27.0"
+description = "The little ASGI library that shines."
+optional = false
+python-versions = ">=3.7"
+groups = ["main"]
+files = [
+ {file = "starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91"},
+ {file = "starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75"},
+]
+
+[package.dependencies]
+anyio = ">=3.4.0,<5"
+
+[package.extras]
+full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"]
+
+[[package]]
+name = "tqdm"
+version = "4.67.3"
+description = "Fast, Extensible Progress Meter"
+optional = false
+python-versions = ">=3.7"
+groups = ["main"]
+files = [
+ {file = "tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf"},
+ {file = "tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[package.extras]
+dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"]
+discord = ["requests"]
+notebook = ["ipywidgets (>=6)"]
+slack = ["slack-sdk"]
+telegram = ["requests"]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+description = "Backported and Experimental Type Hints for Python 3.9+"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
+ {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
+]
+
+[[package]]
+name = "urllib3"
+version = "2.6.3"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"},
+ {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"},
+]
+
+[package.extras]
+brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""]
+h2 = ["h2 (>=4,<5)"]
+socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
+zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""]
+
+[[package]]
+name = "uvicorn"
+version = "0.42.0"
+description = "The lightning-fast ASGI server."
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359"},
+ {file = "uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775"},
+]
+
+[package.dependencies]
+click = ">=7.0"
+h11 = ">=0.8"
+
+[package.extras]
+standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.20)", "websockets (>=10.4)"]
+
+[metadata]
+lock-version = "2.1"
+python-versions = "^3.12"
+content-hash = "0de07dbb334d7b4287957a56ee42aad658675bde7b888a90bda59005cecd624e"
diff --git a/clawedin-openclawjobfindagent/hrflow-skill/pyproject.toml b/clawedin-openclawjobfindagent/hrflow-skill/pyproject.toml
new file mode 100644
index 0000000..1df3148
--- /dev/null
+++ b/clawedin-openclawjobfindagent/hrflow-skill/pyproject.toml
@@ -0,0 +1,19 @@
+[tool.poetry]
+name = "hrflow-skill"
+version = "0.1.0"
+description = ""
+authors = ["Jiso Chacko "]
+readme = "README.md"
+package-mode = false
+
+[tool.poetry.dependencies]
+python = "^3.12"
+fastapi = "0.103.2"
+uvicorn = ">=0.42.0,<0.43.0"
+hrflow = ">=4.2.0,<5.0.0"
+python-multipart = ">=0.0.22,<0.0.23"
+python-dotenv = ">=1.2.2,<2.0.0"
+
+[build-system]
+requires = ["poetry-core>=2.0.0,<3.0.0"]
+build-backend = "poetry.core.masonry.api"
diff --git a/clawedin-openclawjobfindagent/hrflow.skill b/clawedin-openclawjobfindagent/hrflow.skill
new file mode 100644
index 0000000..d93b4ca
Binary files /dev/null and b/clawedin-openclawjobfindagent/hrflow.skill differ
diff --git a/clawedin-openclawjobfindagent/hrflow/SKILL.md b/clawedin-openclawjobfindagent/hrflow/SKILL.md
new file mode 100644
index 0000000..65ced08
--- /dev/null
+++ b/clawedin-openclawjobfindagent/hrflow/SKILL.md
@@ -0,0 +1,71 @@
+---
+name: hrflow
+description: Job finding agent skill. Use this skill when a user wants to find jobs matching their CV/resume. Orchestrates the full pipeline: (1) parse CV PDF via HrFlow Parsing API to extract structured profile, (2) generate smart search keywords from the parsed profile using the LLM, (3) search the web for matching job listings, (4) score and rank each job against the candidate profile via HrFlow Scoring API, (5) return ranked jobs with match explanation. Triggers on phrases like "find me jobs", "match my CV", "job search", "upload my resume", "find jobs for me".
+---
+
+# HrFlow Job Finding Agent
+
+## Pipeline Overview
+```
+CV PDF → Parse → Extract Keywords → Web Search → Score & Rank → Present Results
+```
+
+## Step 1: Parse CV
+
+Call the FastAPI skill endpoint to parse the candidate's CV:
+```
+POST http://localhost:8001/parse-cv
+Content-Type: multipart/form-data
+Body: { file: }
+```
+
+Extract from response:
+- `profile_key` — unique ID for this profile (needed for scoring)
+- `skills` — list of technical and soft skills
+- `job_title` — current/target job title
+- `experiences` — work history summary
+
+## Step 2: Generate Search Keywords
+
+Using the parsed profile, generate 3-5 targeted search queries. Focus on:
+- Job title + key technical skills
+- Industry + seniority level
+- Location if mentioned in CV
+
+Example: if profile has "Python, FastAPI, Data Engineer, 3 years" → generate:
+- "Data Engineer Python FastAPI job"
+- "Backend Python Engineer job opening"
+- "Data pipeline engineer FastAPI position"
+
+## Step 3: Web Search for Jobs
+
+Use web search tools to search each query. Target job boards:
+- LinkedIn, Indeed, Glassdoor, Welcome to the Jungle
+- Collect job title, company, description URL, and job description text
+- Aim for 10-15 raw job listings
+
+## Step 4: Score & Rank Jobs
+
+Send collected jobs to the scoring endpoint:
+```
+POST http://localhost:8001/score-jobs
+Content-Type: application/json
+Body: {
+ "profile_key": "",
+ "jobs": [{ "title": "...", "description": "...", "company": "...", "url": "..." }]
+}
+```
+
+## Step 5: Present Results
+
+Return top 10 ranked jobs to the user with:
+- Match score (%)
+- Job title + company
+- Why it matches (2-3 bullet points)
+- Link to apply
+
+## Error Handling
+
+- If parse-cv fails: ask user to re-upload the PDF
+- If web search returns < 5 results: broaden keywords and retry
+- If scoring fails: return unscored results sorted by keyword relevance
diff --git a/clawedin-openclawjobfindagent/hrflow/references/api.md b/clawedin-openclawjobfindagent/hrflow/references/api.md
new file mode 100644
index 0000000..3045abb
--- /dev/null
+++ b/clawedin-openclawjobfindagent/hrflow/references/api.md
@@ -0,0 +1,60 @@
+# HrFlow FastAPI Endpoints
+
+Base URL: http://localhost:8001
+
+## POST /parse-cv
+Parse a CV PDF into a structured profile.
+
+**Request:** multipart/form-data
+- `file`: PDF file
+
+**Response:**
+```json
+{
+ "profile_key": "string",
+ "info": {
+ "full_name": "string",
+ "email": "string",
+ "location": {}
+ },
+ "skills": [{ "name": "string", "value": "string" }],
+ "experiences": [{ "title": "string", "company": "string", "description": "string" }],
+ "educations": [{ "school": "string", "title": "string" }]
+}
+```
+
+## POST /score-jobs
+Score and rank job listings against a parsed profile.
+
+**Request:** application/json
+```json
+{
+ "profile_key": "string",
+ "jobs": [
+ {
+ "title": "string",
+ "company": "string",
+ "description": "string",
+ "url": "string"
+ }
+ ]
+}
+```
+
+**Response:**
+```json
+{
+ "ranked_jobs": [
+ {
+ "title": "string",
+ "company": "string",
+ "url": "string",
+ "score": 0.95,
+ "reasons": ["string"]
+ }
+ ]
+}
+```
+
+## GET /health
+Returns `{"status": "ok"}` if the service is running.