Basu03 commited on
Commit
282fa46
·
1 Parent(s): 6e106e0

initial commit

Browse files
.DS_Store ADDED
Binary file (6.15 kB). View file
 
app.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from src.graph import build_graph
3
+
4
+ def main():
5
+ # Build LangGraph workflow
6
+ app = build_graph()
7
+
8
+ def run_graph(current_state: dict) -> dict:
9
+ return app.invoke(current_state)
10
+
11
+ st.title("🤖 AI-Powered Excel Mock Interviewer (Phi-3-mini, Local LLM)")
12
+ st.markdown("Welcome! This tool uses a self-hosted open-source model to conduct the interview. Click 'Start New Interview' to begin.")
13
+
14
+ # Session State Initialization
15
+ if "interviewer_state" not in st.session_state:
16
+ st.session_state.interviewer_state = {
17
+ "interview_status": 0, "interview_history": [], "questions": [],
18
+ "question_index": 0, "evaluations": [], "final_feedback": "", "warnings": []
19
+ }
20
+
21
+ # Display chat history
22
+ for message in st.session_state.interviewer_state.get("interview_history", []):
23
+ role, text = message
24
+ with st.chat_message("user" if role == "human" else "assistant"):
25
+ st.markdown(text)
26
+
27
+ # Button handling
28
+ if st.button("Start New Interview"):
29
+ st.session_state.interviewer_state = {
30
+ "interview_status": 0, "interview_history": [], "questions": [],
31
+ "question_index": 0, "evaluations": [], "final_feedback": "", "warnings": []
32
+ }
33
+ new_state = run_graph(st.session_state.interviewer_state)
34
+ st.session_state.interviewer_state = new_state
35
+ st.rerun()
36
+
37
+ # Input handling
38
+ if st.session_state.interviewer_state.get("interview_status") == 1:
39
+ prompt = st.chat_input("Your answer...")
40
+ if prompt:
41
+ st.session_state.interviewer_state["interview_history"].append(("human", prompt))
42
+ new_state = run_graph(st.session_state.interviewer_state)
43
+ st.session_state.interviewer_state = new_state
44
+ st.rerun()
45
+ elif st.session_state.interviewer_state.get("interview_status") == 2:
46
+ st.success("Interview complete!")
47
+
48
+ if __name__ == "__main__":
49
+ main()
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ transformers>=4.41.0
2
+ torch
3
+ streamlit
4
+ langgraph
src/__init__.py ADDED
File without changes
src/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (205 Bytes). View file
 
src/__pycache__/graph.cpython-311.pyc ADDED
Binary file (1.74 kB). View file
 
src/__pycache__/interview_logic.cpython-311.pyc ADDED
Binary file (8.19 kB). View file
 
src/__pycache__/perplexity_detector.cpython-311.pyc ADDED
Binary file (2.91 kB). View file
 
