feat: enhance collaboration guidelines in documentation and refactor mention handling for improved clarity and functionality

This commit is contained in:
Abhimanyu Saharan
2026-02-06 22:52:18 +05:30
parent 5611f8eb67
commit c238ae9876
9 changed files with 363 additions and 256 deletions

View File

@@ -2,7 +2,6 @@ from __future__ import annotations
import asyncio
import json
import re
from collections.abc import AsyncIterator
from datetime import datetime, timezone
from uuid import UUID
@@ -26,11 +25,10 @@ from app.models.boards import Board
from app.models.gateways import Gateway
from app.schemas.board_memory import BoardMemoryCreate, BoardMemoryRead
from app.schemas.pagination import DefaultLimitOffsetPage
from app.services.mentions import extract_mentions, matches_agent_mention
router = APIRouter(prefix="/boards/{board_id}/memory", tags=["board-memory"])
MENTION_PATTERN = re.compile(r"@([A-Za-z][\w-]{0,31})")
def _parse_since(value: str | None) -> datetime | None:
if not value:
@@ -52,23 +50,6 @@ def _serialize_memory(memory: BoardMemory) -> dict[str, object]:
return BoardMemoryRead.model_validate(memory, from_attributes=True).model_dump(mode="json")
def _extract_mentions(message: str) -> set[str]:
return {match.group(1).lower() for match in MENTION_PATTERN.finditer(message)}
def _matches_mention(agent: Agent, mentions: set[str]) -> bool:
if not mentions:
return False
name = (agent.name or "").strip()
if not name:
return False
normalized = name.lower()
if normalized in mentions:
return True
first = normalized.split()[0]
return first in mentions
async def _gateway_config(session: AsyncSession, board: Board) -> GatewayClientConfig | None:
if board.gateway_id is None:
return None
@@ -123,14 +104,14 @@ async def _notify_chat_targets(
config = await _gateway_config(session, board)
if config is None:
return
mentions = _extract_mentions(memory.content)
mentions = extract_mentions(memory.content)
statement = select(Agent).where(col(Agent.board_id) == board.id)
targets: dict[str, Agent] = {}
for agent in await session.exec(statement):
if agent.is_board_lead:
targets[str(agent.id)] = agent
continue
if mentions and _matches_mention(agent, mentions):
if mentions and matches_agent_mention(agent, mentions):
targets[str(agent.id)] = agent
if actor.actor_type == "agent" and actor.agent:
targets.pop(str(actor.agent.id), None)
@@ -148,7 +129,7 @@ async def _notify_chat_targets(
for agent in targets.values():
if not agent.openclaw_session_id:
continue
mentioned = _matches_mention(agent, mentions)
mentioned = matches_agent_mention(agent, mentions)
header = "BOARD CHAT MENTION" if mentioned else "BOARD CHAT"
message = (
f"{header}\n"

View File

@@ -2,7 +2,6 @@ from __future__ import annotations
import asyncio
import json
import re
from collections import deque
from collections.abc import AsyncIterator
from datetime import datetime, timezone
@@ -40,6 +39,7 @@ from app.schemas.common import OkResponse
from app.schemas.pagination import DefaultLimitOffsetPage
from app.schemas.tasks import TaskCommentCreate, TaskCommentRead, TaskCreate, TaskRead, TaskUpdate
from app.services.activity_log import record_activity
from app.services.mentions import extract_mentions, matches_agent_mention
router = APIRouter(prefix="/boards/{board_id}/tasks", tags=["tasks"])
@@ -51,7 +51,6 @@ TASK_EVENT_TYPES = {
"task.comment",
}
SSE_SEEN_MAX = 2000
MENTION_PATTERN = re.compile(r"@([A-Za-z][\w-]{0,31})")
def _comment_validation_error() -> HTTPException:
@@ -99,23 +98,6 @@ def _parse_since(value: str | None) -> datetime | None:
return parsed
def _extract_mentions(message: str) -> set[str]:
return {match.group(1).lower() for match in MENTION_PATTERN.finditer(message)}
def _matches_mention(agent: Agent, mentions: set[str]) -> bool:
if not mentions:
return False
name = (agent.name or "").strip()
if not name:
return False
normalized = name.lower()
if normalized in mentions:
return True
first = normalized.split()[0]
return first in mentions
async def _lead_was_mentioned(
session: AsyncSession,
task: Task,
@@ -130,8 +112,8 @@ async def _lead_was_mentioned(
for message in await session.exec(statement):
if not message:
continue
mentions = _extract_mentions(message)
if _matches_mention(lead, mentions):
mentions = extract_mentions(message)
if matches_agent_mention(lead, mentions):
return True
return False
@@ -527,7 +509,7 @@ async def update_task(
if updates["status"] == "inbox":
task.assigned_agent_id = None
task.in_progress_at = None
task.status = updates["status"]
task.status = updates["status"]
task.updated_at = utcnow()
session.add(task)
if task.status != previous_status:
@@ -718,12 +700,12 @@ async def create_task_comment(
session.add(event)
await session.commit()
await session.refresh(event)
mention_names = _extract_mentions(payload.message)
mention_names = extract_mentions(payload.message)
targets: dict[UUID, Agent] = {}
if mention_names and task.board_id:
statement = select(Agent).where(col(Agent.board_id) == task.board_id)
for agent in await session.exec(statement):
if _matches_mention(agent, mention_names):
if matches_agent_mention(agent, mention_names):
targets[agent.id] = agent
if not mention_names and task.assigned_agent_id:
assigned_agent = await session.get(Agent, task.assigned_agent_id)
@@ -742,7 +724,7 @@ async def create_task_comment(
for agent in targets.values():
if not agent.openclaw_session_id:
continue
mentioned = _matches_mention(agent, mention_names)
mentioned = matches_agent_mention(agent, mention_names)
header = "TASK MENTION" if mentioned else "NEW TASK COMMENT"
action_line = (
"You were mentioned in this comment."

View File

@@ -0,0 +1,34 @@
from __future__ import annotations
import re
from app.models.agents import Agent
# Mention tokens are single, space-free words (e.g. "@alex", "@lead").
MENTION_PATTERN = re.compile(r"@([A-Za-z][\w-]{0,31})")
def extract_mentions(message: str) -> set[str]:
return {match.group(1).lower() for match in MENTION_PATTERN.finditer(message)}
def matches_agent_mention(agent: Agent, mentions: set[str]) -> bool:
if not mentions:
return False
# "@lead" is a reserved shortcut that always targets the board lead.
if "lead" in mentions and agent.is_board_lead:
return True
mentions = mentions - {"lead"}
name = (agent.name or "").strip()
if not name:
return False
normalized = name.lower()
if normalized in mentions:
return True
# Mentions are single tokens; match on first name for display names with spaces.
first = normalized.split()[0]
return first in mentions

View File

@@ -0,0 +1,20 @@
from app.models.agents import Agent
from app.services.mentions import extract_mentions, matches_agent_mention
def test_extract_mentions_parses_tokens():
assert extract_mentions("hi @Alex and @bob-2") == {"alex", "bob-2"}
def test_matches_agent_mention_matches_first_name():
agent = Agent(name="Alice Cooper")
assert matches_agent_mention(agent, {"alice"}) is True
assert matches_agent_mention(agent, {"cooper"}) is False
def test_matches_agent_mention_supports_reserved_lead_shortcut():
lead = Agent(name="Riya", is_board_lead=True)
other = Agent(name="Lead", is_board_lead=False)
assert matches_agent_mention(lead, {"lead"}) is True
assert matches_agent_mention(other, {"lead"}) is False