Route calendar action requests to tools
Co-authored-by: Alex Kenley <Alex.Kenley@threatvectorsecurity.com>
This commit is contained in:
@@ -35,6 +35,7 @@ from routes.chat_helpers import (
|
||||
clean_thinking_for_save,
|
||||
_enforce_chat_privileges,
|
||||
)
|
||||
from src.action_intents import message_needs_tools as _message_needs_tools
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -55,40 +56,6 @@ def _stream_set(session_id: str, **fields) -> None:
|
||||
rec.update(fields)
|
||||
|
||||
|
||||
import re as _re
|
||||
# Phrases that clearly signal the user wants to create a todo / reminder /
|
||||
# calendar event. When any of these hit in plain chat mode we silently
|
||||
# escalate to the agent loop so manage_notes / manage_calendar are in scope.
|
||||
_TOOL_INTENT_PATTERNS = [
|
||||
_re.compile(r"\bremind\s+me\b", _re.I),
|
||||
_re.compile(r"\badd\s+(a\s+|an\s+)?(todo|task|reminder)\b", _re.I),
|
||||
_re.compile(r"\b(create|schedule|book)\s+(a\s+|an\s+)?(event|meeting|appointment|reminder|call)\b", _re.I),
|
||||
_re.compile(r"\bput\s+.+\bon\s+(my\s+)?calendar\b", _re.I),
|
||||
_re.compile(r"\b(todo|reminder)\s*:", _re.I),
|
||||
_re.compile(r"\bmake\s+(a\s+|an\s+)?(note|todo|reminder)\b", _re.I),
|
||||
# Email intent — "write/send/email/message [someone]", "write hi to X"
|
||||
_re.compile(r"\b(write|send)\s+.{1,30}\bto\s+\w+", _re.I),
|
||||
_re.compile(r"\b(send|write|reply)\s+(an?\s+)?(email|message|mail)\b", _re.I),
|
||||
_re.compile(r"\b(email|message)\s+\w+\b", _re.I),
|
||||
_re.compile(r"\bcheck\s+(my\s+)?(email|inbox|mail)\b", _re.I),
|
||||
_re.compile(r"\bunread\s+(email|mail)s?\b", _re.I),
|
||||
# Shell / remote-host intent — covers the deepseek "can you ssh into X"
|
||||
# case. We escalate to agent so `bash` is available; the model can still
|
||||
# decide it doesn't need to actually run anything.
|
||||
_re.compile(r"\bssh\s+(in)?to\b", _re.I),
|
||||
_re.compile(r"\bssh\s+\w+", _re.I),
|
||||
_re.compile(r"\b(run|execute)\s+.{1,40}\bon\s+\w+", _re.I),
|
||||
_re.compile(r"\b(can|could|please|would)\s+you\s+(run|execute|exec)\b", _re.I),
|
||||
_re.compile(r"\b(deploy|build|install|restart|reboot|kill|tail|grep|cat|ls|cd|cp|mv|rm)\b\s+\S+", _re.I),
|
||||
_re.compile(r"\b(check|see)\s+(if|whether|what)\s+.{1,40}\b(running|process|service|port|file|exists?)\b", _re.I),
|
||||
]
|
||||
|
||||
def _message_needs_tools(text: str) -> bool:
|
||||
if not text:
|
||||
return False
|
||||
return any(p.search(text) for p in _TOOL_INTENT_PATTERNS)
|
||||
|
||||
|
||||
def _session_url_matches_endpoint(session_url: str, endpoint_base: str) -> bool:
|
||||
if not session_url or not endpoint_base:
|
||||
return False
|
||||
|
||||
76
src/action_intents.py
Normal file
76
src/action_intents.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""Lightweight routing hints for chat requests that need tools.
|
||||
|
||||
These patterns are intentionally conservative. They only promote plain chat
|
||||
to agent mode when the user asks the assistant to take an action, not when the
|
||||
user asks how a feature works.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Iterable, Pattern
|
||||
|
||||
|
||||
_ACTION_QUESTION = r"\b(?:can|could|would|will)\s+you\s+"
|
||||
_PLEASE = r"^\s*(?:please\s+)?"
|
||||
|
||||
_CALENDAR_ACTION = r"(?:add|create|schedule|book|put|set\s+up|make)"
|
||||
_CALENDAR_THING = r"(?:calendar|calendar\s+(?:entry|item)|event|meeting|appointment|entry|call)"
|
||||
|
||||
_PANEL = (
|
||||
r"(?:calendar|notes?|inbox|email|mail|documents?|docs|library|gallery|"
|
||||
r"settings|cookbook|sessions?|chats?|skills|memories|memory|brain)"
|
||||
)
|
||||
|
||||
_TOOL_INTENT_PATTERNS: tuple[Pattern[str], ...] = tuple(
|
||||
re.compile(pattern, re.I)
|
||||
for pattern in (
|
||||
# Calendar/event creation. Covers "Can you add an entry to my
|
||||
# calendar?" and imperatives like "add lunch to my calendar".
|
||||
rf"{_ACTION_QUESTION}{_CALENDAR_ACTION}\b.{{0,120}}\b{_CALENDAR_THING}\b",
|
||||
rf"{_PLEASE}{_CALENDAR_ACTION}\b.{{0,120}}\b(?:to|on|in|into|for)\s+(?:my\s+|the\s+|this\s+)?calendar\b",
|
||||
rf"{_PLEASE}{_CALENDAR_ACTION}\s+(?:a\s+|an\s+)?(?:calendar\s+)?(?:event|meeting|appointment|entry|item|call)\b",
|
||||
r"\bput\s+.+\bon\s+(?:my\s+)?calendar\b",
|
||||
|
||||
# Notes, todos, checklists, and reminders.
|
||||
r"\bremind\s+me\b",
|
||||
rf"{_ACTION_QUESTION}(?:add|create|make|take|jot|write\s+down|set)\b.{{0,120}}\b(?:note|todo|task|checklist|reminder)\b",
|
||||
rf"{_PLEASE}(?:add|create|make)\s+(?:a\s+|an\s+)?(?:todo|task|reminder|note|checklist)\b",
|
||||
rf"{_PLEASE}(?:take|jot|write\s+down)\s+(?:a\s+|an\s+)?note\b",
|
||||
rf"{_PLEASE}(?:add|jot|write\s+down)\b.{{0,120}}\b(?:to|in|into)\s+(?:my\s+|the\s+)?(?:todo(?:\s+list)?|task\s+list|notes?|checklist)\b",
|
||||
rf"{_PLEASE}set\s+(?:a\s+)?reminder\b",
|
||||
rf"{_ACTION_QUESTION}set\s+(?:a\s+)?reminder\b",
|
||||
|
||||
# Email actions.
|
||||
rf"{_ACTION_QUESTION}(?:send|write|reply|email|message|archive|delete|mark)\b.{{0,120}}\b(?:emails?|mail|messages?|inbox|unread|read)\b",
|
||||
rf"{_PLEASE}(?:send|write|reply)\b.{{0,120}}\b(?:emails?|mail|messages?)\b",
|
||||
rf"{_PLEASE}(?:archive|delete|mark)\b.{{0,120}}\b(?:emails?|mail|messages?|inbox)\b",
|
||||
r"\b(?:send|write|reply)\s+(?:an?\s+)?(?:email|message|mail)\b",
|
||||
r"\bemail\s+\w+\b",
|
||||
r"\bcheck\s+(?:my\s+)?(?:email|inbox|mail)\b",
|
||||
r"\bunread\s+(?:email|mail)s?\b",
|
||||
|
||||
# UI/control-plane actions that should open panels or flip toggles.
|
||||
rf"{_PLEASE}(?:open|show|bring\s+up)\s+(?:me\s+)?(?:my\s+|the\s+)?{_PANEL}\b",
|
||||
r"\b(?:disable|enable|turn\s+(?:on|off))\s+(?:the\s+)?(?:shell|search|web|browser|documents?|memory|skills|images?|calendar|email|mail|research|incognito)\b",
|
||||
|
||||
# Deep research jobs, not quick conceptual mentions of research.
|
||||
rf"{_PLEASE}(?:research|deep\s+dive|look\s+into|investigate)\s+.+",
|
||||
rf"{_ACTION_QUESTION}(?:research|do\s+research|deep\s+dive|look\s+into|investigate)\s+.+",
|
||||
|
||||
# Shell / remote-host intent.
|
||||
r"\bssh\s+(?:in)?to\b",
|
||||
r"\bssh\s+\w+",
|
||||
r"\b(run|execute)\s+.{1,40}\bon\s+\w+",
|
||||
r"\b(can|could|please|would)\s+you\s+(run|execute|exec)\b",
|
||||
r"\b(deploy|build|install|restart|reboot|kill|tail|grep|cat|ls|cd|cp|mv|rm)\b\s+\S+",
|
||||
r"\b(check|see)\s+(if|whether|what)\s+.{1,40}\b(running|process|service|port|file|exists?)\b",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def message_needs_tools(text: str, patterns: Iterable[Pattern[str]] = _TOOL_INTENT_PATTERNS) -> bool:
|
||||
"""Return True when a plain chat message should be promoted to agent mode."""
|
||||
if not text:
|
||||
return False
|
||||
return any(pattern.search(text) for pattern in patterns)
|
||||
@@ -74,7 +74,7 @@ _AGENT_RULES = """\
|
||||
- AFTER A TOOL SUCCEEDS, do not second-guess. The success message ("Document edited: v2, 1 edit") means it worked. Reply in ONE short sentence confirming what was done. No re-checking, no replaying the diff in your head, no validation theater.
|
||||
- AFTER A TOOL FAILS (timeout, error, "Unknown action", "not found"), DO NOT GO SILENT. The user expects a follow-up: either retry with a fix (e.g. correct args, longer-running form, run `tail -f /tmp/foo.log` to see progress, split into smaller steps), OR explicitly tell them "this didn't work, want me to try X instead?". A failed tool is not a stopping condition — only a successful one is.
|
||||
- YOU DECLARE WHEN THE JOB IS DONE — not a timer. Keep taking concrete steps while the task still needs them; you have plenty of rounds, so don't rush to quit just because you've made a few calls. There are exactly three ways to end a turn: (1) DONE — before you declare it, sanity-check that every concrete thing the user asked for actually exists or succeeded (file written, edit applied, command exited clean); then stop calling tools and write the final answer (that IS your "done" signal); (2) BLOCKED — you genuinely can't proceed (a capability is missing, permission denied, or data you can't obtain), so say plainly what's blocking you, in a sentence or two, and stop; (3) keep going with the single most useful next step. The only wrong moves are trailing off mid-task without one of these, and repeating a call you already ran.
|
||||
- CalDAV: Call list-calendars FIRST before any calendar operations.
|
||||
- Calendar: call `manage_calendar` with `action=list_calendars` FIRST before create/update/delete operations.
|
||||
- BULK email actions ("delete all those", "mark all as read", "archive these", "delete all spam", "mark these 19 read") → use the `bulk_email` tool ONCE with either the exact `uids` list from the latest `list_emails` result or `all_unread: true`. NEVER just say you deleted/archived/marked messages unless a delete/archive/mark/bulk email tool call succeeded. NEVER loop mark_email_read / archive_email / delete_email one message at a time — that floods the context and can blow the token budget. One bulk_email call handles the whole set.
|
||||
- Email UIDs are the values after `UID:` in tool output, not list row numbers. For example, row `1.` with `UID: 90186` must use `"90186"`, never `"1"`.
|
||||
- "Last/latest/newest email" means call `list_emails` with `max_results: 1`, `unread_only: false`, and the right `account`, then read the UID returned by that tool if full content is needed. NEVER use a table row number like "#18" as an email UID.
|
||||
@@ -120,7 +120,7 @@ _API_AGENT_RULES = """\
|
||||
- AFTER A TOOL SUCCEEDS, do not second-guess. A success response means it worked. Reply in ONE short sentence confirming what was done. No verification thinking, no re-analyzing — move on.
|
||||
- AFTER A TOOL FAILS, DO NOT GO SILENT. The user expects a follow-up: retry with a fix, run a diagnostic (`tail`, `ls`, `which`), or explicitly tell them what didn't work and what you'll try next. Failure is not a stopping condition.
|
||||
- YOU DECLARE WHEN THE JOB IS DONE — not a timer. Keep taking concrete steps while the task still needs them; don't quit early just because you've made a few calls. Three ways to end a turn: (1) DONE — before declaring it, verify every concrete deliverable the user asked for actually exists or succeeded; then stop calling tools and write the final answer (that IS your "done" signal); (2) BLOCKED — you can't proceed (missing capability, permission denied, unobtainable data), so state plainly what's blocking you and stop; (3) keep going with the single most useful next step. Never trail off mid-task without (1) or (2), and never repeat a call you already ran.
|
||||
- CalDAV: Call list-calendars FIRST before any calendar operations.
|
||||
- Calendar: call `manage_calendar` with `action=list_calendars` FIRST before create/update/delete operations.
|
||||
- "Create/add/write a note" / "notes" / "todos" / "remind me to X at <time>" → use `manage_notes`. Do NOT store notes in `manage_memory`; memory is for persistent facts/preferences about the user, not note content. For reminders, include a `due_date`; for todos, use `note_type=checklist` when appropriate. `manage_tasks` is for RECURRING background AI jobs, NOT for one-off user reminders.
|
||||
- "Disable/turn off/enable/turn on <tool>" (shell, search, research, browser, documents, incognito, etc.) → call `ui_control` with `toggle <name> <on|off>`. Aliases accepted: shell→bash, search→web, deepresearch→research, documents→document_editor. NEVER record this as a memory — the user wants the toggle flipped, not a note about preferring it.
|
||||
- "Research X" / "do research on X" / "look into Y" / "deep dive on Z" → call `trigger_research` with `topic`. This starts a live job that appears in the Deep Research sidebar (streams progress + final report). **Do NOT use `web_search` for these** — saw the agent do a plain web_search for "do research on X" when the user wanted the deep-research job. "research X" is a deep-research request, not a quick lookup. (web_search is only for a single quick fact mid-task.) Do NOT POST /api/research/start via app_api either — blocked. After starting, tell the user it's running in the Deep Research sidebar. Only if the user explicitly wants it inline/quick should you fall back to web_search.
|
||||
|
||||
35
tests/test_action_intents.py
Normal file
35
tests/test_action_intents.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from src.action_intents import message_needs_tools
|
||||
|
||||
|
||||
def test_calendar_entry_request_promotes_to_agent():
|
||||
assert message_needs_tools("Can you add an entry to my calendar?")
|
||||
|
||||
|
||||
def test_calendar_imperative_variants_promote_to_agent():
|
||||
assert message_needs_tools("add lunch with Sam to my calendar tomorrow at noon")
|
||||
assert message_needs_tools("schedule a call with Mina next Friday")
|
||||
assert message_needs_tools("put dentist appointment on my calendar")
|
||||
|
||||
|
||||
def test_note_todo_and_reminder_actions_promote_to_agent():
|
||||
assert message_needs_tools("add milk to my todo list")
|
||||
assert message_needs_tools("take a note that the server needs checking")
|
||||
assert message_needs_tools("set a reminder to call Pat at 4pm")
|
||||
|
||||
|
||||
def test_email_and_ui_actions_promote_to_agent():
|
||||
assert message_needs_tools("reply to that email")
|
||||
assert message_needs_tools("mark those emails as read")
|
||||
assert message_needs_tools("open my calendar")
|
||||
assert message_needs_tools("turn off web search")
|
||||
|
||||
|
||||
def test_research_action_promotes_to_agent():
|
||||
assert message_needs_tools("research cost effective local models")
|
||||
assert message_needs_tools("can you look into GPU hosting options")
|
||||
|
||||
|
||||
def test_explanatory_calendar_questions_stay_plain_chat():
|
||||
assert not message_needs_tools("How do I add an entry to my calendar?")
|
||||
assert not message_needs_tools("What about the built-in Odysseus calendar, is that linked to email?")
|
||||
assert not message_needs_tools("Can you explain how calendar reminders work?")
|
||||
Reference in New Issue
Block a user