diff --git a/src/fabledassistant/services/embeddings.py b/src/fabledassistant/services/embeddings.py index 55947ee..7033ecc 100644 --- a/src/fabledassistant/services/embeddings.py +++ b/src/fabledassistant/services/embeddings.py @@ -80,6 +80,7 @@ async def semantic_search_notes( limit: int = 8, threshold: float = _SIMILARITY_THRESHOLD, project_id: int | None = None, + is_task: bool | None = None, ) -> list[tuple[float, Note]]: """Return up to *limit* (score, note) pairs most relevant to *query*. @@ -102,6 +103,10 @@ async def semantic_search_notes( ) if project_id is not None: stmt = stmt.where(Note.project_id == project_id) + if is_task is True: + stmt = stmt.where(Note.status.isnot(None)) + elif is_task is False: + stmt = stmt.where(Note.status.is_(None)) if exclude_ids: stmt = stmt.where(NoteEmbedding.note_id.notin_(exclude_ids)) rows = list((await session.execute(stmt)).all()) diff --git a/src/fabledassistant/services/tools.py b/src/fabledassistant/services/tools.py index 14cfced..54050d8 100644 --- a/src/fabledassistant/services/tools.py +++ b/src/fabledassistant/services/tools.py @@ -813,26 +813,24 @@ async def execute_tool(user_id: int, tool_name: str, arguments: dict) -> dict: return {"success": False, "error": f"Milestone '{milestone_name}' not found in project '{proj.title}'. Use list_milestones to see available milestones."} pre_fields["milestone_id"] = ms.id - existing = await get_note_by_title(user_id, task_title) - if existing is not None: - item_type = "task" if existing.status is not None else "note" - return {"success": False, "error": f"A {item_type} titled '{task_title}' already exists (id: {existing.id}). Use update_note to modify it instead of creating a duplicate."} + existing_tasks, _ = await list_notes(user_id=user_id, q=task_title, is_task=True, limit=1) + exact = next((t for t in existing_tasks if t.title.lower() == task_title.lower()), None) + if exact is not None: + return {"success": False, "error": f"A task titled '{task_title}' already exists (id: {exact.id}). Use update_note to modify it instead of creating a duplicate."} clean_q = _PUNCT_RE.sub(" ", task_title).strip() - candidates, _ = await list_notes(user_id=user_id, q=clean_q, limit=20) + candidates, _ = await list_notes(user_id=user_id, q=clean_q, is_task=True, limit=20) near, ratio = _fuzzy_title_match(task_title, candidates) if near is not None: - item_type = "task" if near.status is not None else "note" - return {"success": False, "requires_confirmation": True, "similar_note": {"id": near.id, "title": near.title}, "error": f"A {item_type} with a very similar title '{near.title}' already exists (similarity: {ratio:.0%}). Ask the user to confirm before creating a separate entry."} + return {"success": False, "requires_confirmation": True, "similar_note": {"id": near.id, "title": near.title}, "error": f"A task with a very similar title '{near.title}' already exists (similarity: {ratio:.0%}). Ask the user to confirm before creating a separate entry."} if not arguments.get("confirmed") and len(task_body.strip()) >= 200: from fabledassistant.services.embeddings import semantic_search_notes as _ssn sem_query = f"{task_title}\n{task_body}".strip() - sem_hits = await _ssn(user_id, sem_query, limit=3, threshold=0.90) + sem_hits = await _ssn(user_id, sem_query, limit=3, threshold=0.90, is_task=True) if sem_hits: best_score, best_note = sem_hits[0] - item_type = "task" if best_note.status is not None else "note" - return {"success": False, "requires_confirmation": True, "similar_note": {"id": best_note.id, "title": best_note.title}, "error": f"A {item_type} with very similar content exists: '{best_note.title}' (semantic similarity: {best_score:.0%}). Ask the user to confirm before creating a separate entry."} + return {"success": False, "requires_confirmation": True, "similar_note": {"id": best_note.id, "title": best_note.title}, "error": f"A task with very similar content exists: '{best_note.title}' (semantic similarity: {best_score:.0%}). Ask the user to confirm before creating a separate entry."} note = await create_note( user_id=user_id, @@ -891,26 +889,24 @@ async def execute_tool(user_id: int, tool_name: str, arguments: dict) -> dict: return {"success": False, "error": f"Project '{project_name}' not found. Use list_projects to see available projects or create_project to create one."} project_id = proj.id - existing = await get_note_by_title(user_id, note_title) - if existing is not None: - item_type = "task" if existing.status is not None else "note" - return {"success": False, "error": f"A {item_type} titled '{note_title}' already exists (id: {existing.id}). Use update_note to modify it instead of creating a duplicate."} + existing_notes, _ = await list_notes(user_id=user_id, q=note_title, is_task=False, limit=1) + exact = next((n for n in existing_notes if n.title.lower() == note_title.lower()), None) + if exact is not None: + return {"success": False, "error": f"A note titled '{note_title}' already exists (id: {exact.id}). Use update_note to modify it instead of creating a duplicate."} clean_q = _PUNCT_RE.sub(" ", note_title).strip() - candidates, _ = await list_notes(user_id=user_id, q=clean_q, limit=20) + candidates, _ = await list_notes(user_id=user_id, q=clean_q, is_task=False, limit=20) near, ratio = _fuzzy_title_match(note_title, candidates) if near is not None: - item_type = "task" if near.status is not None else "note" - return {"success": False, "requires_confirmation": True, "similar_note": {"id": near.id, "title": near.title}, "error": f"A {item_type} with a very similar title '{near.title}' already exists (similarity: {ratio:.0%}). Ask the user to confirm before creating a separate entry."} + return {"success": False, "requires_confirmation": True, "similar_note": {"id": near.id, "title": near.title}, "error": f"A note with a very similar title '{near.title}' already exists (similarity: {ratio:.0%}). Ask the user to confirm before creating a separate entry."} if not arguments.get("confirmed") and len(note_body.strip()) >= 200: from fabledassistant.services.embeddings import semantic_search_notes as _ssn sem_query = f"{note_title}\n{note_body}".strip() - sem_hits = await _ssn(user_id, sem_query, limit=3, threshold=0.90) + sem_hits = await _ssn(user_id, sem_query, limit=3, threshold=0.90, is_task=False) if sem_hits: best_score, best_note = sem_hits[0] - item_type = "task" if best_note.status is not None else "note" - return {"success": False, "requires_confirmation": True, "similar_note": {"id": best_note.id, "title": best_note.title}, "error": f"A {item_type} with very similar content exists: '{best_note.title}' (semantic similarity: {best_score:.0%}). Ask the user to confirm before creating a separate entry."} + return {"success": False, "requires_confirmation": True, "similar_note": {"id": best_note.id, "title": best_note.title}, "error": f"A note with very similar content exists: '{best_note.title}' (semantic similarity: {best_score:.0%}). Ask the user to confirm before creating a separate entry."} note = await create_note( user_id=user_id,