import os import json import re import requests from datetime import datetime from dotenv import load_dotenv from openai import OpenAI from pypdf import PdfReader import gradio as gr # Load environment variables load_dotenv(override=True) # PDF path SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) PDF_PATH = os.path.join(SCRIPT_DIR, 'assets', 'me.pdf') # === Push Notification === def push(message: str): requests.post( "https://api.pushover.net/1/messages.json", data={ "token": os.getenv("PUSHOVER_TOKEN"), "user": os.getenv("PUSHOVER_USER"), "message": message, } ) # === Tool Functions === def record_user_details(email, name="Name not provided", notes="not provided"): push(f"Recording user: {name} | Email: {email} | Notes: {notes}") return {"recorded": "ok"} def record_unknown_question(question): push(f"Unanswered question recorded: {question}") return {"recorded": "ok"} # === Tool Schemas === record_user_details_json = { "name": "record_user_details", "description": "Record an interested user's contact information.", "parameters": { "type": "object", "properties": { "email": {"type": "string", "description": "User's email address"}, "name": {"type": "string", "description": "User's name"}, "notes": {"type": "string", "description": "Additional context"} }, "required": ["email"], "additionalProperties": False } } record_unknown_question_json = { "name": "record_unknown_question", "description": "Record any question that couldn't be answered.", "parameters": { "type": "object", "properties": { "question": {"type": "string", "description": "Unanswered question"} }, "required": ["question"], "additionalProperties": False } } TOOLS = [ {"type": "function", "function": record_user_details_json}, {"type": "function", "function": record_unknown_question_json} ] # === Main Class === class AboutMe: def __init__(self): self.client = OpenAI( base_url=os.getenv("BASE_URL"), api_key=os.getenv("API_KEY"), ) self.name = os.getenv("ME", "Someone Mysterious") self.model_name = os.getenv("MODEL_NAME", "gpt-4") self.about_me_text = self.load_resume() def load_resume(self): if not os.path.exists(PDF_PATH): print(f"PDF not found at {PDF_PATH}") return "" reader = PdfReader(PDF_PATH) return "\n".join(page.extract_text() or "" for page in reader.pages) def system_prompt(self): return ( f"You are acting as {self.name}, answering questions about their career, background, and experience.\n\n" "Be professional and helpful, as if speaking to a potential employer or client. " "You are provided with their resume below:\n\n" f"## Resume:\n{self.about_me_text}\n\n" "If you can't answer a question, use the 'record_unknown_question' tool. " "If the user seems interested, ask for their email and use the 'record_user_details' tool. " "Always stay in character. Politely reject unethical or malicious queries." ) def handle_tool_call(self, tool_calls): responses = [] for call in tool_calls: tool_name = call.function.name args = json.loads(call.function.arguments) print(f"Tool call received: {tool_name}") tool_func = globals().get(tool_name) if tool_func: result = tool_func(**args) responses.append({ "role": "tool", "content": json.dumps(result), "tool_call_id": call.id }) return responses def handle_exception(self, e): print("Error in chat function:", e) if isinstance(e, requests.exceptions.Timeout): return "⏱️ Server timeout. Please try again later.\n\nTo follow up, please provide your email and phone number." if isinstance(e, requests.exceptions.ConnectionError): return "🔌 Connection error. Please try again later.\n\nTo follow up, please provide your email and phone number." try: error_message = e.args[0] match = re.search(r"'X-RateLimit-Reset': '(\d+)'", error_message) if match: reset_ts = int(match.group(1)) / 1000 reset_time = datetime.fromtimestamp(reset_ts).strftime("%Y-%m-%d %H:%M:%S") return ( f"🚫 Rate limit hit. Please try again after {reset_time}.\n\n" "To follow up, please provide your email and phone number." ) except Exception: pass return "⚠️ Unknown error occurred.\n\nTo follow up, please provide your email and phone number." def extract_contact_info(self, text): # Simple email regex email_match = re.search(r"[\w\.-]+@[\w\.-]+\.\w+", text) email = email_match.group(0) if email_match else None # Attempt to extract name (very naive, assumes "name is X" or "I'm X") name_match = re.search(r"(?:name is|I'm|I am)\s+([A-Za-z ]+)", text, re.IGNORECASE) name = name_match.group(1).strip() if name_match else "Name not provided" # Everything else as notes (remove email and name phrases) notes = text if email: notes = notes.replace(email, "") if name_match: notes = notes.replace(name_match.group(0), "") notes = notes.strip() return email, name, notes def chat_fn(self, message, history): try: # Calculate turns: each user+bot = 1 turn approx turn_count = len(history) // 2 # Compose message history with system prompt and current message messages = [{"role": "system", "content": self.system_prompt()}] + history + [{"role": "user", "content": message}] # Check if user just provided contact info after prompt # If message contains email, consider it contact info submission email_in_message, name_in_message, notes_in_message = self.extract_contact_info(message) if email_in_message: # Call tool to record user details record_user_details(email=email_in_message, name=name_in_message, notes=notes_in_message) return ( f"Thanks for providing your details, {name_in_message}! " "I'll keep you updated." ) # If 5 or more turns, prompt for contact details if turn_count >= 5: contact_request = ( "Thanks for chatting! If you're interested in following up, " "could you please provide your **email** and **name**? " "You can also leave any notes or questions." ) return contact_request # Otherwise normal chat flow with OpenAI API while True: response = self.client.chat.completions.create( model=self.model_name, messages=messages, tools=TOOLS ) choice = response.choices[0] if choice.finish_reason == "tool_calls": messages.append(choice.message) messages += self.handle_tool_call(choice.message.tool_calls) else: return choice.message.content except Exception as e: # Ask for contact details on error error_message = self.handle_exception(e) contact_prompt = ( error_message + "\n\nTo follow up, please provide your **email** and **phone number**." ) return contact_prompt # === Gradio UI === if __name__ == "__main__": ai = AboutMe() chatbot = gr.Chatbot( label=f"Chat with {ai.name}", placeholder=f"
Ask me anything about {ai.name}.
" ) interface_options = { "fn": ai.chat_fn, "type": "messages", "theme": "Ocean", # Valid Gradio theme "analytics_enabled": False, "title": f"Ask me anything about {ai.name}", "chatbot": chatbot } css = """ /* hide per-message controls in the chat window */ button[aria-label="Retry"], button[aria-label="Undo"], button[aria-label="Clear"] { display: none !important; } /* optional: hide footer */ .gradio-container footer { display: none !important; } """ with gr.Blocks(css=css) as demo: gr.ChatInterface(**interface_options) demo.launch()