updated feedback:

This commit is contained in:
2025-02-10 21:39:23 +00:00
11 changed files with 1729 additions and 343 deletions
+304
View File
@@ -0,0 +1,304 @@
import os
import requests
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.prompts.prompt import PromptTemplate
from langchain_core.output_parsers import StrOutputParser, JsonOutputParser
from langchain_openai import OpenAIEmbeddings
from langchain_core.documents import Document
from uuid import uuid4
import json
import getpass
import numpy as np
from concurrent.futures import ThreadPoolExecutor, as_completed
from sklearn.metrics.pairwise import cosine_similarity
from typing import List
import time
from datetime import datetime
import pytz
import logging
load_dotenv()
os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY")
llm_temp = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)
def generate_theme(conversation_data, resume, full_history, form_response=None, feedback=None, previous_result=None) -> dict:
try:
# Define prompt for summarizing and extracting the required fields
theme_prompt = PromptTemplate(
template="""
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
You are a Fire Fighter Interview preparation assistant that generates STARTPOP FORMAT based on user interaction with AI.
Your responsibilities include carefully analyzing user interactions, themes, resumes,Onboarding questions and answers and work history to generate detailed STARTPOP formats for specific themes.
### Context and Guidelines:
1. **Purpose**: Generate a single behavioral question with a detailed STARTPOP format.
2. **Input Sources**:
- Current theme
- User interaction with AI
- User resume
- Full work history
- Onboarding questions and answers for additional context
3. **Output Format**: JSON object with the following fields:
- `theme_title`: Title of the theme provided.
- `question`: Behavioral question aligned with the theme.
- `Situation`: A bulleted list (75-100 words).
- `Task`: A bulleted list (50 words).
- `Action`: A bulleted list (2 negative actions and 2 positive actions).
- `Results and Transitions`: A bulleted list (25-50 words).
- `Personal Lessons`: A bulleted list (25-50 words).
- `Observations of Others`: A bulleted list (25 words).
- `Professional Connection`: A bulleted list (25-50 words). Additionally:
- Connect to the theme of the question.
- Creatively express why you should be part of their team.
### Key Concepts in Firefighting:
Throughout most Probationary Firefighter Interviews, evaluators assess alignment with the **7 Main Concepts of Firefighting**:
- **High Performance Teams**
- **Situational Awareness**
- **Being a Great Problem Solver**
- **Customer Service**
- **Building Construction, Mechanical Aptitude**
- **Emergency Medicine Experience**
- **Mental and Physical Health**
Additionally, they evaluate communication skills, competence, and likability.
### 20 Important Themes:
These themes are used for behavioral questions:
- Customer Service
- Conflict
- Challenge
- Leadership
- Stress
- Successful Team
- Diversity
- Mistake
- Unsuccessful Team
- Disagreement
- Bent a Rule
- Delivered a Difficult Message
- Displayed Integrity
- Took a Shortcut
- Didnt Follow the Rules
- Emergency Response
- Dealt with Disabilities
- Solved a Big Problem
- Continuous Improvement
- Handled Sensitive Information
### Behavioral Question Starters:
Behavioral questions often begin with phrases like:
- "Tell me a time when..."
- "Can you tell me about a time when you..."
- "Describe a situation where you had to..."
- "Give me an example of how you..."
- "Have you ever been in a position where you needed to..."
- "Walk me through a time when you..."
### STARTPOP Framework:
The STARTPOP framework enhances the traditional STAR method. It includes:
1. **Situation**: Set up the scenario concisely (include dates, ages, places, and circumstances).
2. **Task**: Explain what needed to be done and why.
3. **Actions**: Outline both negative and positive approaches.
4. **Results and Transitions**: Share outcomes and ensure coherence.
5. **Personal Lessons**: Reflect on what you learned.
6. **Observations of Others**: Share insights about others involved.
7. **Professional Connection**: Relate the experience to firefighting and express your desire to join the team.
### Example STARTPOP:
**Question**: Tell me a time when you made a mistake and how you fixed it?
- **Situation**:
- In the Fall, my business, Tiger Building Services, does eavestrough cleaning.
- In 2019, we were working on a job late in the day, tired and running out of sunlight.
- I used handheld blowers without checking the wetness of debris, creating a muddy mess on the customer's deck.
- The customer was upset, and I realized my mistake.
- **Task**:
- Defuse the situation and clean up the mess quickly.
- Protect my company's reputation and ensure good customer experiences.
- **Actions**:
- Negative: Matching the customer's anger or ignoring the problem.
- Positive: Getting off the roof safely, apologizing, and switching strategies.
- Positive: Cleaning the gutters by hand and offering a free soft wash service.
- **Results and Transitions**:
- The job took longer than expected, but we waived fees due to the inconvenience.
- The customer was satisfied after our resolution plan.
- **Personal Lessons**:
- I learned to own up to mistakes, stay empathetic, and de-escalate tense situations.
- **Observations of Others**:
- People are entitled to their emotions, and following SOPs prevents mistakes.
- **Professional Connection**:
- Mistakes happen, but learning from them is crucial.
- I align with Markham Fire's values of transparency and accountability.
### JSON Output Requirements:
Generate a well-structured JSON output with the following fields:
- `theme_title`
- `question`
- `Situation`
- `Task`
- `Action`
- `Results and Transitions`
- `Personal Lessons`
- `Observations of Others`
- `Professional Connection`
### Review Process:
1. Ensure all news items align with the specified theme and meet relevance criteria.
2. Verify the JSON format is flawless, comprehensive, and well-structured.
### Additional Notes:
- You may be provided with feedback and previous results if the user is dissatisfied.
- Use this feedback to refine and regenerate the STARTPOP.
<|eot_id|><|start_header_id|>user<|end_header_id|>
Rules for Generating Each Component:
1. Situation: 75-100 words.
2. Task: 50 words.
3. Actions: 2 negative actions and 2 positive actions.
4. Results: 25-50 words.
5. Personal Lessons: 25-50 words.
6. Observations of Others: 25 words.
7. Professional Connection: 25-50 words + creative connection to the theme and team invitation.
NOTE: MAKE SURE THE OUT IS WELL DETAILED
CONVERSATION DATA: {conversation_data}
FEEDBACK: {feedback}
PREVIOUS RESULT: {previous_result}
USER RESUME: {resume}
FULL WORK HISTORY: {full_history}
Onboarding questions and answers for additional context: {form_response}
<|start_header_id|>assistant<|end_header_id|>
Return just the JSON output without any other explanation or comments.
Thank you for your thorough and precise processing!
""",
input_variables=["resume", "conversation_data", "feedback","form_response" "previous_result", "full_history"],
)
# Pipeline to process the prompt and parse output
theme_router = theme_prompt | llm_temp | JsonOutputParser()
# Call the pipeline and generate the cohesive output
output = theme_router.invoke({
"conversation_data": conversation_data,
"feedback": feedback,
"previous_result": previous_result,
"resume": resume,
"full_history": full_history,
"form_response":form_response
})
print(f"Output: {output}")
return output
except Exception as e:
print(f"Error: {e}")
return {}
from fastapi import Response, HTTPException, Depends
from typing import Optional
import os
import requests
import datetime
import base64 # For encoding the PDF content in Base64
@app.post("/rescue-career/generate-theme")
async def generate_pdf_endpoint(
request: GeneratePDFRequest,
api_key: str = Depends(get_api_key)
):
try:
# Fetch conversation data using the conversation_id
conversation_data = await get_conversation_data(request.conversation_id)
if not conversation_data:
raise HTTPException(
status_code=404,
detail=f"No conversation found with ID {request.conversation_id}"
)
resume_docs = ""
if request.resume_url:
docs = load_document(request.resume_url)
if not docs:
raise HTTPException(
status_code=400,
detail="Invalid resume URL: Unable to fetch document"
)
resume_docs = "\n".join(f"- {doc.page_content}" for doc in docs)
full_history_docs = ""
if request.full_history_url:
docs = load_document(request.full_history_url)
if not docs:
raise HTTPException(
status_code=400,
detail="Invalid full_history URL: Unable to fetch document"
)
full_history_docs = "\n".join(f"- {doc.page_content}" for doc in docs)
form_response_docs = ""
if request.form_id:
try:
x_api_key = os.getenv("BACKEND_XAPI_KEY")
url = f"{os.getenv('BACKEND_BASE_URL')}/v3/api/custom/theme-document/answer/{request.form_id}?x-project={x_api_key}"
result = requests.get(url)
form_response = result.json() # Return response in JSON format
form_response_docs = "\n".join(f"- {form_response}")
except:
raise HTTPException(
status_code=400,
detail="Unable to fetch onboarding data"
)
# Generate theme data using the generate_theme function
theme_data = generate_theme(
conversation_data=conversation_data,
feedback=request.feedback,
previous_result=request.previous_results,
resume=resume_docs,
form_response=form_response_docs,
full_history=full_history_docs
)
if not theme_data:
raise HTTPException(
status_code=500,
detail="Failed to generate theme data"
)
# Generate the PDF using the create_pdf function
pdf_content = create_pdf(theme_data)
# Encode the PDF content in Base64
pdf_base64 = base64.b64encode(pdf_content).decode("utf-8")
# Create filename with timestamp
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"theme_{timestamp}.pdf"
# Return both the PDF (as Base64) and the theme data in a JSON response
return {
"theme_data": theme_data,
"pdf": {
"filename": filename,
"content": pdf_base64
}
}
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Error generating PDF: {str(e)}"
)
+368
View File
@@ -0,0 +1,368 @@
import os
from typing import Optional
from fastapi import FastAPI, HTTPException, Security, Depends
from fastapi.security import APIKeyHeader
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from dotenv import load_dotenv
from utils.document_loader import load_document
import json
from pydantic import BaseModel
from src.llm import ai_chat
from langchain_openai import ChatOpenAI
import requests
import tempfile
from scripts.generate_pdf import create_pdf
from scripts.generate_theme import generate_theme
from scripts.generate_quiz import generate_quiz
from typing import Dict, Any
from fastapi.responses import Response
from datetime import datetime
from fastapi import HTTPException
from pydantic import BaseModel
from typing import Optional, Union, Dict, Any
import os
import requests
import os
from PyPDF2 import PdfReader
from config import QUIZ_TYPES
# Load environment variables
load_dotenv()
API_KEY = os.getenv("API_KEY_ACCESS")
base_path = os.path.join("data", "config_files")
QUESTIONS_PATH = os.path.join(base_path, "questions.json")
THEME_CONTEXT_PATH = os.path.join(base_path, "theme_context.json")
# Load themes at module level
with open(THEME_CONTEXT_PATH, "r") as f:
themes = json.load(f)
# Initialize FastAPI app
app = FastAPI(
title="Fire Fighter Interview API",
description="API For fire fighter",
version="1.0.0"
)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Setup API key authentication
api_key_header = APIKeyHeader(name="Authorization", auto_error=False)
async def get_api_key(api_key_header: str = Security(api_key_header)) -> str:
"""Validate API key from header"""
if not api_key_header or not api_key_header.startswith('Bearer '):
raise HTTPException(
status_code=401,
detail={"error": "Unauthorized", "message": "API key is missing or invalid."}
)
token = api_key_header.split(' ')[1]
if token != API_KEY:
raise HTTPException(
status_code=401,
detail={"error": "Unauthorized", "message": "API key does not match."}
)
return token
class ChatRequest(BaseModel):
resume_url: Optional[str] = None
query: str=None
conversation_id: str
theme_id: Optional[int] = 1
class ChatResponse(BaseModel):
message: str
end: bool
error: Optional[str] = None
class GeneratePDFRequest(BaseModel):
conversation_id: str
feedback: Optional[str] = None
previous_results: Optional[Dict[str, Any]] = None
resume_url: Optional[str] = None
full_history_url: Optional[str] = None
form_id:Optional[int] = None
class QuizRequest(BaseModel):
pdf_url: str
quiz_type: int # 1, 2, or 3 corresponding to QUIZ_TYPES
class QuizResponse(BaseModel):
success: bool
message: str
quiz_data: Optional[Dict[str, Any]] = None
error: Optional[str] = None
async def extract_pdf_text(pdf_url: str) -> Union[str, None]:
"""Extract text from PDF and handle potential errors."""
try:
response = requests.get(pdf_url)
response.raise_for_status()
# Create a temporary file
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_pdf:
temp_pdf.write(response.content)
temp_path = temp_pdf.name
# Extract text from PDF
reader = PdfReader(temp_path)
text = "\n\n".join(
page.extract_text() for page in reader.pages if page.extract_text()
)
# Clean up temporary file
os.unlink(temp_path)
if not text.strip():
return None
return text
except requests.RequestException as e:
raise HTTPException(
status_code=400,
detail=f"Error downloading PDF: {str(e)}"
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Error processing PDF: {str(e)}"
)
@app.post("/rescue-career/chat", response_model=ChatResponse)
async def chat_endpoint(
request: ChatRequest,
api_key: str = Depends(get_api_key)
):
try:
# Validate theme
matching_themes = [t for t in themes if t["id"] == request.theme_id]
if not matching_themes:
raise HTTPException(
status_code=400,
detail=f"No theme found with ID {request.theme_id}"
)
# Only try to load document if resume_url is provided
resume_docs = ""
if request.resume_url:
docs = load_document(request.resume_url)
if not docs:
raise HTTPException(
status_code=400,
detail="Invalid resume URL: Unable to fetch document"
)
resume_docs = "\n".join(f"- {doc.page_content}" for doc in docs)
# Get AI chat response
response = ai_chat(
query=request.query,
conversation_id=request.conversation_id,
theme_id=request.theme_id,
resume=resume_docs
)
# Parse response
try:
parsed_response = json.loads(response)
return ChatResponse(
message=parsed_response.get("message", ""),
end=parsed_response.get("end", "no") == "yes",
error=None
)
except json.JSONDecodeError:
return ChatResponse(
message=response,
end=False,
error=None
)
except HTTPException as e:
# Re-raise HTTP exceptions
raise
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Error processing chat request: {str(e)}"
)
@app.post("/rescue-career/generate-theme")
async def generate_pdf_endpoint(
request: GeneratePDFRequest,
api_key: str = Depends(get_api_key)
):
try:
# Here you would fetch the conversation data using the conversation_id
# This is a placeholder - replace with your actual conversation data fetching logic
conversation_data = await get_conversation_data(request.conversation_id)
if not conversation_data:
raise HTTPException(
status_code=404,
detail=f"No conversation found with ID {request.conversation_id}"
)
resume_docs = ""
if request.resume_url:
docs = load_document(request.resume_url)
if not docs:
raise HTTPException(
status_code=400,
detail="Invalid resume URL: Unable to fetch document"
)
resume_docs = "\n".join(f"- {doc.page_content}" for doc in docs)
full_history_docs = ""
if request.full_history_url:
docs = load_document(request.full_history_url)
if not docs:
raise HTTPException(
status_code=400,
detail="Invalid full_history URL: Unable to fetch document"
)
full_history_docs = "\n".join(f"- {doc.page_content}" for doc in docs)
form_response_docs = ""
if request.form_id:
try:
x_api_key = os.getenv("BACKEND_XAPI_KEY")
url = f"{os.getenv('BACKEND_BASE_URL')}/v3/api/custom/theme-document/answer/{request.form_id}?x-project={x_api_key}"
result = requests.get(url)
form_response = result.json() # Return response in JSON format
form_response_docs = "\n".join(f"- {form_response}")
except:
raise HTTPException(
status_code=400,
detail="Unable to fetch onborading data"
)
# Generate theme data using the generate_theme function
theme_data = generate_theme(
conversation_data=conversation_data,
feedback=request.feedback,
previous_result=request.previous_results,
resume = resume_docs,
form_response=form_response_docs,
full_history = full_history_docs
)
if not theme_data:
raise HTTPException(
status_code=500,
detail="Failed to generate theme data"
)
# Generate the PDF using the create_pdf function
pdf_content = create_pdf(theme_data)
# Create filename with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"theme_{timestamp}.pdf"
# Return the PDF as a response
return Response(
content=pdf_content,
media_type="application/pdf",
headers={
"Content-Disposition": f'attachment; filename="{filename}"'
}
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Error generating PDF: {str(e)}"
)
@app.post("/rescue-career/generate-quiz", response_model=QuizResponse)
async def generate_quiz_endpoint(
request: QuizRequest,
api_key: str = Depends(get_api_key)
):
"""Generate quiz based on PDF content and quiz type."""
# Validate quiz type
if request.quiz_type not in QUIZ_TYPES:
raise HTTPException(
status_code=400,
detail=f"Invalid quiz type. Must be one of: {list(QUIZ_TYPES)}"
)
try:
# Extract text from PDF
pdf_text = await extract_pdf_text(request.pdf_url)
if not pdf_text:
return QuizResponse(
success=False,
message="PDF extraction completed but no text content found",
error="Empty PDF content"
)
# Generate quiz using the existing function
quiz_data = generate_quiz(
startpop_pdf=pdf_text,
quiz_type=request.quiz_type
)
if not quiz_data:
return QuizResponse(
success=False,
message="Quiz generation failed",
error="Unable to generate quiz from the provided content"
)
return QuizResponse(
success=True,
message="Quiz generated successfully",
quiz_data=quiz_data
)
except HTTPException as he:
raise he
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Unexpected error during quiz generation: {str(e)}"
)
async def get_conversation_data(conversation_id: str) -> dict:
"""
Fetch conversation data using the conversation ID.
Replace this with your actual implementation to fetch conversation data.
"""
try:
storage_path = "conversations.json"
with open(storage_path, 'r') as f:
convs = json.load(f)
convs_id = convs[conversation_id]
return convs_id
except Exception as e:
print(f"Error fetching conversation data: {e}")
return None
@app.on_event("startup")
async def startup_event():
"""Initialize required components on startup"""
pass
if __name__ == "__main__":
import uvicorn
uvicorn.run("app:app", host="0.0.0.0", port=5048, reload=True)