Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,90 +1,59 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
-
from bs4 import BeautifulSoup
|
| 3 |
-
from urllib.parse import urlencode
|
| 4 |
-
import cloudscraper
|
| 5 |
from fastmcp import FastMCP
|
| 6 |
import logging
|
|
|
|
| 7 |
|
| 8 |
# Set up logging
|
| 9 |
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
| 10 |
logger = logging.getLogger(__name__)
|
| 11 |
|
| 12 |
# Initialize FastMCP server
|
| 13 |
-
mcp = FastMCP("Canada
|
| 14 |
|
| 15 |
@mcp.tool(name="search_jobs")
|
| 16 |
def search_jobs_tool(query: str, location: str, limit: int = 10, salary: str = None, job_type: str = None) -> dict:
|
| 17 |
"""
|
| 18 |
-
|
| 19 |
|
| 20 |
Args:
|
| 21 |
-
query (str):
|
| 22 |
-
location (str): Location
|
| 23 |
-
limit (int):
|
| 24 |
-
salary (str
|
| 25 |
-
job_type (str
|
| 26 |
|
| 27 |
Returns:
|
| 28 |
-
dict:
|
| 29 |
"""
|
| 30 |
-
base_url = "https://
|
| 31 |
params = {
|
| 32 |
-
"
|
| 33 |
-
"
|
| 34 |
-
"sort": "M", # Sort by most recent
|
| 35 |
-
}
|
| 36 |
-
url = base_url + "?" + urlencode(params, safe=",")
|
| 37 |
-
|
| 38 |
-
headers = {
|
| 39 |
-
"User-Agent": (
|
| 40 |
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
| 41 |
-
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
| 42 |
-
"Chrome/126.0.0.0 Safari/537.36"
|
| 43 |
-
),
|
| 44 |
-
"Accept-Language": "en-US,en;q=0.9",
|
| 45 |
-
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
| 46 |
-
"Referer": "https://www.jobbank.gc.ca/",
|
| 47 |
-
"Connection": "keep-alive",
|
| 48 |
}
|
|
|
|
|
|
|
| 49 |
|
| 50 |
try:
|
| 51 |
-
logger.info(f"
|
| 52 |
-
|
| 53 |
-
response = scraper.get(url, headers=headers, timeout=10)
|
| 54 |
response.raise_for_status()
|
| 55 |
-
|
| 56 |
-
cards = soup.find_all("article", class_="job-result") # Verify class name
|
| 57 |
-
|
| 58 |
-
if not cards:
|
| 59 |
-
logger.warning("No job cards found. The website may use JavaScript or the HTML structure may have changed.")
|
| 60 |
-
logger.debug(f"HTML sample: {soup.prettify()[:2000]}") # Increased sample size for debugging
|
| 61 |
-
return {"error": "No job listings found. The website may use JavaScript or the HTML structure may have changed."}
|
| 62 |
|
|
|
|
| 63 |
jobs = []
|
| 64 |
-
for
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
"title": title_elem.get_text(strip=True) if title_elem else "No Title",
|
| 76 |
-
"company": company_elem.get_text(strip=True).replace("Employer", "").strip() if company_elem else "Unknown Company",
|
| 77 |
-
"location": location_elem.get_text(strip=True).replace("Location", "").strip() if location_elem else "Unknown Location",
|
| 78 |
-
"url": link or "#"
|
| 79 |
-
}
|
| 80 |
-
jobs.append(job)
|
| 81 |
-
|
| 82 |
-
logger.info(f"Found {len(jobs)} job listings.")
|
| 83 |
return {"jobs": jobs}
|
| 84 |
|
| 85 |
-
except cloudscraper.exceptions.CloudflareChallengeError as cf_err:
|
| 86 |
-
logger.error(f"Cloudflare challenge error: {cf_err}")
|
| 87 |
-
return {"error": f"Cloudflare challenge error: {cf_err}"}
|
| 88 |
except requests.exceptions.HTTPError as http_err:
|
| 89 |
logger.error(f"HTTP error: {http_err}")
|
| 90 |
return {"error": f"HTTP error occurred: {http_err}"}
|
|
@@ -100,12 +69,12 @@ def search_jobs_tool(query: str, location: str, limit: int = 10, salary: str = N
|
|
| 100 |
|
| 101 |
def search_jobs_ui(query, location, limit=10, salary=None, job_type=None):
|
| 102 |
"""
|
| 103 |
-
|
| 104 |
"""
|
| 105 |
-
if not query
|
| 106 |
-
return "❌ Please provide
|
| 107 |
|
| 108 |
-
limit = int(max(1, min(limit, 50)))
|
| 109 |
result = search_jobs_tool(query, location, limit, salary, job_type)
|
| 110 |
|
| 111 |
if "error" in result:
|
|
@@ -113,14 +82,16 @@ def search_jobs_ui(query, location, limit=10, salary=None, job_type=None):
|
|
| 113 |
|
| 114 |
jobs = result.get("jobs", [])
|
| 115 |
if not jobs:
|
| 116 |
-
return "No jobs found for your search. Try different keywords
|
| 117 |
|
| 118 |
-
output = "# Job Search Results\n\n"
|
| 119 |
for i, job in enumerate(jobs, 1):
|
| 120 |
output += (
|
| 121 |
f"### {i}. {job['title']}\n"
|
| 122 |
f"**Company**: {job['company']}\n"
|
| 123 |
f"**Location**: {job['location']}\n"
|
|
|
|
|
|
|
| 124 |
f"[Apply Here]({job['url']})\n\n"
|
| 125 |
)
|
| 126 |
|
|
@@ -131,16 +102,16 @@ app = gr.Interface(
|
|
| 131 |
fn=search_jobs_ui,
|
| 132 |
inputs=[
|
| 133 |
gr.Textbox(label="Job Title / Keyword", placeholder="e.g., Software Engineer"),
|
| 134 |
-
gr.Textbox(label="Location", placeholder="e.g., Toronto, ON"),
|
| 135 |
gr.Slider(minimum=1, maximum=50, value=10, step=1, label="Number of Results"),
|
| 136 |
gr.Textbox(label="Salary (optional, ignored)", placeholder="Not used"),
|
| 137 |
gr.Textbox(label="Job Type (optional, ignored)", placeholder="Not used")
|
| 138 |
],
|
| 139 |
outputs=gr.Markdown(),
|
| 140 |
-
title="Canada
|
| 141 |
-
description="Search jobs
|
| 142 |
theme="huggingface"
|
| 143 |
)
|
| 144 |
|
| 145 |
if __name__ == "__main__":
|
| 146 |
-
app.launch(mcp_server=True)
|
|
|
|
| 1 |
import gradio as gr
|
|
|
|
|
|
|
|
|
|
| 2 |
from fastmcp import FastMCP
|
| 3 |
import logging
|
| 4 |
+
import requests
|
| 5 |
|
| 6 |
# Set up logging
|
| 7 |
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
| 8 |
logger = logging.getLogger(__name__)
|
| 9 |
|
| 10 |
# Initialize FastMCP server
|
| 11 |
+
mcp = FastMCP("Jobicy Canada Remote Jobs Agent")
|
| 12 |
|
| 13 |
@mcp.tool(name="search_jobs")
|
| 14 |
def search_jobs_tool(query: str, location: str, limit: int = 10, salary: str = None, job_type: str = None) -> dict:
|
| 15 |
"""
|
| 16 |
+
Use Jobicy's Remote Jobs API to search remote jobs in Canada.
|
| 17 |
|
| 18 |
Args:
|
| 19 |
+
query (str): Keyword or job title to search for.
|
| 20 |
+
location (str): Location filter (ignored since it's remote, but kept for API compatibility).
|
| 21 |
+
limit (int): Max number of jobs to return (max 50).
|
| 22 |
+
salary (str): Ignored (for compatibility).
|
| 23 |
+
job_type (str): Ignored (for compatibility).
|
| 24 |
|
| 25 |
Returns:
|
| 26 |
+
dict: Contains 'jobs' list or 'error' message.
|
| 27 |
"""
|
| 28 |
+
base_url = "https://jobicy.com/api/v2/remote-jobs"
|
| 29 |
params = {
|
| 30 |
+
"count": min(max(1, limit), 50), # ensure between 1 and 50
|
| 31 |
+
"geo": "canada",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
}
|
| 33 |
+
if query.strip():
|
| 34 |
+
params["tag"] = query.strip()
|
| 35 |
|
| 36 |
try:
|
| 37 |
+
logger.info(f"Requesting Jobicy API with params: {params}")
|
| 38 |
+
response = requests.get(base_url, params=params, timeout=10)
|
|
|
|
| 39 |
response.raise_for_status()
|
| 40 |
+
data = response.json()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
+
jobs_raw = data.get("jobs", [])
|
| 43 |
jobs = []
|
| 44 |
+
for job in jobs_raw:
|
| 45 |
+
jobs.append({
|
| 46 |
+
"title": job.get("jobTitle", "No Title"),
|
| 47 |
+
"company": job.get("companyName", "Unknown Company"),
|
| 48 |
+
"location": job.get("jobGeo", "Remote"),
|
| 49 |
+
"url": job.get("url", "#"),
|
| 50 |
+
"pubDate": job.get("pubDate", "Unknown Date"),
|
| 51 |
+
"salary": f"{job.get('salaryMin', '')} - {job.get('salaryMax', '')} {job.get('salaryCurrency', '')}".strip()
|
| 52 |
+
})
|
| 53 |
+
|
| 54 |
+
logger.info(f"Found {len(jobs)} jobs from Jobicy.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
return {"jobs": jobs}
|
| 56 |
|
|
|
|
|
|
|
|
|
|
| 57 |
except requests.exceptions.HTTPError as http_err:
|
| 58 |
logger.error(f"HTTP error: {http_err}")
|
| 59 |
return {"error": f"HTTP error occurred: {http_err}"}
|
|
|
|
| 69 |
|
| 70 |
def search_jobs_ui(query, location, limit=10, salary=None, job_type=None):
|
| 71 |
"""
|
| 72 |
+
Gradio UI function for displaying job search results.
|
| 73 |
"""
|
| 74 |
+
if not query:
|
| 75 |
+
return "❌ Please provide a job title or keyword to search for."
|
| 76 |
|
| 77 |
+
limit = int(max(1, min(limit, 50)))
|
| 78 |
result = search_jobs_tool(query, location, limit, salary, job_type)
|
| 79 |
|
| 80 |
if "error" in result:
|
|
|
|
| 82 |
|
| 83 |
jobs = result.get("jobs", [])
|
| 84 |
if not jobs:
|
| 85 |
+
return "No jobs found for your search. Try different keywords."
|
| 86 |
|
| 87 |
+
output = "# Jobicy Canada Remote Job Search Results\n\n"
|
| 88 |
for i, job in enumerate(jobs, 1):
|
| 89 |
output += (
|
| 90 |
f"### {i}. {job['title']}\n"
|
| 91 |
f"**Company**: {job['company']}\n"
|
| 92 |
f"**Location**: {job['location']}\n"
|
| 93 |
+
f"**Salary**: {job['salary']}\n"
|
| 94 |
+
f"**Posted On**: {job['pubDate']}\n"
|
| 95 |
f"[Apply Here]({job['url']})\n\n"
|
| 96 |
)
|
| 97 |
|
|
|
|
| 102 |
fn=search_jobs_ui,
|
| 103 |
inputs=[
|
| 104 |
gr.Textbox(label="Job Title / Keyword", placeholder="e.g., Software Engineer"),
|
| 105 |
+
gr.Textbox(label="Location (ignored)", placeholder="Any, e.g., Toronto, ON"),
|
| 106 |
gr.Slider(minimum=1, maximum=50, value=10, step=1, label="Number of Results"),
|
| 107 |
gr.Textbox(label="Salary (optional, ignored)", placeholder="Not used"),
|
| 108 |
gr.Textbox(label="Job Type (optional, ignored)", placeholder="Not used")
|
| 109 |
],
|
| 110 |
outputs=gr.Markdown(),
|
| 111 |
+
title="Jobicy Canada Remote Job Search",
|
| 112 |
+
description="Search remote jobs in Canada using Jobicy API with FastMCP and Gradio.",
|
| 113 |
theme="huggingface"
|
| 114 |
)
|
| 115 |
|
| 116 |
if __name__ == "__main__":
|
| 117 |
+
app.launch(mcp_server=True)
|