refactor(tools): resolve_project uses shared score_project_match helper
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -54,30 +54,24 @@ def score_project_match(query: str, project) -> float:
|
|||||||
|
|
||||||
|
|
||||||
async def resolve_project(user_id: int, project_name: str):
|
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:
|
Resolution order:
|
||||||
1. Exact title match (case-insensitive via DB)
|
1. Exact title match (case-insensitive via DB query).
|
||||||
2. project_name is a substring of an existing title
|
2. Highest `score_project_match` across all projects, threshold 0.55.
|
||||||
3. Existing title is a substring of project_name
|
|
||||||
4. SequenceMatcher ratio >= 0.55
|
|
||||||
"""
|
"""
|
||||||
from fabledassistant.services.projects import get_project_by_title, list_projects
|
from fabledassistant.services.projects import get_project_by_title, list_projects
|
||||||
|
|
||||||
proj = await get_project_by_title(user_id, project_name)
|
proj = await get_project_by_title(user_id, project_name)
|
||||||
if proj is not None:
|
if proj is not None:
|
||||||
return proj
|
return proj
|
||||||
needle = project_name.lower().strip()
|
|
||||||
all_p = await list_projects(user_id)
|
all_p = await list_projects(user_id)
|
||||||
|
best, best_score = None, 0.0
|
||||||
for p in all_p:
|
for p in all_p:
|
||||||
haystack = p.title.lower().strip()
|
score = score_project_match(project_name, p)
|
||||||
if needle in haystack or haystack in needle:
|
if score >= 0.55 and score > best_score:
|
||||||
return p
|
best, best_score = p, score
|
||||||
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
|
|
||||||
return best
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -109,3 +109,35 @@ async def test_search_projects_tool_ranks_substring_match_above_others():
|
|||||||
top = result["data"]["projects"][0]
|
top = result["data"]["projects"][0]
|
||||||
assert top["id"] == 5, f"expected Famous-Supply to rank first, got {top}"
|
assert top["id"] == 5, f"expected Famous-Supply to rank first, got {top}"
|
||||||
assert top["score"] >= 0.85
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user