Project-aware assist, link suggestions, project-scoped RAG, semantic search tool, SSE race fix
- Writing assistant: inject project notes as context (definition-tagged first), wikilink suggestions - Link suggestions: server-side endpoint finds unlinked term occurrences, NoteEditorView sidebar panel - Project-scoped RAG: ChatView ProjectSelector filters semantic+keyword search to selected project - Semantic search tool: LLM search_notes upgraded to hybrid semantic (0.40 threshold) + keyword merge - SSE race condition fix: drain remaining events after stream loop exits in chat.py and notes.py - RAG_AUTO_SNIPPET raised 800→4000; sidebar include uses full note body; MAX_BODY_CHARS 8000→24000 - Enter-to-submit on writing assistant instruction textareas (note and task editors) - DiffView: equal-line collapsing with 3-line context around changes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -182,19 +182,29 @@ _CORE_TOOLS = [
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "search_notes",
|
||||
"description": "Search the user's notes and tasks. Use this when the user asks about their existing notes, tasks, or wants to find something they've written.",
|
||||
"description": (
|
||||
"Search notes and tasks using semantic understanding. Finds content related to a concept, "
|
||||
"theme, or topic — even when exact keywords don't appear in the title or body. "
|
||||
"Use this when the user asks what notes they have about a topic, wants to explore related "
|
||||
"content, or is looking for something they've written. Falls back to keyword search "
|
||||
"automatically if semantic search is unavailable."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Search query to find notes/tasks",
|
||||
"description": "A natural-language description of what you're looking for — concepts, themes, topics, or keywords",
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["note", "task"],
|
||||
"description": "Restrict results to only notes or only tasks. Omit to search both.",
|
||||
},
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "Optional project name to restrict search to notes in that project",
|
||||
},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
@@ -1070,27 +1080,79 @@ async def execute_tool(user_id: int, tool_name: str, arguments: dict) -> dict:
|
||||
elif tool_name == "search_notes":
|
||||
query = arguments.get("query", "")
|
||||
type_filter = arguments.get("type")
|
||||
project_name = arguments.get("project")
|
||||
|
||||
is_task: bool | None = None
|
||||
if type_filter == "note":
|
||||
is_task = False
|
||||
elif type_filter == "task":
|
||||
is_task = True
|
||||
notes, total = await list_notes(user_id=user_id, q=query, is_task=is_task, limit=5)
|
||||
results = []
|
||||
for n in notes:
|
||||
results.append({
|
||||
"id": n.id,
|
||||
"title": n.title,
|
||||
"type": "task" if n.status is not None else "note",
|
||||
"status": n.status,
|
||||
"preview": (n.body[:200] + "...") if n.body and len(n.body) > 200 else (n.body or ""),
|
||||
})
|
||||
|
||||
# Resolve project name → id
|
||||
search_project_id: int | None = None
|
||||
if project_name:
|
||||
from fabledassistant.services.projects import get_project_by_title as _gpbt2, list_projects as _lp2
|
||||
proj = await _gpbt2(user_id, project_name)
|
||||
if proj is None:
|
||||
all_p = await _lp2(user_id)
|
||||
matches = [p for p in all_p if project_name.lower() in p.title.lower()]
|
||||
proj = matches[0] if matches else None
|
||||
if proj:
|
||||
search_project_id = proj.id
|
||||
|
||||
results_map: dict[int, dict] = {}
|
||||
|
||||
# 1) Semantic search — broad threshold (0.40) since this is an intentional query
|
||||
try:
|
||||
from fabledassistant.services.embeddings import semantic_search_notes as _ssn
|
||||
sem_results = await _ssn(
|
||||
user_id, query, limit=8, threshold=0.40,
|
||||
project_id=search_project_id,
|
||||
)
|
||||
for score, n in sem_results:
|
||||
n_is_task = n.status is not None
|
||||
if is_task is True and not n_is_task:
|
||||
continue
|
||||
if is_task is False and n_is_task:
|
||||
continue
|
||||
results_map[n.id] = {
|
||||
"id": n.id,
|
||||
"title": n.title,
|
||||
"type": "task" if n_is_task else "note",
|
||||
"status": n.status,
|
||||
"relevance": f"{round(score * 100)}%",
|
||||
"preview": (n.body[:300] + "…") if n.body and len(n.body) > 300 else (n.body or ""),
|
||||
}
|
||||
logger.debug("search_notes semantic: %d results for %r", len(results_map), query)
|
||||
except Exception:
|
||||
logger.debug("Semantic search unavailable in search_notes, using keyword only", exc_info=True)
|
||||
|
||||
# 2) Keyword search — merge in anything not already found
|
||||
try:
|
||||
kw_notes, _ = await list_notes(
|
||||
user_id=user_id, q=query, is_task=is_task,
|
||||
project_id=search_project_id, limit=8,
|
||||
)
|
||||
for n in kw_notes:
|
||||
if n.id not in results_map:
|
||||
results_map[n.id] = {
|
||||
"id": n.id,
|
||||
"title": n.title,
|
||||
"type": "task" if n.status is not None else "note",
|
||||
"status": n.status,
|
||||
"relevance": "keyword",
|
||||
"preview": (n.body[:300] + "…") if n.body and len(n.body) > 300 else (n.body or ""),
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("Keyword fallback in search_notes failed", exc_info=True)
|
||||
|
||||
results = list(results_map.values())[:8]
|
||||
return {
|
||||
"success": True,
|
||||
"type": "search",
|
||||
"data": {
|
||||
"query": query,
|
||||
"total": total,
|
||||
"count": len(results),
|
||||
"results": results,
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user