From 32d542f7a317ab483dd878d61b4dae9a630f1e26 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 2 Jul 2026 17:54:31 +0200 Subject: [PATCH] feat(chat): add stop and refresh watch tools --- .../intelligence_agent/tools/refresh_watch.py | 82 ++++++++++++++++++ .../intelligence_agent/tools/stop_watch.py | 85 +++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/intelligence_agent/tools/refresh_watch.py create mode 100644 surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/intelligence_agent/tools/stop_watch.py diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/intelligence_agent/tools/refresh_watch.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/intelligence_agent/tools/refresh_watch.py new file mode 100644 index 000000000..0e8d507b5 --- /dev/null +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/intelligence_agent/tools/refresh_watch.py @@ -0,0 +1,82 @@ +"""``refresh_watch`` — run the current chat's watch now (manual refresh). + +Enqueues an immediate run of every watch bound to this chat, instead of waiting +for the next scheduled fire. Operates on the current thread only. +""" + +from __future__ import annotations + +import logging +from typing import Any + +from fastapi import HTTPException +from langchain_core.tools import tool + +from app.auth.context import AuthContext +from app.automations.services.automation import AutomationService +from app.automations.services.chat_watch import find_watches_for_thread, run_watch_now +from app.db import async_session_maker + +logger = logging.getLogger(__name__) + + +def create_refresh_watch_tool( + *, + workspace_id: int | None, + thread_id: int | str | None, + auth_context: AuthContext | None, +): + """Build the ``refresh_watch`` tool bound to the current chat.""" + + @tool + async def refresh_watch() -> dict[str, Any]: + """Run THIS chat's watch now (a manual refresh). + + Use when the user says "check now", "refresh", or "run it again" + without waiting for the schedule. No arguments — it acts on the + current chat. The refreshed answer arrives as a new turn shortly after. + + Returns: + ``{"status": "refreshing", "refreshed_ids": [int], "count": int}`` + when runs were enqueued, ``{"status": "not_watching", ...}`` when + there is no watch, or ``{"status": "error", "message": str}``. + """ + if thread_id is None or auth_context is None: + return { + "status": "error", + "message": "Watches can only be managed from inside a chat.", + } + + try: + async with async_session_maker() as session: + service = AutomationService(session=session, auth=auth_context) + watches = await find_watches_for_thread( + service, + workspace_id=int(workspace_id) if workspace_id is not None else 0, + thread_id=int(thread_id), + ) + if not watches: + return { + "status": "not_watching", + "message": "This chat has no active watch to refresh.", + } + refreshed_ids = [] + for watch in watches: + await run_watch_now(service, automation_id=watch.id) + refreshed_ids.append(watch.id) + except HTTPException as exc: + return {"status": "error", "message": str(exc.detail)} + except Exception as exc: + logger.exception("refresh_watch failed") + return {"status": "error", "message": f"could not refresh watch: {exc}"} + + return { + "status": "refreshing", + "refreshed_ids": refreshed_ids, + "count": len(refreshed_ids), + } + + return refresh_watch + + +__all__ = ["create_refresh_watch_tool"] diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/intelligence_agent/tools/stop_watch.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/intelligence_agent/tools/stop_watch.py new file mode 100644 index 000000000..4f24c294f --- /dev/null +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/intelligence_agent/tools/stop_watch.py @@ -0,0 +1,85 @@ +"""``stop_watch`` — stop watching in the current chat. + +Deletes every watch automation bound to this chat, so it reverts to a normal +chat. Operates on the current thread only; the model passes no arguments. +""" + +from __future__ import annotations + +import logging +from typing import Any + +from fastapi import HTTPException +from langchain_core.tools import tool + +from app.auth.context import AuthContext +from app.automations.services.automation import AutomationService +from app.automations.services.chat_watch import ( + find_watches_for_thread, + stop_watch as stop_watch_service, +) +from app.db import async_session_maker + +logger = logging.getLogger(__name__) + + +def create_stop_watch_tool( + *, + workspace_id: int | None, + thread_id: int | str | None, + auth_context: AuthContext | None, +): + """Build the ``stop_watch`` tool bound to the current chat.""" + + @tool + async def stop_watch() -> dict[str, Any]: + """Stop watching in THIS chat (cancel the recurring re-asks). + + Use when the user says something like "stop watching", "cancel the + watch", or "you can stop checking now". The chat reverts to a normal + chat. No arguments — it acts on the current chat. + + Returns: + ``{"status": "stopped", "stopped_ids": [int], "count": int}`` when + watches were removed, ``{"status": "not_watching", ...}`` when there + was nothing to stop, or ``{"status": "error", "message": str}``. + """ + if thread_id is None or auth_context is None: + return { + "status": "error", + "message": "Watches can only be managed from inside a chat.", + } + + try: + async with async_session_maker() as session: + service = AutomationService(session=session, auth=auth_context) + watches = await find_watches_for_thread( + service, + workspace_id=int(workspace_id) if workspace_id is not None else 0, + thread_id=int(thread_id), + ) + if not watches: + return { + "status": "not_watching", + "message": "This chat has no active watch.", + } + stopped_ids = [] + for watch in watches: + await stop_watch_service(service, automation_id=watch.id) + stopped_ids.append(watch.id) + except HTTPException as exc: + return {"status": "error", "message": str(exc.detail)} + except Exception as exc: + logger.exception("stop_watch failed") + return {"status": "error", "message": f"could not stop watch: {exc}"} + + return { + "status": "stopped", + "stopped_ids": stopped_ids, + "count": len(stopped_ids), + } + + return stop_watch + + +__all__ = ["create_stop_watch_tool"]