Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 38 additions & 33 deletions chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
INBOX_CHAT_TOP = int(os.environ.get("INBOX_CHAT_TOP", "5"))
PREVIEW_CHARS = 280


def _read_local_settings() -> dict[str, str]:
if not SETTINGS_PATH.exists():
return {}
Expand Down Expand Up @@ -262,12 +263,14 @@ def _sample_snapshot() -> list[dict]:
except (OSError, json.JSONDecodeError):
continue
body = graph.get("body", {}).get("content", "") or ""
out.append({
"Subject": graph.get("subject", "") or "",
"From": graph.get("from", {}).get("emailAddress", {}).get("address", ""),
"BodyPreview": body[:200],
"Importance": graph.get("importance", "normal"),
})
out.append(
{
"Subject": graph.get("subject", "") or "",
"From": graph.get("from", {}).get("emailAddress", {}).get("address", ""),
"BodyPreview": body[:200],
"Importance": graph.get("importance", "normal"),
}
)
return out


Expand Down Expand Up @@ -353,10 +356,7 @@ def _build_prompt(agent_name: str) -> tuple[str, list[str]]:
if agent_name == "weekly_rule_suggestions":
if _run_mode("weekly_rule_suggestions") == "live":
owner = _mailbox_owner()
prompt = (
"Review this week's inbox activity now and email rule suggestions\n"
f"to the mailbox owner: {owner}."
)
prompt = f"Review this week's inbox activity now and email rule suggestions\nto the mailbox owner: {owner}."
return prompt, [f" Mode: 🟢 LIVE — suggestions emailed to {owner}"]
snapshot = _sample_snapshot()
rules_text = _load_vip_rules_text()
Expand Down Expand Up @@ -444,10 +444,14 @@ def _dict_is_error(d: dict) -> bool:

api_code = d.get("code") or d.get("Code")
message = d.get("message") or d.get("Message")
if isinstance(api_code, str) and message and re.search(
r"error|invalid|unauthor|forbidden|denied|badrequest|notfound|failed|fault",
api_code,
re.IGNORECASE,
if (
isinstance(api_code, str)
and message
and re.search(
r"error|invalid|unauthor|forbidden|denied|badrequest|notfound|failed|fault",
api_code,
re.IGNORECASE,
)
):
return True

Expand Down Expand Up @@ -510,14 +514,16 @@ def _tool_failed(call: dict) -> bool:
re.IGNORECASE,
):
return True
return bool(re.search(
r"maximum consecutive function call errors"
r"|(action|request|operation|tool call)[^.\n]{0,80}\bfailed\b"
r"|response status code[^.\n]{0,40}\b[45]\d\d\b"
r"|\b[45]\d\d\s+(unauthorized|forbidden|bad request|internal server error)",
text,
re.IGNORECASE,
))
return bool(
re.search(
r"maximum consecutive function call errors"
r"|(action|request|operation|tool call)[^.\n]{0,80}\bfailed\b"
r"|response status code[^.\n]{0,40}\b[45]\d\d\b"
r"|\b[45]\d\d\s+(unauthorized|forbidden|bad request|internal server error)",
text,
re.IGNORECASE,
)
)


def _render_result(agent_name: str, result: dict, elapsed: float) -> None:
Expand Down Expand Up @@ -558,11 +564,13 @@ def _render_result(agent_name: str, result: dict, elapsed: float) -> None:
print(f" {line}")

if mode == "dry_run":
stray = sorted({
call.get("tool_name", "")
for call in tool_calls
if re.search(r"office365_|teams_|SendEmail|PostMessage", call.get("tool_name", ""))
})
stray = sorted(
{
call.get("tool_name", "")
for call in tool_calls
if re.search(r"office365_|teams_|SendEmail|PostMessage", call.get("tool_name", ""))
}
)
if stray:
print("\n ⚠ DRY RUN violation: a connector tool was called:")
print(f" {', '.join(stray)}")
Expand All @@ -579,9 +587,8 @@ def _render_result(agent_name: str, result: dict, elapsed: float) -> None:
print(" stops further tool calls but still returns a (partial) summary.")
print(" Run `uv run func start --verbose` to see the exact connector error.")

live_blocked = (
mode != "dry_run"
and re.search(r"could not read|forbidden|unauthorized|not authoriz", response_text, re.IGNORECASE)
live_blocked = mode != "dry_run" and re.search(
r"could not read|forbidden|unauthorized|not authoriz", response_text, re.IGNORECASE
)
if live_blocked:
print("\n ⚠ LIVE could not reach your mailbox. The Outlook connection is almost")
Expand Down Expand Up @@ -755,9 +762,7 @@ async def _go() -> list[dict]:
async with tool:
exposed = {getattr(f, "name", "") for f in (tool.functions or [])}
if exposed != {ALLOWED_READ_OP}:
raise RuntimeError(
f"refusing to read: server exposed unexpected tools {sorted(exposed)}"
)
raise RuntimeError(f"refusing to read: server exposed unexpected tools {sorted(exposed)}")
res = await tool.call_tool(ALLOWED_READ_OP, top=top, fetchOnlyUnread=False)
text = ""
for c in res if isinstance(res, list) else [res]:
Expand Down
4 changes: 2 additions & 2 deletions infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ param modelVersion string = '2026-03-17'
@description('Model deployment SKU name.')
param modelSkuName string = 'GlobalStandard'

@description('Model deployment capacity.')
param modelCapacity int = 50
@description('Model deployment capacity (1000s of tokens per minute). The default of 150 is high enough that a single multi-step agent run (e.g. daily-briefing) will not hit 429s on the first try, and is well within the per-region/per-model quota most new subscriptions have. Lower it if you are quota-constrained; raise it for heavier multi-user workloads.')
param modelCapacity int = 150

@description('Name for the model deployment in Azure AI Services.')
param modelDeploymentName string = 'gpt-5.4-mini'
Expand Down
11 changes: 3 additions & 8 deletions tools/match_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@

_BACKTICK_RX = re.compile(r"`([^`]+)`")
_BOLD_RX = re.compile(r"\*\*")
_LABEL_RX = re.compile(
r"^-?\s*(trigger|condition|action|priority|safety|channel)\b[^:]*:", re.IGNORECASE
)
_LABEL_RX = re.compile(r"^-?\s*(trigger|condition|action|priority|safety|channel)\b[^:]*:", re.IGNORECASE)

# A route name is a short, lowercase identifier: letters, digits, and internal
# hyphens only. This keeps the env-suffix transform (`-` -> `_`, upper) free of
Expand Down Expand Up @@ -83,18 +81,15 @@ def _resolve_channel(route: str) -> dict[str, Any] | None:
"teams_recipient": {"groupId": team.strip(), "channelId": channel.strip()},
}


_rules_cache: tuple[tuple[str, int], str] | None = None


def _field(mail: dict[str, Any], *names: str) -> str:
for name in names:
value = mail.get(name)
if isinstance(value, dict):
value = (
value.get("emailAddress", {}).get("address")
or value.get("address")
or value.get("name")
)
value = value.get("emailAddress", {}).get("address") or value.get("address") or value.get("name")
if value:
return str(value)
return ""
Expand Down
Loading