Skip to content

Commit ade65b8

Browse files
committed
Merge branch 'main' into dev/backend
# Conflicts: # be_repo/modules/job_recommendation_system.py # be_repo/modules/recommendation_generator.py # be_repo/modules/retrieval_engine.py # be_repo/modules/view.py
2 parents 3f5c499 + 906cf53 commit ade65b8

File tree

16 files changed

+260
-138
lines changed

16 files changed

+260
-138
lines changed

.github/workflows/ci_be.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,14 @@ jobs:
6262
run: |
6363
sed -i '/<!-- Pytest Coverage Comment:Begin -->/,/<!-- Pytest Coverage Comment:End -->/c\<!-- Pytest Coverage Comment:Begin -->\n\${{ steps.coverageComment.outputs.coverageHtml }}\n<!-- Pytest Coverage Comment:End -->' ../README.md
6464
65+
- name: Debug current branch
66+
run: |
67+
echo "head ref: ${{ github.head_ref }} "
68+
echo "ref name: ${{ github.ref_name }} "
69+
6570
- name: Commit & Push changes to Readme
66-
uses: actions-js/push@master
71+
uses: ad-m/github-push-action@master
6772
with:
68-
branch: |
69-
${{ github.ref_name == 'main' && 'dev/backend' || github.ref_name.startsWith('be/') && github.ref_name || 'dev/backend' }}
73+
branch: ${{ github.head_ref || github.ref_name }}
7074
message: Update coverage on Readme
7175
github_token: ${{ secrets.GITHUB_TOKEN }}

be_repo/app.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from configs.database import get_resume_database, get_user_database
1010
from graphs.qa_graph import create_graph
1111
from modules.evaluator import evaluate_resume, evaluate_resume_with_jd
12+
from modules.job_recommendation_system import job_recommend
1213
from modules.langgraph_qa import get_answer_from_langgraph
1314
from modules.upload import upload_parse_resume
1415

@@ -159,9 +160,17 @@ def ask_question():
159160
return jsonify({"error": "No user ID provided."}), 400
160161
if not question:
161162
return jsonify({"error": "No question provided."}), 400
163+
# Load resume from database
164+
resume = resume_collection.find_one({"user_id": user_id})
165+
if not resume:
166+
return jsonify({"error": "No resume found for this user."}), 404
167+
168+
resume_text = resume.get('resume_text', '')
169+
if not resume_text:
170+
return jsonify({"error": "Resume text is empty."}), 400
162171

163172
# Get answer using LangGraph
164-
response = get_answer_from_langgraph(qa_graph, resume_collection, user_state_collection, user_id, question)
173+
response = get_answer_from_langgraph(qa_graph, resume_text, user_state_collection, user_id, question)
165174

166175
return jsonify({"response": response}), 200
167176

@@ -202,11 +211,43 @@ def interview_question_suggestion():
202211
if not user_id:
203212
return jsonify({"error": "No user ID provided."}), 400
204213

214+
# Load resume from database
215+
resume = resume_collection.find_one({"user_id": user_id})
216+
if not resume:
217+
return jsonify({"error": "No resume found for this user."}), 404
218+
219+
resume_text = resume.get('resume_text', '')
220+
if not resume_text:
221+
return jsonify({"error": "Resume text is empty."}), 400
222+
205223
# Get answer using LangGraph
206-
response = get_answer_from_langgraph(qa_graph, resume_collection, user_state_collection, user_id, prompt)
224+
response = get_answer_from_langgraph(qa_graph, resume_text, user_state_collection, user_id, prompt)
207225

208226
return jsonify({"response": response}), 200
209227

210228

229+
@app.route('/suggest/jobs', methods=['POST', 'OPTIONS'])
230+
def job_suggestion():
231+
if request.method == 'OPTIONS':
232+
return jsonify({'status': 'OK'}), 200
233+
234+
user_id = request.form.get('user_id')
235+
236+
if not user_id:
237+
return jsonify({"error": "No user ID provided."}), 400
238+
239+
# Load resume from database
240+
resume = resume_collection.find_one({"user_id": user_id})
241+
if not resume:
242+
return jsonify({"error": "No resume found for this user."}), 404
243+
244+
# Get answer using LangGraph
245+
resume_text = resume.get('resume_text', '')
246+
if not resume_text:
247+
return jsonify({"error": "Resume text is empty."}), 400
248+
249+
return jsonify({"response": job_recommend(resume_text, user_id)}), 200
250+
251+
211252
if __name__ == '__main__':
212253
app.run(host='0.0.0.0', debug=True, port=5000)
Lines changed: 23 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,23 @@
11
# job_recommendation_system.py
22

3-
from neo4j_model import Neo4jModel
4-
from resume_processor import ResumeProcessor
5-
from retrieval_engine import RetrievalEngine
6-
from recommendation_generator import RecommendationGenerator
7-
from view import CLIView
3+
from .neo4j_model import Neo4jModel
4+
from .resume_processor import ResumeProcessor
5+
from .retrieval_engine import RetrievalEngine
6+
from .recommendation_generator import RecommendationGenerator
7+
from .view import CLIView
8+
import logging
89
import sys
910

