feat(tools): score_project_match helper with substring-first tiering
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,37 @@ def schedule_embedding(note_id: int, user_id: int, title: str, body: str) -> Non
|
|||||||
asyncio.create_task(upsert_note_embedding(note_id, user_id, text))
|
asyncio.create_task(upsert_note_embedding(note_id, user_id, text))
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize(s: str) -> str:
|
||||||
|
"""Lowercase and collapse non-alphanumerics to single spaces."""
|
||||||
|
return re.sub(r"[^a-z0-9]+", " ", s.lower()).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def score_project_match(query: str, project) -> float:
|
||||||
|
"""Score how well `query` matches `project`. Range [0.0, 1.0].
|
||||||
|
|
||||||
|
Tiered: exact title → 1.0, substring either-way → 0.85, query found in
|
||||||
|
description/summary → 0.70, otherwise SequenceMatcher ratio. Substring
|
||||||
|
tiers exist because LLM-generated colloquial queries (e.g. "famous
|
||||||
|
supply project" for "Famous-Supply Work topics") would otherwise score
|
||||||
|
too low under pure SequenceMatcher and be treated as no match.
|
||||||
|
"""
|
||||||
|
q = _normalize(query)
|
||||||
|
if not q:
|
||||||
|
return 0.0
|
||||||
|
title = _normalize(project.title)
|
||||||
|
description = _normalize(project.description or "")
|
||||||
|
summary = _normalize(project.auto_summary or "")
|
||||||
|
combined = f"{title} {description} {summary}".strip()
|
||||||
|
|
||||||
|
if q == title:
|
||||||
|
return 1.0
|
||||||
|
if q in title or title in q:
|
||||||
|
return 0.85
|
||||||
|
if q in combined:
|
||||||
|
return 0.70
|
||||||
|
return SequenceMatcher(None, q, combined).ratio()
|
||||||
|
|
||||||
|
|
||||||
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-fuzzy project lookup. Returns the Project or None.
|
||||||
|
|
||||||
|
|||||||
@@ -54,3 +54,36 @@ def test_strip_type_nouns_handles_empty_string():
|
|||||||
from fabledassistant.services.notes import _strip_type_nouns
|
from fabledassistant.services.notes import _strip_type_nouns
|
||||||
assert _strip_type_nouns("") == []
|
assert _strip_type_nouns("") == []
|
||||||
assert _strip_type_nouns(" ") == []
|
assert _strip_type_nouns(" ") == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_score_project_match_exact_title_returns_1():
|
||||||
|
from fabledassistant.services.tools._helpers import score_project_match
|
||||||
|
p = _project(5, "Famous-Supply Work topics")
|
||||||
|
assert score_project_match("Famous-Supply Work topics", p) == 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_score_project_match_colloquial_substring_at_least_85():
|
||||||
|
"""'famous supply' is a substring of normalized 'famous supply work topics'
|
||||||
|
after stripping the hyphen. Substring match returns 0.85."""
|
||||||
|
from fabledassistant.services.tools._helpers import score_project_match
|
||||||
|
p = _project(5, "Famous-Supply Work topics", auto_summary="AT&T fiber circuit")
|
||||||
|
score = score_project_match("famous supply project", p)
|
||||||
|
assert score >= 0.85, f"expected substring tier (>=0.85), got {score}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_score_project_match_query_in_summary_returns_70():
|
||||||
|
from fabledassistant.services.tools._helpers import score_project_match
|
||||||
|
p = _project(12, "Minstrel", auto_summary="self-hosted music server")
|
||||||
|
assert score_project_match("music server", p) == 0.70
|
||||||
|
|
||||||
|
|
||||||
|
def test_score_project_match_unrelated_returns_low():
|
||||||
|
from fabledassistant.services.tools._helpers import score_project_match
|
||||||
|
p = _project(12, "Minstrel", auto_summary="self-hosted music server")
|
||||||
|
assert score_project_match("garden renovation", p) < 0.5
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user