src/graph.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langgraph.graph import StateGraph, END
2
+ from . import interview_logic
3
+
4
+ def build_graph():
5
+ """
6
+ Builds the LangGraph for the mock interview process.
7
+ """
8
+ workflow = StateGraph(dict)
9
+
10
+ # Add nodes (no changes here)
11
+ workflow.add_node("start_interview", interview_logic.start_interview)
12
+ workflow.add_node("ask_question", interview_logic.ask_question)
13
+ workflow.add_node("process_user_response", interview_logic.process_user_response)
14
+ workflow.add_node("generate_final_report", interview_logic.generate_final_report)
15
+
16
+ # Entry point (no changes here)
17
+ workflow.set_conditional_entry_point(
18
+ interview_logic.route_start_of_interview,
19
+ )
20
+
21
+ # --- THIS IS THE CRITICAL FIX ---
22
+
23
+ # The flow from start to the first question is correct.
24
+ workflow.add_edge("start_interview", "ask_question")
25
+
26
+ # After asking a question, the graph should STOP for that turn.
27
+ # This allows the UI to wait for user input.
28
+ workflow.add_edge("ask_question", END) # <--- THIS IS THE FIX
29
+
30
+ # The conditional edge should ONLY come from process_user_response
31
+ workflow.add_conditional_edges(
32
+ "process_user_response",
33
+ interview_logic.route_after_evaluation,
34
+ {
35
+ "ask_question": "ask_question",
36
+ "generate_final_report": "generate_final_report",
37
+ "terminate": END
38
+ }
39
+ )
40
+
41
+ # The final report node still ends the graph normally.
42
+ workflow.add_edge("generate_final_report", END)
43
+
44
+ # Compile the graph
45
+ return workflow.compile()
src/interview_logic.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/interview_logic.py
2
+
3
+ # No more external LLM libraries needed here!
4
+ from .perplexity_detector import is_ai_generated
5
+ from .local_llm_handler import get_llm_response
6
+
7
+ EXCEL_QUESTIONS = [
8
+ "What is the difference between the VLOOKUP and HLOOKUP functions in Excel?",
9
+ "Explain how to use the INDEX and MATCH functions together, and why you might prefer them over VLOOKUP.",
10
+ "Describe what a Pivot Table is and give an example of a scenario where it would be useful.",
11
+ "What is Conditional Formatting in Excel? Can you provide an example?",
12
+ ]
13
+
14
+ def start_interview(state: dict) -> dict:
15
+ # This function body remains largely the same
16
+ intro_message = "Welcome to the automated Excel skills assessment..."
17
+ return {
18
+ **state, "interview_status": 1, "interview_history": [("ai", intro_message)],
19
+ "questions": EXCEL_QUESTIONS, "question_index": 0, "evaluations": [], "warnings": [], "final_feedback": "",
20
+ }
21
+
22
+ def ask_question(state: dict) -> dict:
23
+ # This function remains the same
24
+ question_index = state["question_index"]
25
+ current_question = state["questions"][question_index]
26
+ history = state.get("interview_history", [])
27
+ history.append(("ai", current_question))
28
+ return { **state, "interview_history": history }
29
+
30
+ # --- MODIFIED FUNCTION ---
31
+ def process_user_response(state: dict) -> dict:
32
+ history = state.get("interview_history", [])
33
+ user_response = history[-1][1]
34
+
35
+ if is_ai_generated(user_response, threshold=35.0):
36
+ termination_message = "This interview will now be terminated..."
37
+ history.append(("ai", termination_message))
38
+ return {**state, "interview_history": history, "interview_status": 2}
39
+
40
+ current_question = state["questions"][state["question_index"]]
41
+
42
+ # Create the prompt manually for our local LLM
43
+ evaluation_prompt = (
44
+ "You are an expert evaluator. Concisely evaluate the following answer to an Excel interview question "
45
+ "based on its technical accuracy and clarity. Start the evaluation directly without conversational filler.\n\n"
46
+ f"Question: {current_question}\n"
47
+ f"Answer: {user_response}"
48
+ )
49
+
50
+ # Get the evaluation from our new local LLM handler
51
+ evaluation = get_llm_response(evaluation_prompt)
52
+
53
+ evaluations = state.get("evaluations", [])
54
+ evaluations.append(evaluation)
55
+
56
+ return {**state, "evaluations": evaluations, "question_index": state["question_index"] + 1}
57
+
58
+ def generate_final_report(state: dict) -> dict:
59
+ interview_transcript = "\n".join([f"{speaker.capitalize()}: {text}" for speaker, text in state['interview_history']])
60
+ evaluations_summary = "\n\n".join(f"Evaluation for Q{i+1}:\n{e}" for i, e in enumerate(state['evaluations']))
61
+
62
+ # Create the report prompt manually
63
+ report_prompt = (
64
+ "You are a career coach. Based on the interview transcript and evaluations below, write a brief, constructive performance summary. "
65
+ "Use Markdown for a 'Strengths' section and an 'Areas for Improvement' section.\n\n"
66
+ f"---TRANSCRIPT---\n{interview_transcript}\n\n---EVALUATIONS---\n{evaluations_summary}"
67
+ )
68
+
69
+ # Get the report from our local LLM handler
70
+ final_feedback = get_llm_response(report_prompt)
71
+
72
+ history = state.get("interview_history", [])
73
+ history.append(("ai", final_feedback))
74
+
75
+ return {**state, "final_feedback": final_feedback, "interview_history": history, "interview_status": 2}
76
+
77
+ # Routing logic does not need to change
78
+ def route_after_evaluation(state: dict):
79
+ if state.get("interview_status") == 2: return "terminate"
80
+ elif state["question_index"] >= len(state["questions"]): return "generate_final_report"
81
+ else: return "ask_question"
82
+
83
+ def route_start_of_interview(state: dict):
84
+ return "start_interview" if state.get("interview_status", 0) == 0 else "process_user_response"
src/local_llm_handler.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/local_llm_handler.py
2
+
3
+ import torch
4
+ from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
5
+ import streamlit as st
6
+
7
+ import os
8
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
9
+
10
+ @st.cache_resource
11
+ def load_llm_pipeline():
12
+ """
13
+ Loads and caches the local LLM pipeline using Phi-3-mini-4k-instruct.
14
+ Designed for Hugging Face Spaces (with upgraded CPU or T4 GPU).
15
+ """
16
+ print("--- Loading main LLM: microsoft/Phi-3-mini-4k-instruct ---")
17
+ model_name = "microsoft/phi-3-mini-4k-instruct"
18
+
19
+ # Load tokenizer and model
20
+ tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
21
+ model = AutoModelForCausalLM.from_pretrained(
22
+ model_name,
23
+ device_map="auto", # Automatically uses GPU if available
24
+ torch_dtype=torch.float32, # Use float16 for memory efficiency
25
+ trust_remote_code=True
26
+ )
27
+
28
+ # Build text generation pipeline
29
+ llm_pipeline = pipeline(
30
+ "text-generation",
31
+ model=model,
32
+ tokenizer=tokenizer,
33
+ max_new_tokens=300,
34
+ return_full_text=False
35
+ )
36
+
37
+ print("--- Phi-3-mini model loaded successfully ---")
38
+ return llm_pipeline
39
+
40
+
41
+ def get_llm_response(prompt: str) -> str:
42
+ """
43
+ Gets a response from the cached Phi-3-mini LLM pipeline.
44
+ """
45
+ llm_pipeline = load_llm_pipeline()
46
+ formatted_prompt = f"<|user|>\n{prompt}\n<|assistant|>"
47
+
48
+ print("AI: (Generating response with Phi-3-mini...)")
49
+ try:
50
+ outputs = llm_pipeline(formatted_prompt)
51
+ response = outputs[0]["generated_text"]
52
+ return response
53
+ except Exception as e:
54
+ print(f"Error during Phi-3-mini generation: {e}")
55
+ return "Sorry, I encountered an error while generating a response."
src/perplexity_detector.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/perplexity_detector.py
2
+
3
+ import torch
4
+ from transformers import AutoModelForCausalLM, AutoTokenizer
5
+ import streamlit as st
6
+
7
+ @st.cache_resource
8
+ def load_detector_model():
9
+ """Loads and caches the gpt-2 model for perplexity calculation."""
10
+ print("--- Loading detector model (gpt-2) for the first time... ---")
11
+ model_name = "gpt2"
12
+ model = AutoModelForCausalLM.from_pretrained(model_name)
13
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
14
+ print("--- Detector model loaded and cached. ---")
15
+ return model, tokenizer
16
+
17
+ def calculate_perplexity(text: str) -> float:
18
+ model, tokenizer = load_detector_model()
19
+ encodings = tokenizer(text, return_tensors="pt")
20
+ with torch.no_grad():
21
+ outputs = model(**encodings, labels=encodings["input_ids"])
22
+ neg_log_likelihood = outputs.loss
23
+ ppl = torch.exp(neg_log_likelihood)
24
+ return ppl.item()
25
+
26
+ def is_ai_generated(text: str, threshold: float = 45.0) -> bool:
27
+ if not text or len(text.split()) < 5: return False
28
+ print("AI: (Calculating perplexity...)")
29
+ perplexity = calculate_perplexity(text)
30
+ print(f"AI: (Perplexity score: {perplexity:.2f}, Threshold: {threshold})")
31
+ return perplexity < threshold