Spaces:
Sleeping
Sleeping
Jonah Ramponi
commited on
Commit
•
0c94c61
1
Parent(s):
98eaa40
Cleanup
Browse files- .streamlit/config.toml +2 -0
- CVReview.py +113 -0
- Interview.py +75 -0
- app.py +74 -180
- assets/logo.png +0 -0
- backend.py → utils/backend.py +10 -4
- utils/format.py +9 -88
- utils/gpt.py +10 -16
- utils/process_doc.py +5 -7
- utils/prompts.py +0 -17
.streamlit/config.toml
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
[client]
|
2 |
+
showSidebarNavigation = false
|
CVReview.py
ADDED
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
CV Review Feature
|
3 |
+
"""
|
4 |
+
|
5 |
+
import json
|
6 |
+
import concurrent.futures
|
7 |
+
from httpx import LocalProtocolError
|
8 |
+
|
9 |
+
import streamlit as st
|
10 |
+
from cohere.core.api_error import ApiError
|
11 |
+
|
12 |
+
from utils.backend import produce_report
|
13 |
+
from utils.format import extract_json
|
14 |
+
|
15 |
+
|
16 |
+
def generate_markdown_report(REPORT_OBJ: dict) -> str:
|
17 |
+
"""Format the report object dictionary into a markdown readable report"""
|
18 |
+
|
19 |
+
def add_section(section_title: str, table_contents: list) -> str:
|
20 |
+
"""Utility function to add a section containing a table to the markdown report"""
|
21 |
+
|
22 |
+
if not table_contents:
|
23 |
+
return ""
|
24 |
+
|
25 |
+
section = f"### {section_title.title()}\n\n"
|
26 |
+
section += (
|
27 |
+
"| Job Posting Requirement | CV Details | Explanation | Impact Score |\n"
|
28 |
+
"| ----------------------- | ---------- | ----------- | -------------- |\n"
|
29 |
+
)
|
30 |
+
for table_row in table_contents:
|
31 |
+
section += f"| {table_row.get('jobPostingDetails', 'N/A')} | {table_row.get('cvDetails', 'N/A')} | {table_row.get('explanation', '')} | **{table_row.get('severityScore', 0)}** |\n"
|
32 |
+
return section + "\n"
|
33 |
+
|
34 |
+
report = (
|
35 |
+
f"# CV Analysis Report\n\n"
|
36 |
+
f"**Name:** {REPORT_OBJ.get('personName', 'Unknown')} \n"
|
37 |
+
f"**Job:** {REPORT_OBJ.get('jobTitle', 'N/A')} at {REPORT_OBJ.get('companyName', 'N/A')} \n"
|
38 |
+
f"**Job Description:** {REPORT_OBJ.get('jobDesc', 'No description available.')}\n\n"
|
39 |
+
"---\n\n"
|
40 |
+
"## Key Findings\n\n"
|
41 |
+
)
|
42 |
+
|
43 |
+
sections = ["experience", "education", "responsibilities", "languages", "tools"]
|
44 |
+
|
45 |
+
for section in sections:
|
46 |
+
report += add_section(section, REPORT_OBJ.get(section, []))
|
47 |
+
|
48 |
+
report += "---\n"
|
49 |
+
return report
|
50 |
+
|
51 |
+
|
52 |
+
def CVReviewPage():
|
53 |
+
"""Source Code for CV Review Page"""
|
54 |
+
|
55 |
+
SHARED_STATE = st.session_state.shared_materials
|
56 |
+
API_KEY = st.session_state.api_key
|
57 |
+
|
58 |
+
produce_report = st.button("Produce Suitability Report")
|
59 |
+
if produce_report:
|
60 |
+
try:
|
61 |
+
results = {}
|
62 |
+
# We will make 3 calls in parallel, to get various bits of information efficiently
|
63 |
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
64 |
+
|
65 |
+
futures = {
|
66 |
+
critique_type: executor.submit(
|
67 |
+
produce_report,
|
68 |
+
SHARED_STATE["cv"],
|
69 |
+
SHARED_STATE["job_posting"],
|
70 |
+
critique_type,
|
71 |
+
API_KEY,
|
72 |
+
)
|
73 |
+
for critique_type in ["basic", "general", "specific"]
|
74 |
+
}
|
75 |
+
|
76 |
+
for critique_type, future in futures.items():
|
77 |
+
results[critique_type] = future.result()
|
78 |
+
|
79 |
+
except LocalProtocolError:
|
80 |
+
st.error("You need to enter a Cohere API Key.")
|
81 |
+
except ApiError:
|
82 |
+
st.error("You need a valid Cohere API Key")
|
83 |
+
|
84 |
+
# merge the from our calls, by extracting the json object from the gpt message
|
85 |
+
resultsDict = {}
|
86 |
+
for jsonText in results.values():
|
87 |
+
_, output_report_json = extract_json(jsonText)
|
88 |
+
|
89 |
+
resultsDict.update(output_report_json)
|
90 |
+
|
91 |
+
# store this as the report object
|
92 |
+
SHARED_STATE["report"] = resultsDict
|
93 |
+
|
94 |
+
# if the report object exists
|
95 |
+
if SHARED_STATE["report"]:
|
96 |
+
REPORT = SHARED_STATE["report"]
|
97 |
+
|
98 |
+
# these are used for file naming
|
99 |
+
name = REPORT.get("personName", "MissingPersonName")
|
100 |
+
job_title = REPORT.get("jobTitle", "MissingTitle")
|
101 |
+
company_name = REPORT.get("companyName", "MissingCompany")
|
102 |
+
|
103 |
+
# render markdown report
|
104 |
+
st.markdown(generate_markdown_report(REPORT))
|
105 |
+
|
106 |
+
# Downloadable in json form !
|
107 |
+
st.download_button(
|
108 |
+
label="Download Report JSON",
|
109 |
+
data=json.dumps(REPORT, indent=4),
|
110 |
+
file_name=f"{name}_{job_title}_{company_name}.json",
|
111 |
+
mime="application/json",
|
112 |
+
use_container_width=True,
|
113 |
+
)
|
Interview.py
ADDED
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Simulates an interview, using uploaded CV and Job Description
|
3 |
+
"""
|
4 |
+
|
5 |
+
import random
|
6 |
+
import streamlit as st
|
7 |
+
from httpx import LocalProtocolError
|
8 |
+
|
9 |
+
from cohere.core.api_error import ApiError
|
10 |
+
|
11 |
+
|
12 |
+
from utils.gpt import stream
|
13 |
+
|
14 |
+
|
15 |
+
def InterviewPage():
|
16 |
+
"""Source Code for the Interview Simulation Page"""
|
17 |
+
|
18 |
+
initial_questions = [
|
19 |
+
"Ready for me to grill you?",
|
20 |
+
"Please let me know when you're ready to begin the interview",
|
21 |
+
"Ready to rumble?",
|
22 |
+
]
|
23 |
+
|
24 |
+
# the initial message will be a random choice, initiating the conversation
|
25 |
+
if "messages" not in st.session_state:
|
26 |
+
st.session_state["messages"] = [
|
27 |
+
{"role": "assistant", "message": random.choice(initial_questions)}
|
28 |
+
]
|
29 |
+
|
30 |
+
MESSAGES = st.session_state.messages
|
31 |
+
SHARED_STATE = st.session_state.shared_materials
|
32 |
+
API_KEY = st.session_state.api_key
|
33 |
+
|
34 |
+
clear_conversation = st.button("Clear Conversation")
|
35 |
+
|
36 |
+
# Clear conversation will clear message state, and initialize with a new random question
|
37 |
+
if clear_conversation:
|
38 |
+
st.session_state["messages"] = [
|
39 |
+
{"role": "assistant", "message": random.choice(initial_questions)}
|
40 |
+
]
|
41 |
+
|
42 |
+
if not SHARED_STATE["valid_flag"]:
|
43 |
+
st.error("You need to upload a Job Description & CV to use this feature.")
|
44 |
+
else:
|
45 |
+
try:
|
46 |
+
# Populate the chat with historic messages
|
47 |
+
for msg in MESSAGES:
|
48 |
+
st.chat_message(msg["role"]).write(msg["message"])
|
49 |
+
|
50 |
+
if user_input := st.chat_input():
|
51 |
+
|
52 |
+
# Write the user question to UI
|
53 |
+
st.chat_message("user").write(user_input)
|
54 |
+
|
55 |
+
assistant_message = st.chat_message("assistant")
|
56 |
+
|
57 |
+
# Stream assistant message, using relevant background information
|
58 |
+
response = assistant_message.write_stream(
|
59 |
+
stream(
|
60 |
+
background_info={
|
61 |
+
"cv": SHARED_STATE["cv"],
|
62 |
+
"job_posting": SHARED_STATE["job_posting"],
|
63 |
+
},
|
64 |
+
chat_history=MESSAGES,
|
65 |
+
api_key=API_KEY,
|
66 |
+
)
|
67 |
+
)
|
68 |
+
|
69 |
+
# Append messages to chat history
|
70 |
+
MESSAGES.append({"role": "user", "message": user_input})
|
71 |
+
MESSAGES.append({"role": "assistant", "message": response})
|
72 |
+
except LocalProtocolError:
|
73 |
+
st.error("You need to enter a Cohere API Key.")
|
74 |
+
except ApiError:
|
75 |
+
st.error("You need a valid Cohere API Key")
|
app.py
CHANGED
@@ -2,191 +2,85 @@
|
|
2 |
For HF, the interface should be called app.py
|
3 |
"""
|
4 |
|
5 |
-
import
|
6 |
-
import random
|
7 |
-
import concurrent.futures
|
8 |
-
|
9 |
import streamlit as st
|
10 |
|
11 |
-
from backend import paired_critique
|
12 |
-
|
13 |
from utils.process_doc import parse_docx, parse_pdf
|
14 |
-
from utils.gpt import test_api_key, gpt_stream_response_chat_history
|
15 |
-
from utils.format import extract_json, generate_markdown_report
|
16 |
-
|
17 |
-
st.set_page_config(layout="wide")
|
18 |
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
)
|
25 |
|
26 |
-
|
27 |
-
st.session_state.state = {"successful_report_flag": True, "paired_report": {}}
|
28 |
-
|
29 |
-
STATE = st.session_state.state
|
30 |
-
|
31 |
-
# Weird Hugging Face display issue, padding fixes it
|
32 |
-
st.markdown("\n")
|
33 |
-
st.markdown("\n")
|
34 |
-
st.markdown("\n")
|
35 |
-
|
36 |
-
cv_upload_box = st.file_uploader(
|
37 |
-
"CV Upload Box",
|
38 |
-
help="Upload your CV in .docx or .pdf form. This CV will be parsed, and used to analyse against the given job post.",
|
39 |
-
type=["docx", "pdf"],
|
40 |
-
accept_multiple_files=False,
|
41 |
-
)
|
42 |
-
job_posting_upload_box = st.text_area(
|
43 |
-
"Job Description Upload Box",
|
44 |
-
value="""Job description
|
45 |
-
As a Data Scientist at Meta, you will shape the future of people-facing and business-facing products we build across our entire family of applications (Facebook, Instagram, Messenger, WhatsApp, Oculus). By applying your technical skills, analytical mindset, and product intuition to one of the richest data sets in the world, you will help define the experiences we build for billions of people and hundreds of millions of businesses around the world. You will collaborate on a wide array of product and business problems with a diverse set of cross-functional partners across Product, Engineering, Research, Data Engineering, Marketing, Sales, Finance and others. You will use data and analysis to identify and solve product development’s biggest challenges. You will influence product strategy and investment decisions with data, be focused on impact, and collaborate with other teams. By joining Meta, you will become part of a world-class analytics community dedicated to skill development and career growth in analytics and beyond.Product leadership: You will use data to shape product development, quantify new opportunities, identify upcoming challenges, and ensure the products we build bring value to people, businesses, and Meta. You will help your partner teams prioritize what to build, set goals, and understand their product’s ecosystem.Analytics: You will guide teams using data and insights. You will focus on developing hypotheses and employ a diverse toolkit of rigorous analytical approaches, different methodologies, frameworks, and technical approaches to test them.Communication and influence: You won’t simply present data, but tell data-driven stories. You will convince and influence your partners using clear insights and recommendations. You will build credibility through structure and clarity, and be a trusted strategic partner.
|
46 |
-
|
47 |
-
Data Scientist, Product Analytics Responsibilities:
|
48 |
-
|
49 |
-
Work with large and complex data sets to solve a wide array of challenging problems using different analytical and statistical approaches.
|
50 |
-
Apply technical expertise with quantitative analysis, experimentation, data mining, and the presentation of data to develop strategies for our products that serve billions of people and hundreds of millions of businesses.
|
51 |
-
Identify and measure success of product efforts through goal setting, forecasting, and monitoring of key product metrics to understand trends.
|
52 |
-
Define, understand, and test opportunities and levers to improve the product, and drive roadmaps through your insights and recommendations.
|
53 |
-
Partner with Product, Engineering, and cross-functional teams to inform, influence, support, and execute product strategy and investment decisions.
|
54 |
-
|
55 |
-
Minimum Qualifications:
|
56 |
-
|
57 |
-
A minimum of 6 years of work experience in analytics (minimum of 4 years with a Ph.D.).
|
58 |
-
Bachelor's degree in Mathematics, Statistics, a relevant technical field, or equivalent practical experience.
|
59 |
-
Experience with data querying languages (e.g. SQL), scripting languages (e.g. Python), and/or statistical/mathematical software (e.g. R).
|
60 |
|
61 |
-
Preferred Qualifications:
|
62 |
-
|
63 |
-
Masters or Ph.D. Degree in a quantitative field.
|
64 |
-
|
65 |
-
About Meta:
|
66 |
-
|
67 |
-
Meta builds technologies that help people connect, find communities, and grow businesses. When Facebook launched in 2004, it changed the way people connect. Apps like Messenger, Instagram and WhatsApp further empowered billions around the world. Now, Meta is moving beyond 2D screens toward immersive experiences like augmented and virtual reality to help build the next evolution in social technology. People who choose to build their careers by building with us at Meta help shape a future that will take us beyond what digital connection makes possible today—beyond the constraints of screens, the limits of distance, and even the rules of physics.
|
68 |
-
|
69 |
-
Individual compensation is determined by skills, qualifications, experience, and location. Compensation details listed in this posting reflect the base hourly rate, monthly rate, or annual salary only, and do not include bonus, equity or sales incentives, if applicable. In addition to base compensation, Meta offers benefits. Learn more about benefits at Meta.""",
|
70 |
-
placeholder="Copy and Paste a job post you are interested in. Make sure to include the full post! More information is better.",
|
71 |
-
help="In this box, please dump text content for a job description you are interested in. This could easily be setup to work directly with a webpage (we'd simply need to scrape said page) however I do not want to do that on HF spaces.",
|
72 |
-
)
|
73 |
-
|
74 |
-
if cv_upload_box and job_posting_upload_box != "":
|
75 |
-
STATE["job_posting"] = job_posting_upload_box
|
76 |
-
|
77 |
-
cv_filetype = cv_upload_box.name.split(".")[-1]
|
78 |
-
cv_file_contents = cv_upload_box.getvalue()
|
79 |
-
|
80 |
-
STATE["cv"] = (
|
81 |
-
parse_docx(cv_file_contents)
|
82 |
-
if cv_filetype == "docx"
|
83 |
-
else parse_pdf(cv_file_contents)
|
84 |
-
)
|
85 |
-
cv_critique, practice_interview, general_cv_critique = st.tabs(
|
86 |
-
["Role Specific CV Critique", "Practice Interview", "General CV Critique"]
|
87 |
-
)
|
88 |
|
89 |
-
|
90 |
-
|
91 |
-
if produce_report:
|
92 |
-
|
93 |
-
# Make 3 calls in parallel
|
94 |
-
with concurrent.futures.ThreadPoolExecutor() as executor:
|
95 |
-
future1 = executor.submit(
|
96 |
-
paired_critique,
|
97 |
-
STATE["cv"],
|
98 |
-
STATE["job_posting"],
|
99 |
-
"basic",
|
100 |
-
COHERE_API_KEY,
|
101 |
-
)
|
102 |
-
|
103 |
-
future2 = executor.submit(
|
104 |
-
paired_critique,
|
105 |
-
STATE["cv"],
|
106 |
-
STATE["job_posting"],
|
107 |
-
"general",
|
108 |
-
COHERE_API_KEY,
|
109 |
-
)
|
110 |
-
|
111 |
-
future3 = executor.submit(
|
112 |
-
paired_critique,
|
113 |
-
STATE["cv"],
|
114 |
-
STATE["job_posting"],
|
115 |
-
"specific",
|
116 |
-
COHERE_API_KEY,
|
117 |
-
)
|
118 |
-
|
119 |
-
basic_details_out = future1.result()
|
120 |
-
general_details_out = future2.result()
|
121 |
-
specific_details_out = future3.result()
|
122 |
-
|
123 |
-
# merge the outputs
|
124 |
-
resultsDict = {}
|
125 |
-
for jsonText in [
|
126 |
-
basic_details_out,
|
127 |
-
general_details_out,
|
128 |
-
specific_details_out,
|
129 |
-
]:
|
130 |
-
valid_json_flag, output_report_json = extract_json(jsonText)
|
131 |
-
|
132 |
-
if not valid_json_flag:
|
133 |
-
STATE["successful_report_flag"] = False
|
134 |
-
|
135 |
-
resultsDict.update(output_report_json)
|
136 |
-
|
137 |
-
STATE["paired_report"] = resultsDict
|
138 |
-
|
139 |
-
if STATE["successful_report_flag"] and STATE["paired_report"]:
|
140 |
-
|
141 |
-
paired_report = STATE["paired_report"]
|
142 |
-
|
143 |
-
name = paired_report.get("personName", "MissingPersonName")
|
144 |
-
job_title = paired_report.get("jobTitle", "MissingTitle")
|
145 |
-
company_name = paired_report.get("companyName", "MissingCompany")
|
146 |
-
|
147 |
-
with cv_critique:
|
148 |
-
st.markdown(generate_markdown_report(STATE["paired_report"]))
|
149 |
-
|
150 |
-
st.download_button(
|
151 |
-
label="Download Report JSON",
|
152 |
-
data=json.dumps(STATE["paired_report"], indent=4),
|
153 |
-
file_name=f"{name}_{job_title}_{company_name}.json",
|
154 |
-
mime="application/json",
|
155 |
-
use_container_width=True,
|
156 |
-
)
|
157 |
-
|
158 |
-
# Streaming Chatbot !!!
|
159 |
-
with practice_interview:
|
160 |
-
initial_questions = [
|
161 |
-
"What do you think is the biggest reason you're unsuitable for the role?",
|
162 |
-
"Why are you interested in this role specifically?",
|
163 |
-
"What do you know about the company?",
|
164 |
-
]
|
165 |
-
|
166 |
-
if "messages" not in st.session_state:
|
167 |
-
st.session_state["messages"] = [
|
168 |
-
{"role": "assistant", "message": random.choice(initial_questions)}
|
169 |
-
]
|
170 |
-
|
171 |
-
# Populate the chat with historic messages
|
172 |
-
for msg in st.session_state.messages:
|
173 |
-
st.chat_message(msg["role"]).write(msg["message"])
|
174 |
-
|
175 |
-
if prompt := st.chat_input():
|
176 |
-
st.session_state.messages.append({"role": "user", "message": prompt})
|
177 |
-
st.chat_message("user").write(prompt)
|
178 |
-
|
179 |
-
assistant_message = st.chat_message("assistant")
|
180 |
-
|
181 |
-
response = assistant_message.write_stream(
|
182 |
-
gpt_stream_response_chat_history(
|
183 |
-
st.session_state.messages,
|
184 |
-
background_info={
|
185 |
-
"cv": STATE["cv"],
|
186 |
-
"job_posting": STATE["job_posting"],
|
187 |
-
},
|
188 |
-
api_key=COHERE_API_KEY,
|
189 |
-
)
|
190 |
-
)
|
191 |
-
|
192 |
-
st.session_state.messages.append({"role": "assistant", "message": response})
|
|
|
2 |
For HF, the interface should be called app.py
|
3 |
"""
|
4 |
|
5 |
+
import pathlib
|
|
|
|
|
|
|
6 |
import streamlit as st
|
7 |
|
|
|
|
|
8 |
from utils.process_doc import parse_docx, parse_pdf
|
|
|
|
|
|
|
|
|
9 |
|
10 |
+
from Interview import InterviewPage
|
11 |
+
from CVReview import CVReviewPage
|
12 |
+
|
13 |
+
CURRENT_DIR = pathlib.Path(__file__).parent.resolve()
|
14 |
+
|
15 |
+
|
16 |
+
def main():
|
17 |
+
st.set_page_config(layout="wide")
|
18 |
+
|
19 |
+
if "api_key" not in st.session_state:
|
20 |
+
st.session_state.api_key = ""
|
21 |
+
|
22 |
+
if "report" not in st.session_state:
|
23 |
+
st.session_state.report = {}
|
24 |
+
|
25 |
+
if "shared_materials" not in st.session_state:
|
26 |
+
st.session_state.shared_materials = {"valid_flag": False, "report": None}
|
27 |
+
|
28 |
+
SHARED_MATERIALS = st.session_state.shared_materials
|
29 |
+
|
30 |
+
with st.sidebar:
|
31 |
+
st.session_state["api_key"] = st.text_input(
|
32 |
+
"Cohere API Key Entry",
|
33 |
+
value="",
|
34 |
+
placeholder="Enter your Free Tier Cohere API Key",
|
35 |
+
)
|
36 |
+
|
37 |
+
job_posting_upload_box = st.text_area(
|
38 |
+
"Job Description Upload Box",
|
39 |
+
value="",
|
40 |
+
placeholder="Copy and Paste the contents of a job post you are interested in.",
|
41 |
+
help="In this box, please dump text content for a job description you are interested in. This could easily be setup to work directly with a webpage (we'd simply need to scrape said page) however I do not want to do that on HF spaces.",
|
42 |
+
)
|
43 |
+
|
44 |
+
cv_upload_box = st.file_uploader(
|
45 |
+
"CV Upload Box",
|
46 |
+
help="Upload your CV in .docx or .pdf form. This CV will be parsed, and used to analyse against the given job post.",
|
47 |
+
type=["docx", "pdf"],
|
48 |
+
accept_multiple_files=False,
|
49 |
+
)
|
50 |
+
|
51 |
+
if cv_upload_box and job_posting_upload_box != "":
|
52 |
+
SHARED_MATERIALS["job_posting"] = job_posting_upload_box
|
53 |
+
|
54 |
+
cv_filetype = cv_upload_box.name.split(".")[-1]
|
55 |
+
cv_file_contents = cv_upload_box.getvalue()
|
56 |
+
|
57 |
+
SHARED_MATERIALS["cv"] = (
|
58 |
+
parse_docx(cv_file_contents)
|
59 |
+
if cv_filetype == "docx"
|
60 |
+
else parse_pdf(cv_file_contents)
|
61 |
+
)
|
62 |
+
|
63 |
+
SHARED_MATERIALS["valid_flag"] = True
|
64 |
+
|
65 |
+
pg = st.navigation(
|
66 |
+
{
|
67 |
+
"Job Search AI Tools": [
|
68 |
+
st.Page(
|
69 |
+
InterviewPage,
|
70 |
+
title="Practice Interview",
|
71 |
+
icon=":material/chat:",
|
72 |
+
),
|
73 |
+
st.Page(
|
74 |
+
CVReviewPage,
|
75 |
+
title="CV Review",
|
76 |
+
icon=":material/description:",
|
77 |
+
),
|
78 |
+
],
|
79 |
+
}
|
80 |
)
|
81 |
|
82 |
+
pg.run()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
84 |
|
85 |
+
if __name__ == "__main__":
|
86 |
+
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assets/logo.png
ADDED
backend.py → utils/backend.py
RENAMED
@@ -1,3 +1,7 @@
|
|
|
|
|
|
|
|
|
|
1 |
from utils.prompts import (
|
2 |
basic_details_extraction_prompt,
|
3 |
general_skils_extraction_prompt,
|
@@ -13,13 +17,15 @@ prompt_mapping = {
|
|
13 |
}
|
14 |
|
15 |
|
16 |
-
def
|
17 |
-
cv_contents: str, job_post_contents: str,
|
18 |
) -> str:
|
19 |
"""Process CV contents, using Cohere"""
|
20 |
|
|
|
|
|
21 |
# First, get the prompt from the prompt dict
|
22 |
-
prompt = prompt_mapping.get(
|
23 |
|
24 |
# Now, populate with the contents of the CV and job posting
|
25 |
prompt = prompt.replace("<cv>", cv_contents).replace(
|
@@ -43,5 +49,5 @@ if __name__ == "__main__":
|
|
43 |
|
44 |
COHERE_API_KEY = ""
|
45 |
|
46 |
-
output =
|
47 |
print(output)
|
|
|
1 |
+
"""
|
2 |
+
Main Backend Handling Function
|
3 |
+
"""
|
4 |
+
|
5 |
from utils.prompts import (
|
6 |
basic_details_extraction_prompt,
|
7 |
general_skils_extraction_prompt,
|
|
|
17 |
}
|
18 |
|
19 |
|
20 |
+
def produce_report(
|
21 |
+
cv_contents: str, job_post_contents: str, PROMPT_TO_USE: str, API_KEY: str
|
22 |
) -> str:
|
23 |
"""Process CV contents, using Cohere"""
|
24 |
|
25 |
+
# The KEY ARGUMENT here is PROMPT_TO_USE, which controls the prompt to use
|
26 |
+
|
27 |
# First, get the prompt from the prompt dict
|
28 |
+
prompt = prompt_mapping.get(PROMPT_TO_USE)
|
29 |
|
30 |
# Now, populate with the contents of the CV and job posting
|
31 |
prompt = prompt.replace("<cv>", cv_contents).replace(
|
|
|
49 |
|
50 |
COHERE_API_KEY = ""
|
51 |
|
52 |
+
output = produce_report(post_contents, cv_contents, "specific", COHERE_API_KEY)
|
53 |
print(output)
|
utils/format.py
CHANGED
@@ -1,7 +1,8 @@
|
|
1 |
import json
|
|
|
2 |
|
3 |
|
4 |
-
def extract_json(input_string: str) -> dict:
|
5 |
"""String to Json function"""
|
6 |
|
7 |
# First, ensure we remove json wrapper
|
@@ -21,98 +22,14 @@ def extract_json(input_string: str) -> dict:
|
|
21 |
return False, {}
|
22 |
|
23 |
|
24 |
-
def generate_markdown_report(data):
|
25 |
-
# Header
|
26 |
-
report = f"# CV Analysis Report\n\n"
|
27 |
-
report += f"**Name:** {data.get('personName', 'Unknown')} \n"
|
28 |
-
report += f"**Job:** {data.get('jobTitle', 'N/A')} at {data.get('companyName', 'N/A')} \n"
|
29 |
-
report += (
|
30 |
-
f"**Job Description:** {data.get('jobDesc', 'No description available.')}\n\n"
|
31 |
-
)
|
32 |
-
report += "---\n\n"
|
33 |
-
report += "## Key Findings\n\n"
|
34 |
-
|
35 |
-
experiences = data.get("experience", [])
|
36 |
-
if experiences:
|
37 |
-
|
38 |
-
report += "### Experience\n\n"
|
39 |
-
report += (
|
40 |
-
"| Job Posting Requirement | CV Details | Explanation | Impact Score |\n"
|
41 |
-
)
|
42 |
-
report += (
|
43 |
-
"| ----------------------- | ---------- | ----------- | -------------- |\n"
|
44 |
-
)
|
45 |
-
for exp in experiences:
|
46 |
-
report += f"| {exp.get('jobPostingDetails', 'N/A')} | {exp.get('cvDetails', 'N/A')} | {exp.get('explanation', '')} | **{exp.get('severityScore', 0)}** |\n"
|
47 |
-
report += "\n"
|
48 |
-
|
49 |
-
education = data.get("education", [])
|
50 |
-
if education:
|
51 |
-
report += "### Education\n\n"
|
52 |
-
report += (
|
53 |
-
"| Job Posting Requirement | CV Details | Explanation | Impact Score |\n"
|
54 |
-
)
|
55 |
-
report += (
|
56 |
-
"| ----------------------- | ---------- | ----------- | -------------- |\n"
|
57 |
-
)
|
58 |
-
for edu in education:
|
59 |
-
report += f"| {edu.get('jobPostingDetails', 'N/A')} | {edu.get('cvDetails', 'N/A')} | {edu.get('explanation', '')} | **{edu.get('severityScore', 0)}** |\n"
|
60 |
-
report += "\n"
|
61 |
-
|
62 |
-
responsibilities = data.get("responsibilities", [])
|
63 |
-
if responsibilities:
|
64 |
-
report += "### Responsibilities\n\n"
|
65 |
-
report += (
|
66 |
-
"| Job Posting Requirement | CV Details | Explanation | Impact Score |\n"
|
67 |
-
)
|
68 |
-
report += (
|
69 |
-
"| ----------------------- | ---------- | ----------- | -------------- |\n"
|
70 |
-
)
|
71 |
-
for resp in responsibilities:
|
72 |
-
report += f"| {resp.get('jobPostingDetails', 'N/A')} | {resp.get('cvDetails', 'N/A')} | {resp.get('explanation', '')} | **{resp.get('severityScore', 0)}** |\n"
|
73 |
-
report += "\n"
|
74 |
-
|
75 |
-
languages = data.get("languages", [])
|
76 |
-
if languages:
|
77 |
-
report += "### Languages\n\n"
|
78 |
-
report += (
|
79 |
-
"| Job Posting Requirement | CV Details | Explanation | Impact Score |\n"
|
80 |
-
)
|
81 |
-
report += (
|
82 |
-
"| ----------------------- | ---------- | ----------- | -------------- |\n"
|
83 |
-
)
|
84 |
-
for lang in languages:
|
85 |
-
report += f"| {lang.get('jobPostingDetails', 'N/A')} | {lang.get('cvDetails', 'N/A')} | {lang.get('explanation', '')} | **{lang.get('severityScore', 0)}** |\n"
|
86 |
-
report += "\n"
|
87 |
-
|
88 |
-
# Tools
|
89 |
-
tools = data.get("tools", [])
|
90 |
-
if tools:
|
91 |
-
report += "### Tools\n\n"
|
92 |
-
report += (
|
93 |
-
"| Job Posting Requirement | CV Details | Explanation | Impact Score |\n"
|
94 |
-
)
|
95 |
-
report += (
|
96 |
-
"| ----------------------- | ---------- | ----------- | -------------- |\n"
|
97 |
-
)
|
98 |
-
for tool in tools:
|
99 |
-
report += f"| {tool.get('jobPostingDetails', 'N/A')} | {tool.get('cvDetails', 'N/A')} | {tool.get('explanation', '')} | **{tool.get('severityScore', 0)}** |\n"
|
100 |
-
report += "\n"
|
101 |
-
|
102 |
-
# Closing
|
103 |
-
report += "---\n"
|
104 |
-
|
105 |
-
return report
|
106 |
-
|
107 |
-
|
108 |
def format_chat_history_cohere(chat_history: list, background_info: dict) -> list:
|
109 |
"""Takes streamlit chat history, and converts to cohere format"""
|
110 |
|
111 |
-
# Could use cohere to track history, maybe for the future
|
112 |
new_output = [
|
113 |
{
|
114 |
"role": "USER",
|
115 |
-
"message": f"Hi there! Here is my CV! {background_info['cv']}.\n\n I'd like you to act as a senior technical recruiter
|
116 |
},
|
117 |
{
|
118 |
"role": "CHATBOT",
|
@@ -124,6 +41,7 @@ def format_chat_history_cohere(chat_history: list, background_info: dict) -> lis
|
|
124 |
},
|
125 |
]
|
126 |
|
|
|
127 |
for item in chat_history:
|
128 |
new_output.append(
|
129 |
{
|
@@ -137,6 +55,9 @@ def format_chat_history_cohere(chat_history: list, background_info: dict) -> lis
|
|
137 |
|
138 |
if __name__ == "__main__":
|
139 |
example_json = """
|
140 |
-
|
|
|
|
|
|
|
141 |
"""
|
142 |
extract_json(example_json)
|
|
|
1 |
import json
|
2 |
+
from typing import Union
|
3 |
|
4 |
|
5 |
+
def extract_json(input_string: str) -> Union[bool, dict]:
|
6 |
"""String to Json function"""
|
7 |
|
8 |
# First, ensure we remove json wrapper
|
|
|
22 |
return False, {}
|
23 |
|
24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
def format_chat_history_cohere(chat_history: list, background_info: dict) -> list:
|
26 |
"""Takes streamlit chat history, and converts to cohere format"""
|
27 |
|
28 |
+
# TODO: Could use cohere to track history, maybe for the future
|
29 |
new_output = [
|
30 |
{
|
31 |
"role": "USER",
|
32 |
+
"message": f"Hi there! Here is my CV! {background_info['cv']}.\n\n I'd like you to act as a senior technical recruiter. Critique my CV and its' suitability for the role. You may only ask me one question at a time. Be as critical of the CV as possible, really focus on the CV and its relevance to the role. Ask about inconsistencies, mistakes in grammar, things which are technically not quite right suggesting a misunderstanding, or ask me to justify claims I make in my CV. You could also ask specific technical details about the architectures I have used.",
|
33 |
},
|
34 |
{
|
35 |
"role": "CHATBOT",
|
|
|
41 |
},
|
42 |
]
|
43 |
|
44 |
+
# Lazy approach to format it correctly for cohere input!
|
45 |
for item in chat_history:
|
46 |
new_output.append(
|
47 |
{
|
|
|
55 |
|
56 |
if __name__ == "__main__":
|
57 |
example_json = """
|
58 |
+
```json
|
59 |
+
{
|
60 |
+
"dogs": "are blue?"
|
61 |
+
}
|
62 |
"""
|
63 |
extract_json(example_json)
|
utils/gpt.py
CHANGED
@@ -1,9 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import cohere
|
2 |
from utils.format import format_chat_history_cohere
|
3 |
|
4 |
|
5 |
def test_api_key(api_key: str):
|
6 |
-
|
7 |
try:
|
8 |
# try to just generate 3 tokens
|
9 |
co = cohere.Client(
|
@@ -15,7 +21,9 @@ def test_api_key(api_key: str):
|
|
15 |
return False
|
16 |
|
17 |
|
18 |
-
def
|
|
|
|
|
19 |
"""Get response from Cohere and stream response"""
|
20 |
co = cohere.Client(
|
21 |
api_key=api_key,
|
@@ -32,20 +40,6 @@ def gpt_stream_response_chat_history(chat_history, background_info, api_key):
|
|
32 |
yield event.text
|
33 |
|
34 |
|
35 |
-
def gpt_stream_response(prompt: str, api_key: str):
|
36 |
-
"""Get response from Cohere and stream response"""
|
37 |
-
|
38 |
-
co = cohere.Client(
|
39 |
-
api_key=api_key,
|
40 |
-
)
|
41 |
-
|
42 |
-
stream = co.chat_stream(message=prompt)
|
43 |
-
|
44 |
-
for event in stream:
|
45 |
-
if event.event_type == "text-generation":
|
46 |
-
yield event.text
|
47 |
-
|
48 |
-
|
49 |
def gpt_response(prompt: str, api_key: str) -> str:
|
50 |
"""Get response from Cohere, with option to get output in json format"""
|
51 |
|
|
|
1 |
+
"""
|
2 |
+
GPT Related Functions
|
3 |
+
"""
|
4 |
+
|
5 |
+
from typing import List, Dict, Generator
|
6 |
+
|
7 |
import cohere
|
8 |
from utils.format import format_chat_history_cohere
|
9 |
|
10 |
|
11 |
def test_api_key(api_key: str):
|
12 |
+
"""Function to test Cohere API is working"""
|
13 |
try:
|
14 |
# try to just generate 3 tokens
|
15 |
co = cohere.Client(
|
|
|
21 |
return False
|
22 |
|
23 |
|
24 |
+
def stream(
|
25 |
+
background_info: str, chat_history: List[Dict[str, str]] = [], api_key: str = ""
|
26 |
+
) -> Generator:
|
27 |
"""Get response from Cohere and stream response"""
|
28 |
co = cohere.Client(
|
29 |
api_key=api_key,
|
|
|
40 |
yield event.text
|
41 |
|
42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
def gpt_response(prompt: str, api_key: str) -> str:
|
44 |
"""Get response from Cohere, with option to get output in json format"""
|
45 |
|
utils/process_doc.py
CHANGED
@@ -12,10 +12,11 @@ def parse_pdf(pdf_file) -> str:
|
|
12 |
"""Read PDF from Streamlit's file uploader"""
|
13 |
|
14 |
pdf_document = fitz.open("pdf", pdf_file)
|
|
|
15 |
|
16 |
all_text = []
|
17 |
|
18 |
-
for page_number in range(
|
19 |
|
20 |
page = pdf_document.load_page(page_number)
|
21 |
|
@@ -26,15 +27,12 @@ def parse_pdf(pdf_file) -> str:
|
|
26 |
return "\n\n".join(all_text)
|
27 |
|
28 |
|
29 |
-
def parse_docx(docx_file):
|
30 |
"""Read in docx file"""
|
31 |
docx_file = io.BytesIO(docx_file)
|
32 |
|
33 |
-
|
34 |
|
35 |
-
all_text = []
|
36 |
-
|
37 |
-
for paragraph in document.paragraphs:
|
38 |
-
all_text.append(paragraph.text)
|
39 |
|
40 |
return "\n".join(all_text)
|
|
|
12 |
"""Read PDF from Streamlit's file uploader"""
|
13 |
|
14 |
pdf_document = fitz.open("pdf", pdf_file)
|
15 |
+
n_pages = len(pdf_document)
|
16 |
|
17 |
all_text = []
|
18 |
|
19 |
+
for page_number in range(n_pages):
|
20 |
|
21 |
page = pdf_document.load_page(page_number)
|
22 |
|
|
|
27 |
return "\n\n".join(all_text)
|
28 |
|
29 |
|
30 |
+
def parse_docx(docx_file) -> str:
|
31 |
"""Read in docx file"""
|
32 |
docx_file = io.BytesIO(docx_file)
|
33 |
|
34 |
+
doc = Document(docx_file)
|
35 |
|
36 |
+
all_text = [para.text for para in doc.paragraphs]
|
|
|
|
|
|
|
37 |
|
38 |
return "\n".join(all_text)
|
utils/prompts.py
CHANGED
@@ -88,7 +88,6 @@ If you wish to leave a field blank in a given json, use "". You must never use n
|
|
88 |
Now respond with your professional, concise, answer.
|
89 |
"""
|
90 |
|
91 |
-
|
92 |
specific_skills_comparison_prompt = """
|
93 |
You are an expert at understanding how suitable a given CV is for a given job posting.
|
94 |
You will focus on evaluating the candidates experience quality
|
@@ -141,19 +140,3 @@ You must never use null as this will be loaded using json.loads! If there is not
|
|
141 |
|
142 |
Now respond with your professional, concise, answer.
|
143 |
"""
|
144 |
-
|
145 |
-
interviewer_prompt = """
|
146 |
-
**Goal**
|
147 |
-
You are an interviewer, and will carry out a natural interview
|
148 |
-
|
149 |
-
**Background Information**
|
150 |
-
The user has the following CV:
|
151 |
-
<cv>
|
152 |
-
|
153 |
-
The user is interested in the following job:
|
154 |
-
<job-posting>
|
155 |
-
|
156 |
-
You will act like an interviewer for that job, critiquing the user's CV and asking them questions. Follow the conversation naturally.
|
157 |
-
|
158 |
-
Ask either STAR reasoning, or technical questions, based on the job posting, or general job title.
|
159 |
-
"""
|
|
|
88 |
Now respond with your professional, concise, answer.
|
89 |
"""
|
90 |
|
|
|
91 |
specific_skills_comparison_prompt = """
|
92 |
You are an expert at understanding how suitable a given CV is for a given job posting.
|
93 |
You will focus on evaluating the candidates experience quality
|
|
|
140 |
|
141 |
Now respond with your professional, concise, answer.
|
142 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|