import json import logging from decimal import Decimal import boto3 from conversation.tools import TOOLS, dispatch_tool, set_tool_context from shared.dynamo import get_conversation, save_conversation from shared.secrets import get_slack_token from shared.slack import update_message, post_message class _DecimalEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, Decimal): return int(o) if o == int(o) else float(o) return super().default(o) logger = logging.getLogger() logger.setLevel(logging.INFO) _bedrock = boto3.client("bedrock-runtime", region_name="us-east-1") MODEL_ID = "us.anthropic.claude-sonnet-4-6" MAX_TOOL_ITERATIONS = 10 SYSTEM_PROMPT = """\ You are Lauren, a personal executive assistant for Adam Moussa, President \ of Sea Haven Industries (a facilities maintenance company). You have access to Adam's email inbox. Use the available tools to answer \ questions about emails, find information, and provide inbox summaries. Be concise and direct. Adam is busy - give him the key information without \ unnecessary preamble. Use bullet points for lists of emails. When referencing emails, always include the sender name and subject line. \ For time references, use relative terms (e.g., "2 hours ago", "yesterday"). You can manage Adam's task list — create tasks, list them, mark them \ complete, or delete them. You have access to Adam's Google Calendar. You can view upcoming events, \ check availability, and create new events. Always confirm event details \ (time, attendees, title) with Adam before creating. Default timezone: \ America/New_York. Use ISO 8601 format for all times sent to calendar tools. Speak naturally and casually, like a capable coworker - not a bot or AI. \ Keep responses conversational. FORMATTING: You are posting in Slack, which uses mrkdwn (NOT standard Markdown). \ Use *single asterisks* for bold, _underscores_ for italic. \ Do NOT use **double asterisks** or __double underscores__ - they will not render. \ Bullet points with - or • are fine. Slack emoji codes like :warning: work.\ """ CHANNEL_ADDENDUM = """\ IMPORTANT: You are currently in a Slack channel visible to others - NOT a \ private DM. NEVER share Adam's private information (emails, inbox contents, \ calendar, schedule, or any personal data) in this context. You may only \ discuss information that Adam has explicitly shared in this conversation. \ If someone asks about Adam's private information, politely decline: \ "I can only discuss that with Adam directly." You CAN help with coordination tasks visible to the channel - scheduling \ meetings, answering general questions, and interacting with other people \ when Adam asks you to.\ """ def lambda_handler(event, context): user_message = event["user_message"] thread_ts = event.get("thread_ts") channel = event["channel"] placeholder_ts = event.get("placeholder_ts") is_dm = event.get("is_dm", True) coordination = event.get("coordination") try: system = SYSTEM_PROMPT if not is_dm: system += CHANNEL_ADDENDUM conv_key = thread_ts or channel messages = get_conversation(conv_key) messages.append({"role": "user", "content": [{"text": user_message}]}) set_tool_context(channel, thread_ts) assistant_message = _run_conversation(messages, system) final_text = _extract_text(assistant_message) save_conversation(conv_key, messages) token = get_slack_token() if placeholder_ts: update_message(channel, placeholder_ts, final_text, token) elif coordination: post_message( channel, final_text, token, thread_ts=coordination["coord_thread_ts"] ) elif thread_ts: post_message(channel, final_text, token, thread_ts=thread_ts) else: post_message(channel, final_text, token) logger.info( "Conversation complete: channel=%s, thread=%s, turns=%d, is_dm=%s", channel, thread_ts, len(messages), is_dm, ) except Exception: logger.exception("Conversation failed for thread %s", thread_ts) try: token = get_slack_token() error_msg = "Sorry, I hit an error processing that. Try again in a moment." if placeholder_ts: update_message(channel, placeholder_ts, error_msg, token) elif thread_ts: post_message(channel, error_msg, token, thread_ts=thread_ts) else: post_message(channel, error_msg, token) except Exception: logger.exception("Failed to post error message") def _run_conversation(messages, system): assistant_message = None for _ in range(MAX_TOOL_ITERATIONS): response = _bedrock.converse( modelId=MODEL_ID, system=[{"text": system}], messages=messages, toolConfig={"tools": TOOLS}, inferenceConfig={"maxTokens": 4096}, ) assistant_message = response["output"]["message"] messages.append(assistant_message) if response["stopReason"] != "tool_use": break tool_results = [] for block in assistant_message["content"]: tool_use = block.get("toolUse") if tool_use: logger.info( "Tool call: %s(%s)", tool_use["name"], json.dumps(tool_use["input"]) ) result = dispatch_tool(tool_use["name"], tool_use["input"]) tool_results.append( { "toolResult": { "toolUseId": tool_use["toolUseId"], "content": [ {"text": json.dumps(result, cls=_DecimalEncoder)} ], } } ) messages.append({"role": "user", "content": tool_results}) return assistant_message def _extract_text(message): parts = [] for block in message.get("content", []): if "text" in block: parts.append(block["text"]) return ( "\n".join(parts) if parts else "I processed your request but have nothing to report." )