Add CI workflow #27
12 changed files with 235 additions and 134 deletions
15
.github/workflows/ci.yaml
vendored
Normal file
15
.github/workflows/ci.yaml
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
name: CI
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
python:
|
||||||
|
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-sam.yaml@main
|
||||||
|
with:
|
||||||
|
source-dirs: "src"
|
||||||
|
run-sam-validate: false
|
||||||
|
typescript:
|
||||||
|
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@main
|
||||||
|
with:
|
||||||
|
run-cdk-synth: false
|
||||||
|
|
@ -4,6 +4,11 @@ from decimal import Decimal
|
||||||
|
|
||||||
import boto3
|
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):
|
class _DecimalEncoder(json.JSONEncoder):
|
||||||
def default(self, o):
|
def default(self, o):
|
||||||
|
|
@ -11,10 +16,6 @@ class _DecimalEncoder(json.JSONEncoder):
|
||||||
return int(o) if o == int(o) else float(o)
|
return int(o) if o == int(o) else float(o)
|
||||||
return super().default(o)
|
return super().default(o)
|
||||||
|
|
||||||
from shared.dynamo import get_conversation, save_conversation
|
|
||||||
from shared.secrets import get_slack_token
|
|
||||||
from shared.slack import update_message, post_message
|
|
||||||
from conversation.tools import TOOLS, dispatch_tool, set_tool_context
|
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
|
|
@ -99,15 +100,21 @@ def lambda_handler(event, context):
|
||||||
if placeholder_ts:
|
if placeholder_ts:
|
||||||
update_message(channel, placeholder_ts, final_text, token)
|
update_message(channel, placeholder_ts, final_text, token)
|
||||||
elif coordination:
|
elif coordination:
|
||||||
post_message(channel, final_text, token,
|
post_message(
|
||||||
thread_ts=coordination["coord_thread_ts"])
|
channel, final_text, token, thread_ts=coordination["coord_thread_ts"]
|
||||||
|
)
|
||||||
elif thread_ts:
|
elif thread_ts:
|
||||||
post_message(channel, final_text, token, thread_ts=thread_ts)
|
post_message(channel, final_text, token, thread_ts=thread_ts)
|
||||||
else:
|
else:
|
||||||
post_message(channel, final_text, token)
|
post_message(channel, final_text, token)
|
||||||
|
|
||||||
logger.info("Conversation complete: channel=%s, thread=%s, turns=%d, is_dm=%s",
|
logger.info(
|
||||||
channel, thread_ts, len(messages), is_dm)
|
"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:
|
||||||
|
|
@ -143,14 +150,20 @@ def _run_conversation(messages, system):
|
||||||
for block in assistant_message["content"]:
|
for block in assistant_message["content"]:
|
||||||
tool_use = block.get("toolUse")
|
tool_use = block.get("toolUse")
|
||||||
if tool_use:
|
if tool_use:
|
||||||
logger.info("Tool call: %s(%s)", tool_use["name"], json.dumps(tool_use["input"]))
|
logger.info(
|
||||||
|
"Tool call: %s(%s)", tool_use["name"], json.dumps(tool_use["input"])
|
||||||
|
)
|
||||||
result = dispatch_tool(tool_use["name"], tool_use["input"])
|
result = dispatch_tool(tool_use["name"], tool_use["input"])
|
||||||
tool_results.append({
|
tool_results.append(
|
||||||
"toolResult": {
|
{
|
||||||
"toolUseId": tool_use["toolUseId"],
|
"toolResult": {
|
||||||
"content": [{"text": json.dumps(result, cls=_DecimalEncoder)}],
|
"toolUseId": tool_use["toolUseId"],
|
||||||
|
"content": [
|
||||||
|
{"text": json.dumps(result, cls=_DecimalEncoder)}
|
||||||
|
],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
messages.append({"role": "user", "content": tool_results})
|
messages.append({"role": "user", "content": tool_results})
|
||||||
|
|
||||||
return assistant_message
|
return assistant_message
|
||||||
|
|
@ -161,4 +174,8 @@ def _extract_text(message):
|
||||||
for block in message.get("content", []):
|
for block in message.get("content", []):
|
||||||
if "text" in block:
|
if "text" in block:
|
||||||
parts.append(block["text"])
|
parts.append(block["text"])
|
||||||
return "\n".join(parts) if parts else "I processed your request but have nothing to report."
|
return (
|
||||||
|
"\n".join(parts)
|
||||||
|
if parts
|
||||||
|
else "I processed your request but have nothing to report."
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,13 @@ from datetime import datetime, timezone
|
||||||
import boto3
|
import boto3
|
||||||
|
|
||||||
from shared.dynamo import (
|
from shared.dynamo import (
|
||||||
get_todays_messages, get_unanswered_threads, save_coordination,
|
get_todays_messages,
|
||||||
create_task, get_tasks, update_task_status, delete_task,
|
get_unanswered_threads,
|
||||||
|
save_coordination,
|
||||||
|
create_task,
|
||||||
|
get_tasks,
|
||||||
|
update_task_status,
|
||||||
|
delete_task,
|
||||||
)
|
)
|
||||||
from shared.gmail import get_authenticated_service, fetch_thread
|
from shared.gmail import get_authenticated_service, fetch_thread
|
||||||
from shared.secrets import get_config, get_slack_token
|
from shared.secrets import get_config, get_slack_token
|
||||||
|
|
@ -536,26 +541,38 @@ def _handle_search_inbox(inputs):
|
||||||
query = inputs["query"]
|
query = inputs["query"]
|
||||||
max_results = inputs.get("max_results", 10)
|
max_results = inputs.get("max_results", 10)
|
||||||
|
|
||||||
resp = service.users().messages().list(
|
resp = (
|
||||||
userId="me", q=query, maxResults=max_results
|
service.users()
|
||||||
).execute()
|
.messages()
|
||||||
|
.list(userId="me", q=query, maxResults=max_results)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
|
||||||
messages = []
|
messages = []
|
||||||
for msg_meta in resp.get("messages", []):
|
for msg_meta in resp.get("messages", []):
|
||||||
msg = service.users().messages().get(
|
msg = (
|
||||||
userId="me", id=msg_meta["id"], format="metadata",
|
service.users()
|
||||||
metadataHeaders=["From", "To", "Subject", "Date"],
|
.messages()
|
||||||
).execute()
|
.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"]}
|
headers = {h["name"]: h["value"] for h in msg["payload"]["headers"]}
|
||||||
messages.append({
|
messages.append(
|
||||||
"id": msg["id"],
|
{
|
||||||
"thread_id": msg["threadId"],
|
"id": msg["id"],
|
||||||
"from": headers.get("From", ""),
|
"thread_id": msg["threadId"],
|
||||||
"to": headers.get("To", ""),
|
"from": headers.get("From", ""),
|
||||||
"subject": headers.get("Subject", ""),
|
"to": headers.get("To", ""),
|
||||||
"date": headers.get("Date", ""),
|
"subject": headers.get("Subject", ""),
|
||||||
"snippet": msg.get("snippet", ""),
|
"date": headers.get("Date", ""),
|
||||||
})
|
"snippet": msg.get("snippet", ""),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return {"query": query, "count": len(messages), "messages": messages}
|
return {"query": query, "count": len(messages), "messages": messages}
|
||||||
|
|
||||||
|
|
@ -668,8 +685,10 @@ def _handle_get_calendar_events(inputs):
|
||||||
{
|
{
|
||||||
"id": e.get("id", ""),
|
"id": e.get("id", ""),
|
||||||
"summary": e.get("summary", "(no title)"),
|
"summary": e.get("summary", "(no title)"),
|
||||||
"start": e.get("start", {}).get("dateTime") or e.get("start", {}).get("date", ""),
|
"start": e.get("start", {}).get("dateTime")
|
||||||
"end": e.get("end", {}).get("dateTime") or e.get("end", {}).get("date", ""),
|
or e.get("start", {}).get("date", ""),
|
||||||
|
"end": e.get("end", {}).get("dateTime")
|
||||||
|
or e.get("end", {}).get("date", ""),
|
||||||
"attendees": [a.get("email", "") for a in e.get("attendees", [])],
|
"attendees": [a.get("email", "") for a in e.get("attendees", [])],
|
||||||
"location": e.get("location", ""),
|
"location": e.get("location", ""),
|
||||||
"status": e.get("status", ""),
|
"status": e.get("status", ""),
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,8 @@ def lambda_handler(event, context):
|
||||||
thread_msgs = fetch_thread(service, thread_id)
|
thread_msgs = fetch_thread(service, thread_id)
|
||||||
adam_replied_after = any(
|
adam_replied_after = any(
|
||||||
m["from_email"] == config["adam_email"]
|
m["from_email"] == config["adam_email"]
|
||||||
and m["internal_date"] > _iso_to_epoch_ms(thread.get("unanswered_since", ""))
|
and m["internal_date"]
|
||||||
|
> _iso_to_epoch_ms(thread.get("unanswered_since", ""))
|
||||||
for m in thread_msgs
|
for m in thread_msgs
|
||||||
)
|
)
|
||||||
if adam_replied_after:
|
if adam_replied_after:
|
||||||
|
|
@ -42,7 +43,9 @@ def lambda_handler(event, context):
|
||||||
normal_count = sum(1 for m in messages if m.get("classification") == "NORMAL")
|
normal_count = sum(1 for m in messages if m.get("classification") == "NORMAL")
|
||||||
low_count = sum(1 for m in messages if m.get("classification") == "LOW")
|
low_count = sum(1 for m in messages if m.get("classification") == "LOW")
|
||||||
|
|
||||||
blocks = build_daily_digest(high_items, bypassed, unanswered, normal_count, low_count)
|
blocks = build_daily_digest(
|
||||||
|
high_items, bypassed, unanswered, normal_count, low_count
|
||||||
|
)
|
||||||
send_dm(
|
send_dm(
|
||||||
config["adam_slack_user_id"],
|
config["adam_slack_user_id"],
|
||||||
blocks,
|
blocks,
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,26 @@ import logging
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from shared.secrets import get_config, get_slack_token
|
from shared.secrets import get_config, get_slack_token
|
||||||
from shared.gmail import get_authenticated_service, fetch_history, fetch_message, initial_sync
|
from shared.gmail import (
|
||||||
from shared.dynamo import message_exists, save_message, save_thread_state, get_sync_metadata, save_sync_metadata
|
get_authenticated_service,
|
||||||
|
fetch_history,
|
||||||
|
fetch_message,
|
||||||
|
initial_sync,
|
||||||
|
)
|
||||||
|
from shared.dynamo import (
|
||||||
|
message_exists,
|
||||||
|
save_message,
|
||||||
|
save_thread_state,
|
||||||
|
get_sync_metadata,
|
||||||
|
save_sync_metadata,
|
||||||
|
)
|
||||||
from shared.classify import classify_email, check_bypassed_workorder
|
from shared.classify import classify_email, check_bypassed_workorder
|
||||||
from shared.slack import send_dm, build_high_priority_alert
|
from shared.slack import send_dm, build_high_priority_alert
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
def lambda_handler(event, context):
|
def lambda_handler(event, context):
|
||||||
config = get_config()
|
config = get_config()
|
||||||
service = get_authenticated_service()
|
service = get_authenticated_service()
|
||||||
|
|
@ -47,27 +59,34 @@ def lambda_handler(event, context):
|
||||||
|
|
||||||
slack_ts = ""
|
slack_ts = ""
|
||||||
if classification["classification"] == "HIGH":
|
if classification["classification"] == "HIGH":
|
||||||
blocks = build_high_priority_alert({
|
blocks = build_high_priority_alert(
|
||||||
**msg,
|
{
|
||||||
"classification_reason": classification["reason"],
|
**msg,
|
||||||
})
|
"classification_reason": classification["reason"],
|
||||||
slack_ts = send_dm(
|
}
|
||||||
config["adam_slack_user_id"],
|
)
|
||||||
blocks,
|
slack_ts = (
|
||||||
f"HIGH: {msg['subject']}",
|
send_dm(
|
||||||
get_slack_token(),
|
config["adam_slack_user_id"],
|
||||||
) or ""
|
blocks,
|
||||||
|
f"HIGH: {msg['subject']}",
|
||||||
|
get_slack_token(),
|
||||||
|
)
|
||||||
|
or ""
|
||||||
|
)
|
||||||
results["high"] += 1
|
results["high"] += 1
|
||||||
|
|
||||||
save_message({
|
save_message(
|
||||||
**msg,
|
{
|
||||||
"classification": classification["classification"],
|
**msg,
|
||||||
"classification_reason": classification["reason"],
|
"classification": classification["classification"],
|
||||||
"bypassed_wo": bypassed,
|
"classification_reason": classification["reason"],
|
||||||
"classified_date": today,
|
"bypassed_wo": bypassed,
|
||||||
"notified": bool(slack_ts),
|
"classified_date": today,
|
||||||
"slack_ts": slack_ts,
|
"notified": bool(slack_ts),
|
||||||
})
|
"slack_ts": slack_ts,
|
||||||
|
}
|
||||||
|
)
|
||||||
save_thread_state(msg["thread_id"], msg, config["adam_email"])
|
save_thread_state(msg["thread_id"], msg, config["adam_email"])
|
||||||
|
|
||||||
results["processed"] += 1
|
results["processed"] += 1
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ def lambda_handler(event, context):
|
||||||
token = get_slack_token()
|
token = get_slack_token()
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
resp = requests.post(
|
resp = requests.post(
|
||||||
"https://slack.com/api/conversations.open",
|
"https://slack.com/api/conversations.open",
|
||||||
headers={"Authorization": f"Bearer {token}"},
|
headers={"Authorization": f"Bearer {token}"},
|
||||||
|
|
|
||||||
|
|
@ -38,14 +38,18 @@ def get_calendar_service():
|
||||||
|
|
||||||
|
|
||||||
def list_events(service, time_min, time_max, max_results=10):
|
def list_events(service, time_min, time_max, max_results=10):
|
||||||
result = service.events().list(
|
result = (
|
||||||
calendarId="primary",
|
service.events()
|
||||||
timeMin=time_min,
|
.list(
|
||||||
timeMax=time_max,
|
calendarId="primary",
|
||||||
maxResults=max_results,
|
timeMin=time_min,
|
||||||
singleEvents=True,
|
timeMax=time_max,
|
||||||
orderBy="startTime",
|
maxResults=max_results,
|
||||||
).execute()
|
singleEvents=True,
|
||||||
|
orderBy="startTime",
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
return result.get("items", [])
|
return result.get("items", [])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ def classify_email(message, vip_senders, vip_domains):
|
||||||
inferenceConfig={"temperature": 0.0, "maxTokens": 256},
|
inferenceConfig={"temperature": 0.0, "maxTokens": 256},
|
||||||
)
|
)
|
||||||
raw = resp["output"]["message"]["content"][0]["text"]
|
raw = resp["output"]["message"]["content"][0]["text"]
|
||||||
match = re.search(r'\{[^{}]*\}', raw)
|
match = re.search(r"\{[^{}]*\}", raw)
|
||||||
result = json.loads(match.group()) if match else json.loads(raw)
|
result = json.loads(match.group()) if match else json.loads(raw)
|
||||||
return {
|
return {
|
||||||
"classification": result.get("classification", "NORMAL"),
|
"classification": result.get("classification", "NORMAL"),
|
||||||
|
|
@ -75,7 +75,6 @@ def classify_email(message, vip_senders, vip_domains):
|
||||||
|
|
||||||
def check_bypassed_workorder(message, work_order_addresses):
|
def check_bypassed_workorder(message, work_order_addresses):
|
||||||
all_recipients = [
|
all_recipients = [
|
||||||
addr.lower()
|
addr.lower() for addr in message["to_emails"] + message["cc_emails"]
|
||||||
for addr in message["to_emails"] + message["cc_emails"]
|
|
||||||
]
|
]
|
||||||
return not any(wo in all_recipients for wo in work_order_addresses)
|
return not any(wo in all_recipients for wo in work_order_addresses)
|
||||||
|
|
|
||||||
|
|
@ -101,9 +101,7 @@ def save_thread_state(thread_id, message, adam_email):
|
||||||
|
|
||||||
|
|
||||||
def get_sync_metadata():
|
def get_sync_metadata():
|
||||||
resp = _get_table().get_item(
|
resp = _get_table().get_item(Key={"pk": "META#sync", "sk": "META"})
|
||||||
Key={"pk": "META#sync", "sk": "META"}
|
|
||||||
)
|
|
||||||
return resp.get("Item")
|
return resp.get("Item")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -139,34 +137,36 @@ def get_todays_messages(date_str):
|
||||||
|
|
||||||
|
|
||||||
def get_conversation(thread_ts):
|
def get_conversation(thread_ts):
|
||||||
resp = _get_table().get_item(
|
resp = _get_table().get_item(Key={"pk": f"CONV#{thread_ts}", "sk": "CONV"})
|
||||||
Key={"pk": f"CONV#{thread_ts}", "sk": "CONV"}
|
|
||||||
)
|
|
||||||
item = resp.get("Item")
|
item = resp.get("Item")
|
||||||
return item["messages"] if item else []
|
return item["messages"] if item else []
|
||||||
|
|
||||||
|
|
||||||
def save_conversation(thread_ts, messages):
|
def save_conversation(thread_ts, messages):
|
||||||
_get_table().put_item(Item={
|
_get_table().put_item(
|
||||||
"pk": f"CONV#{thread_ts}",
|
Item={
|
||||||
"sk": "CONV",
|
"pk": f"CONV#{thread_ts}",
|
||||||
"messages": messages,
|
"sk": "CONV",
|
||||||
"updated_at": datetime.now(timezone.utc).isoformat(),
|
"messages": messages,
|
||||||
"ttl": _ttl_90_days(),
|
"updated_at": datetime.now(timezone.utc).isoformat(),
|
||||||
})
|
"ttl": _ttl_90_days(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def save_coordination(channel, coord_thread_ts, conversation_thread_ts, started_by):
|
def save_coordination(channel, coord_thread_ts, conversation_thread_ts, started_by):
|
||||||
_get_table().put_item(Item={
|
_get_table().put_item(
|
||||||
"pk": f"COORD#{channel}#{coord_thread_ts}",
|
Item={
|
||||||
"sk": "COORD",
|
"pk": f"COORD#{channel}#{coord_thread_ts}",
|
||||||
"conversation_thread_ts": conversation_thread_ts,
|
"sk": "COORD",
|
||||||
"channel": channel,
|
"conversation_thread_ts": conversation_thread_ts,
|
||||||
"started_by": started_by,
|
"channel": channel,
|
||||||
"status": "active",
|
"started_by": started_by,
|
||||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
"status": "active",
|
||||||
"ttl": int(time.time()) + (7 * 24 * 60 * 60),
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||||
})
|
"ttl": int(time.time()) + (7 * 24 * 60 * 60),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_coordination(channel, coord_thread_ts):
|
def get_coordination(channel, coord_thread_ts):
|
||||||
|
|
@ -203,7 +203,8 @@ def get_unanswered_threads(threshold_hours):
|
||||||
items.extend(resp.get("Items", []))
|
items.extend(resp.get("Items", []))
|
||||||
|
|
||||||
return [
|
return [
|
||||||
item for item in items
|
item
|
||||||
|
for item in items
|
||||||
if datetime.fromisoformat(item["unanswered_since"]).timestamp() < cutoff
|
if datetime.fromisoformat(item["unanswered_since"]).timestamp() < cutoff
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -213,16 +214,18 @@ def get_unanswered_threads(threshold_hours):
|
||||||
|
|
||||||
def create_task(title, due_date=None):
|
def create_task(title, due_date=None):
|
||||||
task_id = str(ulid.new())
|
task_id = str(ulid.new())
|
||||||
_get_table().put_item(Item={
|
_get_table().put_item(
|
||||||
"pk": f"TASK#{task_id}",
|
Item={
|
||||||
"sk": "TASK",
|
"pk": f"TASK#{task_id}",
|
||||||
"title": title,
|
"sk": "TASK",
|
||||||
"due_date": due_date or "",
|
"title": title,
|
||||||
"status": "open",
|
"due_date": due_date or "",
|
||||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
"status": "open",
|
||||||
"completed_at": "",
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||||
"ttl": _ttl_90_days(),
|
"completed_at": "",
|
||||||
})
|
"ttl": _ttl_90_days(),
|
||||||
|
}
|
||||||
|
)
|
||||||
return task_id
|
return task_id
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timezone
|
|
||||||
|
|
||||||
import google.auth.transport.requests
|
import google.auth.transport.requests
|
||||||
from google.oauth2.credentials import Credentials
|
from google.oauth2.credentials import Credentials
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,7 @@ _config = None
|
||||||
def get_gmail_oauth():
|
def get_gmail_oauth():
|
||||||
global _gmail_oauth
|
global _gmail_oauth
|
||||||
if _gmail_oauth is None:
|
if _gmail_oauth is None:
|
||||||
resp = _secrets_client.get_secret_value(
|
resp = _secrets_client.get_secret_value(SecretId=os.environ["SECRET_GMAIL"])
|
||||||
SecretId=os.environ["SECRET_GMAIL"]
|
|
||||||
)
|
|
||||||
_gmail_oauth = json.loads(resp["SecretString"])
|
_gmail_oauth = json.loads(resp["SecretString"])
|
||||||
return _gmail_oauth
|
return _gmail_oauth
|
||||||
|
|
||||||
|
|
@ -37,9 +35,7 @@ def get_slack_credentials():
|
||||||
"""Returns { botToken, signingSecret, appToken }."""
|
"""Returns { botToken, signingSecret, appToken }."""
|
||||||
global _slack_creds
|
global _slack_creds
|
||||||
if _slack_creds is None:
|
if _slack_creds is None:
|
||||||
resp = _secrets_client.get_secret_value(
|
resp = _secrets_client.get_secret_value(SecretId=os.environ["SECRET_SLACK"])
|
||||||
SecretId=os.environ["SECRET_SLACK"]
|
|
||||||
)
|
|
||||||
_slack_creds = json.loads(resp["SecretString"])
|
_slack_creds = json.loads(resp["SecretString"])
|
||||||
return _slack_creds
|
return _slack_creds
|
||||||
|
|
||||||
|
|
@ -67,6 +63,8 @@ def get_config():
|
||||||
"vip_senders": json.loads(params.get("vip-senders", "[]")),
|
"vip_senders": json.loads(params.get("vip-senders", "[]")),
|
||||||
"vip_domains": json.loads(params.get("vip-domains", "[]")),
|
"vip_domains": json.loads(params.get("vip-domains", "[]")),
|
||||||
"work_order_addresses": json.loads(params.get("work-order-addresses", "[]")),
|
"work_order_addresses": json.loads(params.get("work-order-addresses", "[]")),
|
||||||
"unanswered_threshold_hours": int(params.get("unanswered-threshold-hours", "24")),
|
"unanswered_threshold_hours": int(
|
||||||
|
params.get("unanswered-threshold-hours", "24")
|
||||||
|
),
|
||||||
}
|
}
|
||||||
return _config
|
return _config
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
|
@ -46,23 +45,35 @@ def build_high_priority_alert(message):
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"type": "header",
|
"type": "header",
|
||||||
"text": {"type": "plain_text", "text": ":rotating_light: High-Priority Email"},
|
"text": {
|
||||||
|
"type": "plain_text",
|
||||||
|
"text": ":rotating_light: High-Priority Email",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "section",
|
"type": "section",
|
||||||
"fields": [
|
"fields": [
|
||||||
{"type": "mrkdwn", "text": f"*From*\n{message['from_name'] or message['from_email']}"},
|
{
|
||||||
|
"type": "mrkdwn",
|
||||||
|
"text": f"*From*\n{message['from_name'] or message['from_email']}",
|
||||||
|
},
|
||||||
{"type": "mrkdwn", "text": f"*Received*\n{time_ago}"},
|
{"type": "mrkdwn", "text": f"*Received*\n{time_ago}"},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "section",
|
"type": "section",
|
||||||
"text": {"type": "mrkdwn", "text": f"*{message['subject']}*\n{message['snippet'][:300]}"},
|
"text": {
|
||||||
|
"type": "mrkdwn",
|
||||||
|
"text": f"*{message['subject']}*\n{message['snippet'][:300]}",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "context",
|
"type": "context",
|
||||||
"elements": [
|
"elements": [
|
||||||
{"type": "mrkdwn", "text": f":brain: _{message.get('classification_reason', '')}_"},
|
{
|
||||||
|
"type": "mrkdwn",
|
||||||
|
"text": f":brain: _{message.get('classification_reason', '')}_",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -90,7 +101,10 @@ def build_daily_digest(high_items, bypassed, unanswered, normal_count, low_count
|
||||||
{
|
{
|
||||||
"type": "context",
|
"type": "context",
|
||||||
"elements": [
|
"elements": [
|
||||||
{"type": "mrkdwn", "text": f"{today_str} · {total} emails processed today"},
|
{
|
||||||
|
"type": "mrkdwn",
|
||||||
|
"text": f"{today_str} · {total} emails processed today",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{"type": "divider"},
|
{"type": "divider"},
|
||||||
|
|
@ -108,7 +122,9 @@ def build_daily_digest(high_items, bypassed, unanswered, normal_count, low_count
|
||||||
)
|
)
|
||||||
if len(high_items) > max_items:
|
if len(high_items) > max_items:
|
||||||
lines.append(f" _...and {len(high_items) - max_items} more_")
|
lines.append(f" _...and {len(high_items) - max_items} more_")
|
||||||
blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": "\n".join(lines)}})
|
blocks.append(
|
||||||
|
{"type": "section", "text": {"type": "mrkdwn", "text": "\n".join(lines)}}
|
||||||
|
)
|
||||||
blocks.append({"type": "divider"})
|
blocks.append({"type": "divider"})
|
||||||
|
|
||||||
if bypassed:
|
if bypassed:
|
||||||
|
|
@ -117,10 +133,14 @@ def build_daily_digest(high_items, bypassed, unanswered, normal_count, low_count
|
||||||
"_Sent directly to you without CC'ing work-orders@_",
|
"_Sent directly to you without CC'ing work-orders@_",
|
||||||
]
|
]
|
||||||
for item in bypassed[:max_items]:
|
for item in bypassed[:max_items]:
|
||||||
lines.append(f" · *{item['subject']}* from {item.get('from_name') or item['from_email']}")
|
lines.append(
|
||||||
|
f" · *{item['subject']}* from {item.get('from_name') or item['from_email']}"
|
||||||
|
)
|
||||||
if len(bypassed) > max_items:
|
if len(bypassed) > max_items:
|
||||||
lines.append(f" _...and {len(bypassed) - max_items} more_")
|
lines.append(f" _...and {len(bypassed) - max_items} more_")
|
||||||
blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": "\n".join(lines)}})
|
blocks.append(
|
||||||
|
{"type": "section", "text": {"type": "mrkdwn", "text": "\n".join(lines)}}
|
||||||
|
)
|
||||||
blocks.append({"type": "divider"})
|
blocks.append({"type": "divider"})
|
||||||
|
|
||||||
if unanswered:
|
if unanswered:
|
||||||
|
|
@ -137,22 +157,26 @@ def build_daily_digest(high_items, bypassed, unanswered, normal_count, low_count
|
||||||
)
|
)
|
||||||
if len(unanswered) > max_items:
|
if len(unanswered) > max_items:
|
||||||
lines.append(f" _...and {len(unanswered) - max_items} more_")
|
lines.append(f" _...and {len(unanswered) - max_items} more_")
|
||||||
blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": "\n".join(lines)}})
|
blocks.append(
|
||||||
|
{"type": "section", "text": {"type": "mrkdwn", "text": "\n".join(lines)}}
|
||||||
|
)
|
||||||
blocks.append({"type": "divider"})
|
blocks.append({"type": "divider"})
|
||||||
|
|
||||||
blocks.append({
|
blocks.append(
|
||||||
"type": "context",
|
{
|
||||||
"elements": [
|
"type": "context",
|
||||||
{
|
"elements": [
|
||||||
"type": "mrkdwn",
|
{
|
||||||
"text": (
|
"type": "mrkdwn",
|
||||||
f":bar_chart: Today: {len(high_items)} high · "
|
"text": (
|
||||||
f"{normal_count} normal · {low_count} low · "
|
f":bar_chart: Today: {len(high_items)} high · "
|
||||||
f"{len(bypassed)} bypassed WO · {len(unanswered)} unanswered"
|
f"{normal_count} normal · {low_count} low · "
|
||||||
),
|
f"{len(bypassed)} bypassed WO · {len(unanswered)} unanswered"
|
||||||
}
|
),
|
||||||
],
|
}
|
||||||
})
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return blocks
|
return blocks
|
||||||
|
|
||||||
|
|
|
||||||
Reference in a new issue