forked from Rockachopa/Timmy-time-dashboard
fix: word-boundary routing + debug route command (#31)
- Replace substring matching with word-boundary regex in route_request() - "fix the bug" now correctly routes to coder - Multi-word patterns match if all words appear (any order) - Add "timmy route" CLI command for debugging routing - Add route_request_with_match() for pattern visibility - Expand routing keywords in agents.yaml - 22 new routing tests, all passing
This commit is contained in:
@@ -16,6 +16,7 @@ Usage:
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -181,6 +182,23 @@ def get_routing_config() -> dict[str, Any]:
|
||||
return config.get("routing", {"method": "pattern", "patterns": {}})
|
||||
|
||||
|
||||
def _matches_pattern(pattern: str, message: str) -> bool:
|
||||
"""Check if a pattern matches using word-boundary matching.
|
||||
|
||||
For single-word patterns, uses \b word boundaries.
|
||||
For multi-word patterns, all words must appear as whole words (in any order).
|
||||
"""
|
||||
pattern_lower = pattern.lower()
|
||||
message_lower = message.lower()
|
||||
words = pattern_lower.split()
|
||||
|
||||
for word in words:
|
||||
# Use word boundary regex to match whole words only
|
||||
if not re.search(rf"\b{re.escape(word)}\b", message_lower):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def route_request(user_message: str) -> str | None:
|
||||
"""Route a user request to an agent using pattern matching.
|
||||
|
||||
@@ -193,17 +211,36 @@ def route_request(user_message: str) -> str | None:
|
||||
return None
|
||||
|
||||
patterns = routing.get("patterns", {})
|
||||
message_lower = user_message.lower()
|
||||
|
||||
for agent_id, keywords in patterns.items():
|
||||
for keyword in keywords:
|
||||
if keyword.lower() in message_lower:
|
||||
if _matches_pattern(keyword, user_message):
|
||||
logger.debug("Routed to %s (matched: %r)", agent_id, keyword)
|
||||
return agent_id
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def route_request_with_match(user_message: str) -> tuple[str | None, str | None]:
|
||||
"""Route a user request and return both the agent and the matched pattern.
|
||||
|
||||
Returns a tuple of (agent_id, matched_pattern). If no match, returns (None, None).
|
||||
"""
|
||||
routing = get_routing_config()
|
||||
|
||||
if routing.get("method") != "pattern":
|
||||
return None, None
|
||||
|
||||
patterns = routing.get("patterns", {})
|
||||
|
||||
for agent_id, keywords in patterns.items():
|
||||
for keyword in keywords:
|
||||
if _matches_pattern(keyword, user_message):
|
||||
return agent_id, keyword
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def reload_agents() -> dict[str, Any]:
|
||||
"""Force reload agents from YAML. Call after editing agents.yaml."""
|
||||
global _agents, _config
|
||||
|
||||
@@ -325,5 +325,19 @@ def voice(
|
||||
loop.run()
|
||||
|
||||
|
||||
@app.command()
|
||||
def route(
|
||||
message: str = typer.Argument(..., help="Message to route"),
|
||||
):
|
||||
"""Show which agent would handle a message (debug routing)."""
|
||||
from timmy.agents.loader import route_request_with_match
|
||||
|
||||
agent_id, matched_pattern = route_request_with_match(message)
|
||||
if agent_id:
|
||||
typer.echo(f"→ {agent_id} (matched: {matched_pattern})")
|
||||
else:
|
||||
typer.echo("→ orchestrator (no pattern match)")
|
||||
|
||||
|
||||
def main():
|
||||
app()
|
||||
|
||||
Reference in New Issue
Block a user