From 460959f0d430fff74068da705f4fb2bd38d98cab Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 12 May 2026 16:42:34 -0400 Subject: [PATCH] refactor(tools): search_projects uses shared score_project_match helper Co-Authored-By: Claude Opus 4.7 (1M context) --- .../services/tools/projects.py | 14 +++--------- tests/test_tool_use_fixes.py | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/fabledassistant/services/tools/projects.py b/src/fabledassistant/services/tools/projects.py index 899c317..a685062 100644 --- a/src/fabledassistant/services/tools/projects.py +++ b/src/fabledassistant/services/tools/projects.py @@ -135,20 +135,12 @@ async def update_project_tool(*, user_id, arguments, **_ctx): briefing=True, ) async def search_projects_tool(*, user_id, arguments, **_ctx): - from difflib import SequenceMatcher - from fabledassistant.services.projects import list_projects + from fabledassistant.services.tools._helpers import score_project_match - query = str(arguments.get("query", "")).lower() + query = str(arguments.get("query", "")) projects = await list_projects(user_id) - scored: list[tuple[float, object]] = [] - for p in projects: - combined = f"{p.title} {p.description or ''} {p.auto_summary or ''}".lower() - base_score = SequenceMatcher(None, query, combined).ratio() - query_words = set(query.split()) - overlap = sum(1 for w in query_words if w in combined) - score = base_score + overlap * 0.05 - scored.append((score, p)) + scored: list[tuple[float, object]] = [(score_project_match(query, p), p) for p in projects] scored.sort(key=lambda x: x[0], reverse=True) results = [] for score, p in scored[:5]: diff --git a/tests/test_tool_use_fixes.py b/tests/test_tool_use_fixes.py index d7ff034..ef62872 100644 --- a/tests/test_tool_use_fixes.py +++ b/tests/test_tool_use_fixes.py @@ -87,3 +87,25 @@ def test_score_project_match_empty_query_returns_zero(): from fabledassistant.services.tools._helpers import score_project_match p = _project(5, "Famous-Supply Work topics") assert score_project_match("", p) == 0.0 + + +@pytest.mark.asyncio +async def test_search_projects_tool_ranks_substring_match_above_others(): + """Substring hits (score 0.85) must rank above SequenceMatcher misses + for unrelated projects.""" + from fabledassistant.services.tools import projects as projects_tool + + fake_projects = [ + _project(12, "Minstrel", auto_summary="self-hosted music server"), + _project(5, "Famous-Supply Work topics", auto_summary="AT&T fiber circuit"), + _project(13, "ImageRepo", auto_summary="self-hosted Flask app"), + ] + + with patch.object(projects_tool, "list_projects", new=AsyncMock(return_value=fake_projects)): + result = await projects_tool.search_projects_tool( + user_id=1, arguments={"query": "famous supply project"}, + ) + + top = result["data"]["projects"][0] + assert top["id"] == 5, f"expected Famous-Supply to rank first, got {top}" + assert top["score"] >= 0.85