refactor(tools): search_projects uses shared score_project_match helper
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -135,20 +135,12 @@ async def update_project_tool(*, user_id, arguments, **_ctx):
|
|||||||
briefing=True,
|
briefing=True,
|
||||||
)
|
)
|
||||||
async def search_projects_tool(*, user_id, arguments, **_ctx):
|
async def search_projects_tool(*, user_id, arguments, **_ctx):
|
||||||
from difflib import SequenceMatcher
|
|
||||||
|
|
||||||
from fabledassistant.services.projects import list_projects
|
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)
|
projects = await list_projects(user_id)
|
||||||
scored: list[tuple[float, object]] = []
|
scored: list[tuple[float, object]] = [(score_project_match(query, p), p) for p in projects]
|
||||||
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.sort(key=lambda x: x[0], reverse=True)
|
scored.sort(key=lambda x: x[0], reverse=True)
|
||||||
results = []
|
results = []
|
||||||
for score, p in scored[:5]:
|
for score, p in scored[:5]:
|
||||||
|
|||||||
@@ -87,3 +87,25 @@ def test_score_project_match_empty_query_returns_zero():
|
|||||||
from fabledassistant.services.tools._helpers import score_project_match
|
from fabledassistant.services.tools._helpers import score_project_match
|
||||||
p = _project(5, "Famous-Supply Work topics")
|
p = _project(5, "Famous-Supply Work topics")
|
||||||
assert score_project_match("", p) == 0.0
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user