diff --git a/src/fabledassistant/services/tools/_helpers.py b/src/fabledassistant/services/tools/_helpers.py index d6bbc2f..2ec5b1b 100644 --- a/src/fabledassistant/services/tools/_helpers.py +++ b/src/fabledassistant/services/tools/_helpers.py @@ -54,30 +54,24 @@ def score_project_match(query: str, project) -> float: async def resolve_project(user_id: int, project_name: str): - """Exact-then-fuzzy project lookup. Returns the Project or None. + """Exact-then-scored project lookup. Returns the Project or None. Resolution order: - 1. Exact title match (case-insensitive via DB) - 2. project_name is a substring of an existing title - 3. Existing title is a substring of project_name - 4. SequenceMatcher ratio >= 0.55 + 1. Exact title match (case-insensitive via DB query). + 2. Highest `score_project_match` across all projects, threshold 0.55. """ from fabledassistant.services.projects import get_project_by_title, list_projects proj = await get_project_by_title(user_id, project_name) if proj is not None: return proj - needle = project_name.lower().strip() + all_p = await list_projects(user_id) + best, best_score = None, 0.0 for p in all_p: - haystack = p.title.lower().strip() - if needle in haystack or haystack in needle: - return p - best, best_r = None, 0.0 - for p in all_p: - r = SequenceMatcher(None, needle, p.title.lower().strip()).ratio() - if r >= 0.55 and r > best_r: - best, best_r = p, r + score = score_project_match(project_name, p) + if score >= 0.55 and score > best_score: + best, best_score = p, score return best diff --git a/tests/test_tool_use_fixes.py b/tests/test_tool_use_fixes.py index ef62872..18b8278 100644 --- a/tests/test_tool_use_fixes.py +++ b/tests/test_tool_use_fixes.py @@ -109,3 +109,35 @@ async def test_search_projects_tool_ranks_substring_match_above_others(): top = result["data"]["projects"][0] assert top["id"] == 5, f"expected Famous-Supply to rank first, got {top}" assert top["score"] >= 0.85 + + +@pytest.mark.asyncio +async def test_resolve_project_finds_colloquial_match(monkeypatch): + """resolve_project must surface 'Famous-Supply Work topics' when the + user passes 'famous supply project' — substring match via the shared + score helper, score 0.85 ≥ 0.55 threshold.""" + from fabledassistant.services.tools import _helpers + + fake_projects = [ + _project(12, "Minstrel", auto_summary="self-hosted music server"), + _project(5, "Famous-Supply Work topics", auto_summary="AT&T fiber circuit"), + ] + + async def fake_get_project_by_title(uid, name): + return None # force the scored path + + async def fake_list_projects(uid): + return fake_projects + + monkeypatch.setattr( + "fabledassistant.services.projects.get_project_by_title", + fake_get_project_by_title, + ) + monkeypatch.setattr( + "fabledassistant.services.projects.list_projects", + fake_list_projects, + ) + + result = await _helpers.resolve_project(user_id=1, project_name="famous supply project") + assert result is not None + assert result.id == 5