-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
160 lines (145 loc) · 6.69 KB
/
Copy pathapp.py
File metadata and controls
160 lines (145 loc) · 6.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
"""Streamlit interface."""
from __future__ import annotations
import os
import tempfile
import streamlit as st
from jobpilot.agent import Agent
from jobpilot.export import export
from jobpilot.llm import LLMError, OllamaClient
from jobpilot.pipeline import ats_check, generate_cover_letter, generate_rewrite
st.set_page_config(page_title="JobPilot", page_icon="🧭", layout="wide")
st.title("🧭 JobPilot — local AI job-application agent")
st.caption(
"Runs 100% locally on Ollama. Full agent runs, standalone ATS tests, and "
"tailored resume rewrites — no data leaves your machine."
)
with st.sidebar:
st.header("Inputs")
mode = st.radio("Mode", ["Full agent run", "ATS test only"])
ats_only = mode == "ATS test only"
resume_file = st.file_uploader("Resume", type=["pdf", "docx", "md", "txt"])
job_mode = st.radio("Job posting", ["Paste text", "URL"], horizontal=True)
job_url, job_text = "", ""
if job_mode == "URL":
job_url = st.text_input("Job posting URL")
st.caption("LinkedIn/Workday often block scraping — paste the text if the fetch fails.")
else:
label = "Paste the job description" + (" (optional for ATS test)" if ats_only else "")
job_text = st.text_area(label, height=240)
job_given = bool(job_url or job_text.strip())
run = st.button(
"Run ATS test" if ats_only else "Run agent",
type="primary",
use_container_width=True,
disabled=not (resume_file and (job_given or ats_only)),
)
if run and resume_file:
suffix = os.path.splitext(resume_file.name)[1]
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
tmp.write(resume_file.getvalue())
resume_path = tmp.name
st.session_state.pop("rewritten", None)
st.session_state.pop("cover_letter", None)
trace_box = st.container()
def show(step):
with trace_box:
if step["type"] == "tool_call":
st.markdown(f"**→ {step['tool']}** `{step['args'] or ''}` *({step['t']}s)*")
elif step["type"] == "tool_result":
(st.success if step["ok"] else st.error)(step["detail"][:300])
try:
if ats_only:
with st.spinner("Running ATS test..."):
ctx = ats_check(resume_path, job_text=job_text, job_url=job_url)
st.session_state["ctx"] = ctx
st.session_state["answer"] = ""
else:
with st.spinner("Agent running (local 8B model, ~1-2 min)..."):
result = Agent(OllamaClient()).run(
resume_path=resume_path, job_url=job_url, job_text=job_text, on_step=show
)
st.session_state["ctx"] = result["context"]
st.session_state["answer"] = result["answer"]
except (ValueError, LLMError) as exc:
st.error(str(exc))
finally:
os.unlink(resume_path)
ctx = st.session_state.get("ctx")
if ctx and ctx.ats_report:
ats = ctx.ats_report
job = ctx.job_info or {}
st.subheader(
f"ATS score — {job.get('title', 'general resume health check')}"
+ (f" @ {job['company']}" if job.get("company") else "")
)
st.metric("Overall", f"{ats['overall']}/100", ats["grade"])
cols = st.columns(len(ats["breakdown"]))
for col, (name, res) in zip(cols, ats["breakdown"].items()):
score = res.get("score")
col.metric(name, "—" if score is None else f"{score:.0f}")
kw = ats["breakdown"]["keywords"]
if kw.get("matched_required"):
st.success("Matched required: " + ", ".join(kw["matched_required"]))
if kw.get("missing_required"):
st.error("Missing required skills: " + ", ".join(kw["missing_required"]))
if kw.get("missing_keywords"):
st.warning("Missing keywords: " + ", ".join(kw["missing_keywords"][:12]))
for issue in ats["breakdown"]["quality"]["issues"]:
st.info(issue)
if ctx.extras.get("resume_review"):
with st.expander("Detailed resume review & improvements", expanded=True):
st.markdown(ctx.extras["resume_review"])
if st.session_state.get("answer"):
st.markdown("### Agent summary")
st.write(st.session_state["answer"])
if ctx.tailoring:
with st.expander("Tailoring suggestions", expanded=False):
st.markdown(ctx.tailoring)
if ctx.report_path and os.path.exists(ctx.report_path):
with open(ctx.report_path, "r", encoding="utf-8") as f:
st.download_button("Download full report (.md)", f.read(),
file_name=os.path.basename(ctx.report_path))
# opt-in rewrite
st.divider()
if "rewritten" not in st.session_state:
ask = ("**Want to see your resume rewritten for this job?**"
if ctx.job_info else
"**Want a modified resume with an improved ATS score?**")
st.markdown(ask + " Grounded in your real experience — never invents anything.")
if st.button("Yes, generate rewritten resume", type="primary"):
with st.spinner("Rewriting resume..."):
st.session_state["rewritten"] = generate_rewrite(ctx)
st.rerun()
else:
md = st.session_state["rewritten"]
st.markdown("### Rewritten resume (draft — review before using)")
preview, raw = st.tabs(["Preview", "Markdown"])
with preview:
st.markdown(md)
with raw:
st.code(md, language="markdown")
cols = st.columns(4)
for col, fmt, label in zip(
cols, ("pdf", "docx", "md", "html"),
("PDF", "Word (.docx)", "Markdown", "HTML"),
):
data, mime = export(md, fmt)
col.download_button(label, data, file_name=f"rewritten-resume.{fmt}",
mime=mime, use_container_width=True)
# opt-in cover letter
if ctx.job_info:
if "cover_letter" not in st.session_state:
if st.button("Write a cover letter for this job"):
with st.spinner("Writing cover letter..."):
st.session_state["cover_letter"] = generate_cover_letter(ctx)
st.rerun()
else:
st.markdown("### Cover letter (draft — review before sending)")
st.markdown(st.session_state["cover_letter"])
lcols = st.columns(3)
for col, fmt, label in zip(lcols, ("pdf", "docx", "md"), ("PDF", "Word (.docx)", "Markdown")):
data, mime = export(st.session_state["cover_letter"], fmt)
col.download_button(label, data, file_name=f"cover-letter.{fmt}",
mime=mime, use_container_width=True, key=f"cl-{fmt}")
else:
st.info("Upload a resume in the sidebar (job posting optional for ATS-only mode), then run.")