Files
admin 065a6f3d1a fix: tighten OW background request filter to stop spurious task creation
Added three new detection patterns beyond the existing keyword checks:
1. Conversation history dumps: messages containing both "user:" and
   "assistant:" role markers — real user messages never have both
2. Context synthesis phrases: "provided context", "following conversation",
   "based on the conversation" (OW's query augmentation patterns)
3. Markdown template headers: "### " in messages >200 chars (OW uses
   section headers in background templates; real mobile messages don't)

This closes the gap that caused "Fix CobraTrans ESET" to be created
spuriously in Agent Ecosystem from an OW history-dump background request.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 06:31:43 +00:00

334 lines
12 KiB
Python

AGENT_NAMES = {"hodor", "bran", "varys", "sam", "raven", "qyburn", "citadel", "jon", "hermes"}
STATUS_PHRASES = {
"status", "health", "running", "last run", "what did", "when did",
"show me", "how is", "is it", "is running", "did it run", "output",
"summary", "report", "check", "monitor", "alive", "up",
}
TASK_PHRASES = {
"create task", "add task", "add issue", "create issue", "log task",
"log this", "new task", "new issue", "add to plane", "add to backlog",
"schedule", "remind", "track", "todo", "to do",
"job item", "job ticket", "work order",
}
# "plan" removed from TASK_PHRASES — it substring-matches "plane" (the tool name)
TASK_WORDS = {"plan"}
QUERY_PHRASES = {
"list", "list all", "show all", "what are", "give me", "get",
"my projects", "all projects", "list projects", "show projects",
"issues", "tasks", "backlog", "what's in", "whats in", "show issues",
"list issues", "issues in", "tasks in", "work items", "work item",
}
RESEARCH_PHRASES = {
"research", "search", "find out", "look up", "what is", "explain",
"how does", "documentation", "docs",
}
CREATE_PROJECT_PHRASES = {
"create project", "new project", "make project", "add project",
"create a project", "new project called", "create project called",
"start a project", "set up a project",
"create a plane project", "create plane project",
"create a new project", "make a new project",
}
# Execute intent — words that signal a request to run/deploy a container
EXECUTE_VERBS = {"restart", "rebuild", "redeploy", "reload", "deploy"}
# Short agent name → Docker stack directory name
STACK_NAMES = {
"hodor": "hodor-gateway",
"bran": "bran-changelog",
"varys": "varys-monitor",
"sam": "sam-research",
"raven": "raven-notify",
"qyburn": "qyburn-coder",
"citadel": "citadel-mcp",
"jon": "jon-snow",
"hermes": "hermes-cloud",
"monitoring": "monitoring",
"netbox": "netbox",
"plane": "plane",
"gitea": "gitea",
"searxng": "searxng",
}
def classify_intent(message: str) -> str:
msg = message.lower()
words = set(msg.split())
# Open WebUI background tasks — sent automatically after every chat response.
# They include the full chat history, so they'd mis-trigger execute/task intents.
# Pattern 1: OW meta-task headers and keyword phrases
if (msg.startswith("### task:") or
msg.startswith("query:") or
"chat history" in msg or
"summarizing the chat" in msg or
"categorizing the main themes" in msg or
"follow-up questions" in msg or
"3-5 word title" in msg or
"provided context" in msg or
"following conversation" in msg or
"based on the conversation" in msg):
return "planning"
# Pattern 2: conversation history dumps — contain role marker pairs.
# Real user messages never contain both "user:" and "assistant:" labels.
if "user:" in msg and "assistant:" in msg:
return "planning"
if msg.count("user:") >= 2 or msg.count("assistant:") >= 2:
return "planning"
# Pattern 3: OW markdown template headers (### ) in long messages.
# Real mobile messages don't use markdown section headers.
if "### " in msg and len(msg) > 200:
return "planning"
# Project creation — check before task/query
if any(p in msg for p in CREATE_PROJECT_PHRASES):
return "create_project"
# Execute intent — agent/stack name + action verb → queue for approval
if words & EXECUTE_VERBS and words & set(STACK_NAMES.keys()):
return "execute"
if any(v in msg for v in EXECUTE_VERBS) and any(s in msg for s in STACK_NAMES):
return "execute"
# Explicit task phrases — check before query to prevent "add task X" → query mis-route
if any(p in msg for p in TASK_PHRASES):
return "task"
if words & TASK_WORDS:
return "task"
# Plane query — list/show projects or issues (check before task to avoid "plan" ⊂ "plane" bug)
if ("project" in msg or "projects" in msg) and any(p in msg for p in QUERY_PHRASES):
return "query"
if "plane" in words and any(p in msg for p in QUERY_PHRASES):
return "query"
# Issue list query — "show issues in X", "list tasks for X", "work items for X"
# Use word-boundary check (words set) to avoid "in" matching inside "incident" etc.
has_issue_word = bool(words & ISSUE_KEYWORDS) or any(p in msg for p in ISSUE_PHRASES)
if has_issue_word and words & {"show", "list", "get", "what", "give", "in", "for"}:
return "query"
# Agent name + status word → status
if words & AGENT_NAMES:
if any(p in msg for p in STATUS_PHRASES) or words & {"status", "check", "output", "run"}:
return "status"
# Natural task creation: "add X to [project]"
if msg.startswith("add ") and " to " in msg:
return "task"
# "please add" / "add" anywhere + known project keyword → task
if "add" in words and any(kw in msg for kw in PROJECT_KEYWORDS):
return "task"
# Generic status signal words
if any(p in msg for p in STATUS_PHRASES) and words & AGENT_NAMES:
return "status"
# Status if asking purely about the agent ecosystem
if words & AGENT_NAMES and not any(p in msg for p in {"build", "implement", "create", "make"}):
return "status"
# Research intent → route to smart model
if any(p in msg for p in RESEARCH_PHRASES):
return "planning"
return "planning"
def extract_agent_name(message: str) -> str | None:
msg = message.lower()
for name in AGENT_NAMES:
if name in msg:
return name
return None
def extract_execute_target(message: str) -> tuple[str, str] | None:
"""Extract (action_type, stack_dir_name) from an execute message.
Examples:
'restart citadel' → ('docker_restart', 'citadel-mcp')
'rebuild raven' → ('docker_rebuild', 'raven-notify')
'redeploy hermes' → ('docker_rebuild', 'hermes-cloud')
Returns None if action type or stack name cannot be determined.
"""
msg = message.lower()
words = set(msg.split())
action_type = None
if words & {"restart", "reload"}:
action_type = "docker_restart"
elif words & {"rebuild", "redeploy", "deploy"}:
action_type = "docker_rebuild"
stack_dir = None
for short, full in STACK_NAMES.items():
if short in words:
stack_dir = full
break
if action_type and stack_dir:
return (action_type, stack_dir)
return None
PROJECT_KEYWORDS = {
"bni": "BNI Scheduler",
"scheduler": "BNI Scheduler",
"monitor": "Monitoring",
"monitoring": "Monitoring",
"grafana": "Monitoring",
"maester": "Maester Reports",
"report": "Maester Reports",
"csf": "Maester Reports",
"nist": "Maester Reports",
"portal": "Nexum Portal",
"authelia": "Nexum Portal",
"nexum": "Nexum Portal",
"general": "General / Admin",
"admin": "General / Admin",
"agent": "Agent Ecosystem",
"ecosystem": "Agent Ecosystem",
"milkwood": "Milkwood Primary",
"grgl": "GRGL",
"private": "Private Clients",
}
ISSUE_KEYWORDS = {"issue", "issues", "task", "tasks", "backlog", "ticket", "tickets"}
ISSUE_PHRASES = {"work items", "work item"}
def is_issue_query(message: str) -> bool:
msg = message.lower()
return bool(set(msg.split()) & ISSUE_KEYWORDS) or any(p in msg for p in ISSUE_PHRASES)
def extract_task_title(message: str) -> str:
"""Extract clean task title from natural phrasing like 'add buy milk to General'."""
import re as _re
msg = message.strip()
lower = msg.lower()
# Explicit prefixes
for prefix in ("create task:", "add task:", "log task:", "new task:", "log this:"):
if lower.startswith(prefix):
return msg[len(prefix):].strip()
# Item noun phrases — "add X a job/work item [description]"
# Check these before "add X to Y" so the wrong " to " isn't captured
for noun in (r'\bjob item\b', r'\bwork item\b', r'\bwork order\b', r'\bjob ticket\b'):
m = _re.search(noun + r'\s*(.*)', lower)
if m:
remainder = msg[m.start(1):].strip()
for filler in ("that we need to ", "that is ", "which is ", "to "):
if remainder.lower().startswith(filler):
remainder = remainder[len(filler):]
# "project, title" leftover → take after the comma
if "," in remainder:
remainder = remainder[remainder.index(",") + 1:].strip()
if remainder.strip():
return remainder.strip()
# "please add X to [project], title" or "add X to [project], title"
work_msg = msg[7:].strip() if lower.startswith("please ") else msg
work_lower = work_msg.lower()
if work_lower.startswith("add ") and " to " in work_lower:
to_idx = work_lower.rfind(" to ")
after_to = work_msg[to_idx + 4:]
if "," in after_to:
title_candidate = after_to[after_to.index(",") + 1:].strip()
if title_candidate:
return title_candidate
# No comma — what's between "add " and " to [project]" is the title
before_to = work_msg[4:to_idx].strip()
for noise in ("a work item", "an issue", "a task", "a job item", "a ticket",
"an item", "a to-do", "a todo"):
if before_to.lower().startswith(noise):
remainder = before_to[len(noise):].strip()
if remainder:
return remainder
if before_to:
return before_to
return msg
def extract_compound_task(message: str) -> str | None:
"""Extract a task title from compound messages like 'create project X and add [todo item,] Y'.
Returns the task title or None if no compound task found."""
lower = message.lower()
# Split on compound markers
for marker in ("and add to do item,", "and add todo item,", "and add a task,",
"and add task,", "and add issue,", "and add to do,",
"and add to-do,", "and add,"):
idx = lower.find(marker)
if idx != -1:
return message[idx + len(marker):].strip().strip('"\'')
# Simpler: "and add [title]" — only if no comma separator
idx = lower.find(" and add ")
if idx != -1:
candidate = message[idx + 9:].strip()
# Skip if it looks like a project destination ("and add to X")
if not candidate.lower().startswith("to "):
return candidate.strip('"\'')
return None
def _trim_at_compound(text: str) -> str:
"""Trim text at compound boundaries like 'and add'."""
lower = text.lower()
for stop in (" and add", " then add", ", and", " also add"):
idx = lower.find(stop)
if idx != -1:
text = text[:idx]
return text.strip().strip('"\',.')
def extract_new_project_name(message: str) -> str | None:
"""Extract project name from 'create project called X', 'new project: X', etc."""
msg = message.strip()
lower = msg.lower()
for marker in ("called ", "named ", "name ", ": ", "- "):
idx = lower.find(marker)
if idx != -1:
name = _trim_at_compound(msg[idx + len(marker):])
if name:
return name
for prefix in ("create a new project ", "create a plane project ", "create plane project ",
"create a project ", "create project ", "new project ", "make a new project ",
"make project ", "add project ", "start a project ", "set up a project "):
if lower.startswith(prefix):
name = _trim_at_compound(msg[len(prefix):])
if name:
return name
return None
def extract_task_destination(message: str) -> str | None:
"""Extract the destination project from 'add X to Y' → returns Y, or None."""
lower = message.strip().lower()
if lower.startswith("add ") and " to " in lower:
parts = message.strip().rsplit(" to ", 1)
if len(parts) == 2:
return parts[1].strip()
return None
def extract_project_name(message: str) -> str | None:
msg = message.lower()
for kw, project in PROJECT_KEYWORDS.items():
if kw in msg:
return project
return None