10-
def main():
11-
12-
# Redirect standard output to a file
13-
sys.stdout = open('output.log', 'w')
14-
15-
# Your code here
16-
print("Lots of output")
17-
18-
11+
def job_recommend(resume_text, user_id):
1912
# Setup Logging
20-
import logging
2113
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
2214
logger = logging.getLogger(__name__)
23-
15+
16+
# Get Resume Input from User
17+
if not resume_text.strip():
18+
logger.error(f'No resume text provided, user_id: {user_id}.')
19+
return 'Error: No resume text provided.'
20+
2421
# Neo4j Connection Details
2522
NEO4J_URI = "neo4j+ssc://7bf5a48e.databases.neo4j.io" # Replace with your Neo4j URI
2623
NEO4J_USERNAME = "neo4j" # Replace with your Neo4j username
@@ -32,44 +29,28 @@ def main():
3229
username=NEO4J_USERNAME,
3330
password=NEO4J_PASSWORD
3431
)
35-
32+
3633
node_label = "JTitle" # Adjust as needed; could be dynamic based on user input or other criteria
3734

3835
# Initialize Controller Components
3936
resume_processor = ResumeProcessor()
4037
retrieval_engine = RetrievalEngine(resume_processor, neo4j_model)
4138
recommendation_generator = RecommendationGenerator()
42-
39+
4340
# Initialize View
4441
view = CLIView()
45-
46-
# Get Resume Input from User
47-
resume_text = view.get_resume_input()
48-
49-
if not resume_text.strip():
50-
logger.error("No resume text provided.")
51-
print("Error: No resume text provided.")
52-
return
53-
54-
# Perform Mixed Retrieval for 'JD' Node Label
42+
43+
# Perform Mixed Retrieval
5544
similar_docs, graph_results = retrieval_engine.perform_mixed_retrieval(resume_text, node_label=node_label)
56-
45+
5746
if not similar_docs and not graph_results:
58-
print("No job recommendations found based on your resume.")
59-
return
60-
47+
return 'No job recommendations found based on your resume.'
48+
6149
# Generate Recommendations
6250
try:
6351
recommendations = recommendation_generator.generate_recommendations(similar_docs, graph_results)
6452
except Exception as e:
65-
print("Error: Failed to generate job recommendations.")
66-
return
67-
68-
# Display Recommendations
69-
view.display_recommendations(recommendations)
53+
return 'Error: Failed to generate job recommendations.'
7054

71-
# Close the file
72-
sys.stdout.close()
73-
74-
if __name__ == "__main__":
75-
main()
55+
# Display Recommendations
56+
return view.display_recommendations(recommendations)

be_repo/modules/langgraph_qa.py

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,24 @@
1-
def get_answer_from_langgraph(qa_graph, resume_collection, user_state_collection, user_id, question):
2-
resume = resume_collection.find_one({"user_id": user_id})
1+
def get_answer_from_langgraph(qa_graph, resume_text, user_state_collection, user_id, question):
32
user_state = user_state_collection.find_one({"user_id": user_id})
4-
resume_text = resume.get('resume_text', '')
5-
state = user_state.get('state', '')
63
thread_id = user_state.get('thread_id', '')
74

85
config = {"configurable": {"thread_id": user_id + thread_id}}
96

