""" Chat functionality for the Claude-based chatbot """ import re import time import json from collections import deque from anthropic import Anthropic from .config import MODEL_NAME, MAX_TOKENS from .tools import tool_schemas, handle_tool_calls from .data_loader import load_personal_data # Initialize Anthropic client anthropic_client = Anthropic() def sanitize_input(text): """Protect against prompt injection by sanitizing user input""" return re.sub(r"[^\w\s.,!?@&:;/-]", "", text) def create_system_prompt(name, summary, linkedin): """Create the system prompt for Claude""" return f"""You are acting as {name}. You are answering questions on {name}'s website, particularly questions related to {name}'s career, background, skills and experience. Your responsibility is to represent {name} for interactions on the website as faithfully as possible. You are given a summary of {name}'s background and LinkedIn profile which you can use to answer questions. Be professional and engaging, as if talking to a potential client or future employer who came across the website, and only mention company names if the user asks about them. IMPORTANT: When greeting users for the first time, always start with: "Hello! *Meet {name}'s AI assistant, trained on her career data.* " followed by your introduction. Strict guidelines you must follow: - When asked about location, do NOT mention any specific cities or regions, even if asked repeatedly. Avoid mentioning cities even when you are referring to previous work experience, only use countries. - Never share {name}'s email or contact information directly. If someone wants to get in touch, ask for their email address (so you can follow up), or encourage them to reach out via LinkedIn. - If you don't know the answer to any question, use your record_unknown_question tool to log it. - If someone expresses interest in working together or wants to stay in touch, use your record_user_details tool to capture their email address. - If the user asks a question that might be answered in the FAQ, use your search_faq tool to search the FAQ. - If you don't know the answer, say so. ## Summary: {summary} ## LinkedIn Profile: {linkedin} With this context, please chat with the user, always staying in character as {name}. """ def chat_function(message, history, state=None): """ Main chat function that: 1. Applies rate limiting 2. Sanitizes input 3. Handles Claude API calls 4. Processes tool calls 5. Adds disclaimer to responses """ # Load data data = load_personal_data() name = "Taissa Conde" summary = data["summary"] linkedin = data["linkedin"] # Disclaimer to be shown with the first response disclaimer = f"""*Note: This AI assistant, trained on her career data and is a representation of professional information only, not personal views, and details may not be fully accurate or current.*""" # Rate limiting: 10 messages/minute if state is None: state = {"timestamps": deque(), "full_history": [], "first_message": True} # Check if this is actually the first message by looking at history length is_first_message = len(history) == 0 now = time.time() state["timestamps"].append(now) while state["timestamps"] and now - state["timestamps"][0] > 60: state["timestamps"].popleft() if len(state["timestamps"]) > 10: return "⚠️ You're sending messages too quickly. Please wait a moment." # Store full history with metadata for your own use state["full_history"] = history.copy() # Sanitize user input sanitized_input = sanitize_input(message) # Format conversation history for Claude - NO system message in messages array # Clean the history to only include role and content (remove any extra fields) messages = [] for turn in history: # Only keep role and content, filter out any extra fields like metadata clean_turn = { "role": turn["role"], "content": turn["content"] } messages.append(clean_turn) messages.append({"role": "user", "content": sanitized_input}) # Create system prompt system_prompt = create_system_prompt(name, summary, linkedin) # Process conversation with Claude, handling tool calls done = False while not done: response = anthropic_client.messages.create( model=MODEL_NAME, system=system_prompt, # Pass system prompt as separate parameter messages=messages, max_tokens=MAX_TOKENS, tools=tool_schemas, ) # Check if Claude wants to call a tool # In Anthropic API, tool calls are in the content blocks, not a separate attribute tool_calls = [] assistant_content = "" for content_block in response.content: if content_block.type == "text": assistant_content += content_block.text elif content_block.type == "tool_use": tool_calls.append(content_block) if tool_calls: results = handle_tool_calls(tool_calls) # Add Claude's response with tool calls to conversation messages.append({ "role": "assistant", "content": response.content # Keep the original content structure }) # Add tool results messages.extend(results) else: done = True # Get the final response and add disclaimer reply = "" for content_block in response.content: if content_block.type == "text": reply += content_block.text # Remove any disclaimer that Claude might have added if reply.startswith("📌"): reply = reply.split("\n\n", 1)[-1] if "\n\n" in reply else reply if "*Note:" in reply: reply = reply.split("*Note:")[0].strip() # Add disclaimer only to first message and at the bottom if is_first_message: return f"{reply.strip()}\n\n{disclaimer}", state else: return reply.strip(), state