Rename to Lauren, add @mention support and channel-aware privacy
- Rename Exec Aide to Lauren across manifest and system prompt - Add app_mention handler for @mentions in channels and group DMs - Channel-aware system prompt: decline private info in public channels - Add coordination thread support (COORD# DynamoDB records) - Add search_inbox tool (full Gmail search via API) - Add coordinate_with_user tool (start threaded coordination) - Add post_message helper for flat/threaded channel replies - Fix Decimal serialization from DynamoDB in tool results - Use Slack mrkdwn formatting (single asterisks) instead of Markdown
This commit is contained in:
parent
aebf6c43c6
commit
0a4912aec7
6 changed files with 332 additions and 44 deletions
107
listener/app.py
107
listener/app.py
|
|
@ -1,19 +1,22 @@
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
import boto3
|
import boto3
|
||||||
from slack_bolt import App
|
from slack_bolt import App
|
||||||
from slack_bolt.adapter.socket_mode import SocketModeHandler
|
from slack_bolt.adapter.socket_mode import SocketModeHandler
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger("exec-aide")
|
logger = logging.getLogger("lauren")
|
||||||
|
|
||||||
secrets_client = boto3.client("secretsmanager")
|
secrets_client = boto3.client("secretsmanager")
|
||||||
ssm_client = boto3.client("ssm")
|
ssm_client = boto3.client("ssm")
|
||||||
lambda_client = boto3.client("lambda")
|
lambda_client = boto3.client("lambda")
|
||||||
|
dynamodb = boto3.resource("dynamodb")
|
||||||
|
|
||||||
_config = None
|
_config = None
|
||||||
|
_table = None
|
||||||
|
|
||||||
|
|
||||||
def get_slack_credentials():
|
def get_slack_credentials():
|
||||||
|
|
@ -41,26 +44,26 @@ def get_config():
|
||||||
return _config
|
return _config
|
||||||
|
|
||||||
|
|
||||||
creds = get_slack_credentials()
|
def _get_table():
|
||||||
app = App(token=creds["botToken"])
|
global _table
|
||||||
|
if _table is None:
|
||||||
|
_table = dynamodb.Table(os.environ["TABLE_NAME"])
|
||||||
|
return _table
|
||||||
|
|
||||||
|
|
||||||
@app.event("message")
|
def _get_coordination(channel, thread_ts):
|
||||||
def handle_message(event, say):
|
resp = _get_table().get_item(
|
||||||
if event.get("bot_id") or event.get("subtype"):
|
Key={"pk": f"COORD#{channel}#{thread_ts}", "sk": "COORD"}
|
||||||
return
|
)
|
||||||
|
item = resp.get("Item")
|
||||||
|
if item and item.get("status") == "active":
|
||||||
|
return item
|
||||||
|
return None
|
||||||
|
|
||||||
config = get_config()
|
|
||||||
if event.get("user") != config["adam_slack_user_id"]:
|
|
||||||
return
|
|
||||||
|
|
||||||
text = event.get("text", "").strip()
|
|
||||||
if not text:
|
|
||||||
return
|
|
||||||
|
|
||||||
thread_ts = event.get("thread_ts") or event["ts"]
|
|
||||||
channel = event["channel"]
|
|
||||||
|
|
||||||
|
def _dispatch_conversation(text, thread_ts, channel, say, is_dm):
|
||||||
|
placeholder_ts = None
|
||||||
|
if is_dm:
|
||||||
result = say(text="Thinking...", thread_ts=thread_ts)
|
result = say(text="Thinking...", thread_ts=thread_ts)
|
||||||
placeholder_ts = result["ts"]
|
placeholder_ts = result["ts"]
|
||||||
|
|
||||||
|
|
@ -72,13 +75,81 @@ def handle_message(event, say):
|
||||||
"thread_ts": thread_ts,
|
"thread_ts": thread_ts,
|
||||||
"channel": channel,
|
"channel": channel,
|
||||||
"placeholder_ts": placeholder_ts,
|
"placeholder_ts": placeholder_ts,
|
||||||
|
"is_dm": is_dm,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _dispatch_coordination_reply(event, coord):
|
||||||
|
lambda_client.invoke(
|
||||||
|
FunctionName=os.environ["CONVERSATION_FN_ARN"],
|
||||||
|
InvocationType="Event",
|
||||||
|
Payload=json.dumps({
|
||||||
|
"user_message": event.get("text", ""),
|
||||||
|
"thread_ts": coord["conversation_thread_ts"],
|
||||||
|
"channel": event["channel"],
|
||||||
|
"is_dm": False,
|
||||||
|
"coordination": {
|
||||||
|
"coord_thread_ts": event["thread_ts"],
|
||||||
|
"reply_user_id": event.get("user"),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
creds = get_slack_credentials()
|
||||||
|
app = App(token=creds["botToken"])
|
||||||
|
|
||||||
|
|
||||||
|
@app.event("app_mention")
|
||||||
|
def handle_mention(event, say, context):
|
||||||
|
config = get_config()
|
||||||
|
if event.get("user") != config["adam_slack_user_id"]:
|
||||||
|
return
|
||||||
|
|
||||||
|
bot_id = context.get("bot_user_id", "")
|
||||||
|
text = re.sub(rf"<@{bot_id}>\s*", "", event.get("text", "")).strip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
|
||||||
|
thread_ts = event.get("thread_ts")
|
||||||
|
_dispatch_conversation(text, thread_ts, event["channel"], say, is_dm=False)
|
||||||
|
|
||||||
|
|
||||||
|
@app.event("message")
|
||||||
|
def handle_message(event, say):
|
||||||
|
if event.get("bot_id") or event.get("subtype"):
|
||||||
|
return
|
||||||
|
|
||||||
|
channel_type = event.get("channel_type", "")
|
||||||
|
|
||||||
|
if channel_type != "im":
|
||||||
|
thread_ts = event.get("thread_ts")
|
||||||
|
if not thread_ts:
|
||||||
|
return
|
||||||
|
|
||||||
|
coord = _get_coordination(event["channel"], thread_ts)
|
||||||
|
if not coord:
|
||||||
|
return
|
||||||
|
|
||||||
|
_dispatch_coordination_reply(event, coord)
|
||||||
|
return
|
||||||
|
|
||||||
|
config = get_config()
|
||||||
|
if event.get("user") != config["adam_slack_user_id"]:
|
||||||
|
return
|
||||||
|
|
||||||
|
text = event.get("text", "").strip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
|
||||||
|
thread_ts = event.get("thread_ts") or event["ts"]
|
||||||
|
_dispatch_conversation(text, thread_ts, event["channel"], say, is_dm=True)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
handler = SocketModeHandler(app, creds["appToken"])
|
handler = SocketModeHandler(app, creds["appToken"])
|
||||||
logger.info("Exec Aide listener starting (Socket Mode)")
|
logger.info("Lauren listener starting (Socket Mode)")
|
||||||
handler.start()
|
handler.start()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
display_information:
|
display_information:
|
||||||
name: Exec Aide
|
name: Lauren
|
||||||
description: Adam's personal executive assistant — inbox monitoring, triage, and more.
|
description: Executive assistant for Adam — inbox monitoring, triage, and coordination.
|
||||||
background_color: "#1a1a2e"
|
background_color: "#6B4984"
|
||||||
|
|
||||||
features:
|
features:
|
||||||
bot_user:
|
bot_user:
|
||||||
display_name: Exec Aide
|
display_name: Lauren
|
||||||
always_online: true
|
always_online: true
|
||||||
app_home:
|
app_home:
|
||||||
home_tab_enabled: false
|
home_tab_enabled: false
|
||||||
|
|
@ -15,15 +15,23 @@ features:
|
||||||
oauth_config:
|
oauth_config:
|
||||||
scopes:
|
scopes:
|
||||||
bot:
|
bot:
|
||||||
|
- app_mentions:read
|
||||||
|
- channels:history
|
||||||
- chat:write
|
- chat:write
|
||||||
|
- groups:history
|
||||||
- im:history
|
- im:history
|
||||||
- im:read
|
- im:read
|
||||||
- im:write
|
- im:write
|
||||||
|
- mpim:history
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
event_subscriptions:
|
event_subscriptions:
|
||||||
bot_events:
|
bot_events:
|
||||||
|
- app_mention
|
||||||
|
- message.channels
|
||||||
|
- message.groups
|
||||||
- message.im
|
- message.im
|
||||||
|
- message.mpim
|
||||||
org_deploy_enabled: false
|
org_deploy_enabled: false
|
||||||
socket_mode_enabled: true
|
socket_mode_enabled: true
|
||||||
token_rotation_enabled: false
|
token_rotation_enabled: false
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,20 @@
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
import boto3
|
import boto3
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
from shared.dynamo import get_conversation, save_conversation
|
from shared.dynamo import get_conversation, save_conversation
|
||||||
from shared.secrets import get_slack_token
|
from shared.secrets import get_slack_token
|
||||||
from shared.slack import update_message
|
from shared.slack import update_message, post_message
|
||||||
from conversation.tools import TOOLS, dispatch_tool
|
from conversation.tools import TOOLS, dispatch_tool, set_tool_context
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
|
|
@ -17,7 +25,7 @@ MODEL_ID = "us.anthropic.claude-sonnet-4-6"
|
||||||
MAX_TOOL_ITERATIONS = 10
|
MAX_TOOL_ITERATIONS = 10
|
||||||
|
|
||||||
SYSTEM_PROMPT = """\
|
SYSTEM_PROMPT = """\
|
||||||
You are Exec Aide, a personal executive assistant for Adam Moussa, President \
|
You are Lauren, a personal executive assistant for Adam Moussa, President \
|
||||||
of Sea Haven Industries (a facilities maintenance company).
|
of Sea Haven Industries (a facilities maintenance company).
|
||||||
|
|
||||||
You have access to Adam's email inbox. Use the available tools to answer \
|
You have access to Adam's email inbox. Use the available tools to answer \
|
||||||
|
|
@ -30,45 +38,89 @@ When referencing emails, always include the sender name and subject line. \
|
||||||
For time references, use relative terms (e.g., "2 hours ago", "yesterday").
|
For time references, use relative terms (e.g., "2 hours ago", "yesterday").
|
||||||
|
|
||||||
If Adam asks about something outside your current capabilities (calendar, \
|
If Adam asks about something outside your current capabilities (calendar, \
|
||||||
tasks, etc.), acknowledge it and let him know it's not available yet.\
|
tasks, etc.), acknowledge it and let him know it's not available yet.
|
||||||
|
|
||||||
|
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):
|
def lambda_handler(event, context):
|
||||||
user_message = event["user_message"]
|
user_message = event["user_message"]
|
||||||
thread_ts = event["thread_ts"]
|
thread_ts = event.get("thread_ts")
|
||||||
channel = event["channel"]
|
channel = event["channel"]
|
||||||
placeholder_ts = event["placeholder_ts"]
|
placeholder_ts = event.get("placeholder_ts")
|
||||||
|
is_dm = event.get("is_dm", True)
|
||||||
|
coordination = event.get("coordination")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
messages = get_conversation(thread_ts)
|
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}]})
|
messages.append({"role": "user", "content": [{"text": user_message}]})
|
||||||
|
|
||||||
assistant_message = _run_conversation(messages)
|
set_tool_context(channel, thread_ts)
|
||||||
|
assistant_message = _run_conversation(messages, system)
|
||||||
|
|
||||||
final_text = _extract_text(assistant_message)
|
final_text = _extract_text(assistant_message)
|
||||||
save_conversation(thread_ts, messages)
|
save_conversation(conv_key, messages)
|
||||||
update_message(channel, placeholder_ts, final_text, get_slack_token())
|
|
||||||
|
|
||||||
logger.info("Conversation complete: thread=%s, turns=%d", thread_ts, len(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:
|
except Exception:
|
||||||
logger.exception("Conversation failed for thread %s", thread_ts)
|
logger.exception("Conversation failed for thread %s", thread_ts)
|
||||||
try:
|
try:
|
||||||
update_message(
|
token = get_slack_token()
|
||||||
channel, placeholder_ts,
|
error_msg = "Sorry, I hit an error processing that. Try again in a moment."
|
||||||
"Sorry, I hit an error processing that. Try again in a moment.",
|
if placeholder_ts:
|
||||||
get_slack_token(),
|
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:
|
except Exception:
|
||||||
logger.exception("Failed to update error placeholder")
|
logger.exception("Failed to post error message")
|
||||||
|
|
||||||
|
|
||||||
def _run_conversation(messages):
|
def _run_conversation(messages, system):
|
||||||
assistant_message = None
|
assistant_message = None
|
||||||
for _ in range(MAX_TOOL_ITERATIONS):
|
for _ in range(MAX_TOOL_ITERATIONS):
|
||||||
response = _bedrock.converse(
|
response = _bedrock.converse(
|
||||||
modelId=MODEL_ID,
|
modelId=MODEL_ID,
|
||||||
system=[{"text": SYSTEM_PROMPT}],
|
system=[{"text": system}],
|
||||||
messages=messages,
|
messages=messages,
|
||||||
toolConfig={"tools": TOOLS},
|
toolConfig={"tools": TOOLS},
|
||||||
inferenceConfig={"maxTokens": 4096},
|
inferenceConfig={"maxTokens": 4096},
|
||||||
|
|
@ -88,7 +140,7 @@ def _run_conversation(messages):
|
||||||
tool_results.append({
|
tool_results.append({
|
||||||
"toolResult": {
|
"toolResult": {
|
||||||
"toolUseId": tool_use["toolUseId"],
|
"toolUseId": tool_use["toolUseId"],
|
||||||
"content": [{"text": json.dumps(result)}],
|
"content": [{"text": json.dumps(result, cls=_DecimalEncoder)}],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
messages.append({"role": "user", "content": tool_results})
|
messages.append({"role": "user", "content": tool_results})
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,23 @@ from datetime import datetime, timezone
|
||||||
|
|
||||||
import boto3
|
import boto3
|
||||||
|
|
||||||
from shared.dynamo import get_todays_messages, get_unanswered_threads
|
from shared.dynamo import (
|
||||||
|
get_todays_messages, get_unanswered_threads, save_coordination,
|
||||||
|
)
|
||||||
from shared.gmail import get_authenticated_service, fetch_thread
|
from shared.gmail import get_authenticated_service, fetch_thread
|
||||||
from shared.secrets import get_config
|
from shared.secrets import get_config, get_slack_token
|
||||||
|
from shared.slack import post_message
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_lambda_client = None
|
_lambda_client = None
|
||||||
|
_tool_context = {}
|
||||||
|
|
||||||
|
|
||||||
|
def set_tool_context(channel, thread_ts):
|
||||||
|
global _tool_context
|
||||||
|
_tool_context = {"channel": channel, "thread_ts": thread_ts}
|
||||||
|
|
||||||
|
|
||||||
TOOLS = [
|
TOOLS = [
|
||||||
{
|
{
|
||||||
|
|
@ -129,6 +139,50 @@ TOOLS = [
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"toolSpec": {
|
||||||
|
"name": "search_inbox",
|
||||||
|
"description": "Search Adam's Gmail inbox with a query. Supports Gmail search syntax: from:, to:, subject:, after:, before:, is:unread, label:, has:attachment, etc.",
|
||||||
|
"inputSchema": {
|
||||||
|
"json": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"query": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Gmail search query (e.g., 'from:jane after:2024/01/01 subject:invoice').",
|
||||||
|
},
|
||||||
|
"max_results": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Maximum results to return. Defaults to 10.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["query"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"toolSpec": {
|
||||||
|
"name": "coordinate_with_user",
|
||||||
|
"description": "Start a coordination thread to interact with someone in the channel. Posts a message in the channel and starts a threaded conversation with the mentioned person. Use when Adam asks you to coordinate, schedule, or communicate with someone.",
|
||||||
|
"inputSchema": {
|
||||||
|
"json": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"flat_message": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Brief message to post flat in the channel (e.g., 'Coordinating a meeting with Jane').",
|
||||||
|
},
|
||||||
|
"thread_message": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The message to post in the thread, @mentioning the person (use Slack format <@USER_ID>).",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["flat_message", "thread_message"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -153,7 +207,7 @@ def _format_email(msg):
|
||||||
"snippet": msg.get("snippet", ""),
|
"snippet": msg.get("snippet", ""),
|
||||||
"classification": msg.get("classification", ""),
|
"classification": msg.get("classification", ""),
|
||||||
"classification_reason": msg.get("classification_reason", ""),
|
"classification_reason": msg.get("classification_reason", ""),
|
||||||
"internal_date": msg.get("internal_date", 0),
|
"internal_date": int(msg.get("internal_date", 0)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -166,6 +220,8 @@ def dispatch_tool(tool_name, tool_input):
|
||||||
"search_emails_by_sender": _handle_search_sender,
|
"search_emails_by_sender": _handle_search_sender,
|
||||||
"get_email_thread_detail": _handle_thread_detail,
|
"get_email_thread_detail": _handle_thread_detail,
|
||||||
"trigger_daily_digest": _handle_trigger_digest,
|
"trigger_daily_digest": _handle_trigger_digest,
|
||||||
|
"search_inbox": _handle_search_inbox,
|
||||||
|
"coordinate_with_user": _handle_coordinate,
|
||||||
}
|
}
|
||||||
handler = handlers.get(tool_name)
|
handler = handlers.get(tool_name)
|
||||||
if not handler:
|
if not handler:
|
||||||
|
|
@ -277,3 +333,56 @@ def _handle_trigger_digest(inputs):
|
||||||
InvocationType="Event",
|
InvocationType="Event",
|
||||||
)
|
)
|
||||||
return {"status": "triggered", "message": "Daily digest has been triggered and will arrive shortly."}
|
return {"status": "triggered", "message": "Daily digest has been triggered and will arrive shortly."}
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_search_inbox(inputs):
|
||||||
|
service = get_authenticated_service()
|
||||||
|
query = inputs["query"]
|
||||||
|
max_results = inputs.get("max_results", 10)
|
||||||
|
|
||||||
|
resp = service.users().messages().list(
|
||||||
|
userId="me", q=query, maxResults=max_results
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
messages = []
|
||||||
|
for msg_meta in resp.get("messages", []):
|
||||||
|
msg = service.users().messages().get(
|
||||||
|
userId="me", id=msg_meta["id"], format="metadata",
|
||||||
|
metadataHeaders=["From", "To", "Subject", "Date"],
|
||||||
|
).execute()
|
||||||
|
headers = {h["name"]: h["value"] for h in msg["payload"]["headers"]}
|
||||||
|
messages.append({
|
||||||
|
"id": msg["id"],
|
||||||
|
"thread_id": msg["threadId"],
|
||||||
|
"from": headers.get("From", ""),
|
||||||
|
"to": headers.get("To", ""),
|
||||||
|
"subject": headers.get("Subject", ""),
|
||||||
|
"date": headers.get("Date", ""),
|
||||||
|
"snippet": msg.get("snippet", ""),
|
||||||
|
})
|
||||||
|
|
||||||
|
return {"query": query, "count": len(messages), "messages": messages}
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_coordinate(inputs):
|
||||||
|
ctx = _tool_context
|
||||||
|
if not ctx.get("channel"):
|
||||||
|
return {"error": "Coordination is only available in channels, not DMs."}
|
||||||
|
|
||||||
|
token = get_slack_token()
|
||||||
|
channel = ctx["channel"]
|
||||||
|
|
||||||
|
coord_ts = post_message(channel, inputs["flat_message"], token)
|
||||||
|
post_message(channel, inputs["thread_message"], token, thread_ts=coord_ts)
|
||||||
|
|
||||||
|
save_coordination(
|
||||||
|
channel=channel,
|
||||||
|
coord_thread_ts=coord_ts,
|
||||||
|
conversation_thread_ts=ctx.get("thread_ts", ""),
|
||||||
|
started_by="lauren",
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "coordinating",
|
||||||
|
"message": "Thread started. I'll follow up when they respond.",
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,38 @@ def save_conversation(thread_ts, messages):
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def save_coordination(channel, coord_thread_ts, conversation_thread_ts, started_by):
|
||||||
|
_get_table().put_item(Item={
|
||||||
|
"pk": f"COORD#{channel}#{coord_thread_ts}",
|
||||||
|
"sk": "COORD",
|
||||||
|
"conversation_thread_ts": conversation_thread_ts,
|
||||||
|
"channel": channel,
|
||||||
|
"started_by": started_by,
|
||||||
|
"status": "active",
|
||||||
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"ttl": int(time.time()) + (7 * 24 * 60 * 60),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def get_coordination(channel, coord_thread_ts):
|
||||||
|
resp = _get_table().get_item(
|
||||||
|
Key={"pk": f"COORD#{channel}#{coord_thread_ts}", "sk": "COORD"}
|
||||||
|
)
|
||||||
|
item = resp.get("Item")
|
||||||
|
if item and item.get("status") == "active":
|
||||||
|
return item
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def complete_coordination(channel, coord_thread_ts):
|
||||||
|
_get_table().update_item(
|
||||||
|
Key={"pk": f"COORD#{channel}#{coord_thread_ts}", "sk": "COORD"},
|
||||||
|
UpdateExpression="SET #s = :done",
|
||||||
|
ExpressionAttributeNames={"#s": "status"},
|
||||||
|
ExpressionAttributeValues={":done": "completed"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_unanswered_threads(threshold_hours):
|
def get_unanswered_threads(threshold_hours):
|
||||||
cutoff = datetime.now(timezone.utc).timestamp() - (threshold_hours * 3600)
|
cutoff = datetime.now(timezone.utc).timestamp() - (threshold_hours * 3600)
|
||||||
items = []
|
items = []
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,22 @@ def send_message_to_channel(channel, text, token):
|
||||||
logger.error("chat.postMessage failed: %s", data.get("error"))
|
logger.error("chat.postMessage failed: %s", data.get("error"))
|
||||||
|
|
||||||
|
|
||||||
|
def post_message(channel, text, token, thread_ts=None):
|
||||||
|
payload = {"channel": channel, "text": text}
|
||||||
|
if thread_ts:
|
||||||
|
payload["thread_ts"] = thread_ts
|
||||||
|
resp = requests.post(
|
||||||
|
"https://slack.com/api/chat.postMessage",
|
||||||
|
headers={"Authorization": f"Bearer {token}"},
|
||||||
|
json=payload,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
if not data.get("ok"):
|
||||||
|
raise RuntimeError(f"chat.postMessage failed: {data.get('error')}")
|
||||||
|
return data["ts"]
|
||||||
|
|
||||||
|
|
||||||
def update_message(channel, ts, text, token):
|
def update_message(channel, ts, text, token):
|
||||||
resp = requests.post(
|
resp = requests.post(
|
||||||
"https://slack.com/api/chat.update",
|
"https://slack.com/api/chat.update",
|
||||||
|
|
|
||||||
Reference in a new issue