# Main chatbot class extracted from app.py import os from openai import OpenAI from content import ContentStore from notifications.pushover import PushoverService from tools.definitions import TOOLS from tools.handler import ToolHandler from core.router import MessageRouter from config.prompts import build_system_prompt class Chatbot: """Main chatbot orchestration class""" def __init__(self, name: str = "Yuelin Liu"): self.name = name # Initialize OpenAI client self.openai = OpenAI( api_key=os.getenv("GOOGLE_API_KEY"), base_url="https://generativelanguage.googleapis.com/v1beta/openai/" ) # Initialize services self.pushover = PushoverService( token=os.getenv("PUSHOVER_TOKEN"), user=os.getenv("PUSHOVER_USER") ) self.tool_handler = ToolHandler(pushover_service=self.pushover) self.router = MessageRouter(self.openai) # Initialize content store self.content = ContentStore() # Put career.pdf + summary.txt here (and any other work docs) self.content.load_folder("me/career", "career") # Merge everything else (hobby/life/projects/education) into personal/ self.content.load_folder("me/personal", "personal") # Optional: quick startup log (comment out if noisy) self._log_loaded_docs() def build_context_for_mode(self, mode: str) -> str: """Build document context for the given mode""" domain = "career" if mode == "career" else "personal" return self.content.join_domain_text([domain]) def system_prompt(self, mode: str) -> str: """Generate system prompt for the given mode""" domain_text = self.build_context_for_mode(mode) return build_system_prompt(self.name, domain_text, mode) def chat(self, message: str, history: list) -> str: """Main chat entrypoint with guarded execution""" try: # 1) Route message route = self.router.classify(message) intent = route.get("intent", "career") # Determine mode if intent == "contact_exchange": mode = "career" # keep professional context for contact flows else: mode = "career" if intent == "career" else "personal" # 2) Check for immediate responses (boundaries, contact collection, pitch) immediate_response = self.router.get_response_for_route(message, route, mode) if immediate_response: return immediate_response # 3) Regular chat with tools enabled messages = [{"role": "system", "content": self.system_prompt(mode)}] \ + history + [{"role": "user", "content": message}] while True: response = self.openai.chat.completions.create( model="gemini-2.5-flash", messages=messages, tools=TOOLS, temperature=0.2, top_p=0.9 ) choice = response.choices[0] if choice.finish_reason == "tool_calls": results = self.tool_handler.handle_tool_calls(choice.message.tool_calls) messages.append(choice.message) messages.extend(results) continue return choice.message.content or "Thanks—I've noted that." except Exception as e: # Fail-closed, keep UI stable print(f"[FATAL] Chat turn failed: {e}", flush=True) return "Oops, something went wrong on my side. Please ask that again—I've reset my context." def _log_loaded_docs(self): """Optional: log loaded documents at startup""" by_domain = self.content.by_domain for domain, docs in by_domain.items(): print(f"[LOAD] Domain '{domain}': {len(docs)} document(s)") for d in docs: print(f" - {d.title}")