107
# If state is 0, send resume to LLM first
11-
if state == '0':
12-
events = qa_graph.stream(
13-
{"messages": [("user", resume_text)]}, config, stream_mode="values"
14-
)
15-
# Update state to 1
16-
new_state = {
17-
"user_id": user_id,
18-
"state": '1',
19-
"thread_id": thread_id
20-
}
21-
user_state_collection.replace_one({"user_id": user_id}, new_state, upsert=True)
22-
for event in events:
23-
if event["messages"][-1].type == "ai":
24-
print('User ask for the first time!')
8+
# if state == '0':
9+
events = qa_graph.stream(
10+
{"messages": [("user", resume_text)]}, config, stream_mode="values"
11+
)
12+
# Update state to 1
13+
new_state = {
14+
"user_id": user_id,
15+
"state": '1',
16+
"thread_id": thread_id
17+
}
18+
user_state_collection.replace_one({"user_id": user_id}, new_state, upsert=True)
19+
for event in events:
20+
if event["messages"][-1].type == "ai":
21+
print('User ask for the first time!')
2522
# Then send the question
2623
events = qa_graph.stream(
2724
{"messages": [("user", question)]}, config, stream_mode="values"
Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
# retrieval_engine.py
22

3-
from langchain_neo4j import GraphCypherQAChain
4-
from langchain_openai import ChatOpenAI
5-
from langchain.chains.retrieval import create_retrieval_chain
63
from langchain.chains.combine_documents import create_stuff_documents_chain
7-
from configs.openai_key import get_openai_api_key # New import
4+
from langchain.chains.retrieval import create_retrieval_chain
85
from langchain.prompts import PromptTemplate
96

7+
108
class RetrievalEngine:
119
def __init__(self, resume_processor, neo4j_model):
1210
"""
@@ -21,15 +19,18 @@ def __init__(self, resume_processor, neo4j_model):
2119

2220
# Initialize Language Model (already initialized in Neo4jModel)
2321
self.llm = self.neo4j_model.llm
24-
22+
2523
# Initialize GraphCypherQAChain (already initialized in Neo4jModel)
2624
self.graph_chain = self.neo4j_model.get_graph_chain()
2725

2826
# Define the PromptTemplate with 'context' as input variable
29-
template="""
30-
You are an assistant that matches resumes to relevant job descriptions.
27+
prompt = PromptTemplate(
28+
template="""
29+
You are an expert Cypher query writer for a Neo4j graph database.
3130
32-
Given the user's resume, find the most relevant job descriptions.
31+
Given the user's question, generate an efficient Cypher query that:
32+
- extract entities and relationships from the following resume.
33+
- Focus solely on the resume content.
3334
3435
**Entities to Extract:**
3536
- **Education (Edu):** Details about degrees, fields of study, institutions, start and end years, GPA.
@@ -39,19 +40,29 @@ def __init__(self, resume_processor, neo4j_model):
3940
- **Certifications (Cert):** Certification names, issuing organizations, expiration dates.
4041
- **Soft Skills (SSkill):** Non-technical skills like leadership, communication.
4142
43+
**Relationships to Identify:**
44+
- **UTILIZES_SKILL:** A Work Experience (WE) node utilizes a Skill (Skill) node.
45+
- **USES_TECH:** A Project (Proj) node uses a Skill (Skill) node as a technology.
46+
- **REL_TO (Proj to Skill):** A Project (Proj) node is related to a Skill (Skill) node.
47+
- **REL_TO (Skill to Skill):** A Skill (Skill) node is similar to another Skill (Skill) node.
48+
4249
**Resume:**
4350
\"\"\"
4451
{context}
4552
\"\"\"
46-
"""
47-
48-
self.prompt_template = PromptTemplate(
49-
template=template,
53+
""",
5054
input_variables=["input"]
5155
)
5256

5357
# Create a documents chain
54-
self.combine_docs_chain = create_stuff_documents_chain(self.llm, self.prompt_template)
58+
self.combine_docs_chain = create_stuff_documents_chain(self.llm, prompt=prompt)
59+
60+
# Initialize Retrieval Chain
61+
# Default node_label is 'JD'; can be adjusted as needed
62+
self.retrieval_chain = create_retrieval_chain(
63+
self.neo4j_model.get_retriever(node_label="JD"),
64+
self.combine_docs_chain
65+
)
5566

5667
def perform_mixed_retrieval(self, resume_text, node_label="JD"):
5768
"""
@@ -66,38 +77,30 @@ def perform_mixed_retrieval(self, resume_text, node_label="JD"):
6677
"""
6778
# Process resume into a Document
6879
doc = self.resume_processor.process_resume(resume_text)
69-
80+
7081
if not doc:
7182
return [], {}
72-
83+
7384
# Store the Document in the appropriate vector store
7485
self.neo4j_model.store_documents([doc], node_label=node_label)
75-
86+
7687
# Access the schema property correctly
7788
schema = self.neo4j_model.graph.get_schema
7889

79-
# Get the retriever for the given node label
80-
retriever = self.neo4j_model.get_retriever(node_label=node_label)
81-
82-
# Create the retrieval chain with the retriever and the combine_docs_chain
83-
retrieval_chain = create_retrieval_chain(
84-
retriever,
85-
self.combine_docs_chain
86-
)
87-
8890
# Perform vector similarity search
89-
similar_docs_result = retrieval_chain.invoke({"input": resume_text}) # Corrected to 'context'
91+
similar_docs_result = self.retrieval_chain.invoke({"input": resume_text}) # Corrected to 'context'
9092
similar_docs = similar_docs_result.get("output", [])
9193
print("similar_docs_result:", similar_docs_result)
9294
print("Keys in similar_docs_result:", similar_docs_result.keys())
93-
95+
9496
for doc in similar_docs:
9597
print("Document Metadata:", doc.metadata)
9698

97-
query = f"Based on the following resume, recommend relevant job positions: {resume_text}"
99+
query = (f"Based on the following resume, recommend relevant job positions based on skills and experience, "
100+
f"while ignoring the location: {resume_text}")
98101
graph_response = self.graph_chain.invoke({"query": query, "schema": schema})
99102
# After graph query
100103
print("Graph Response:")
101104
print(graph_response)
102-
103-
return similar_docs, graph_response
105+
106+
return similar_docs, graph_response

be_repo/modules/view.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ def display_recommendations(self, recommendations):
2828
Display job recommendations to the user.
2929
"""
3030
if not recommendations:
31-
print("No job recommendations found based on your resume.")
32-
return
33-
print("\nRecommended Jobs for You:")
31+
return 'No job recommendations found based on your resume.'
32+
res = '\nRecommended Jobs for You:\n'
3433
for idx, job in enumerate(recommendations, start=1):
35-
print(f"{idx}. {job}")
34+
res += f'{idx}. {job}\n'
35+
return res

0 commit comments

Comments
 (0)