Phase 20: Dedicated tag field — chip input, explicit tags array
Tags are now a first-class field rather than being auto-extracted from the note body. A new TagInput.vue chip component handles tag entry in both editor views with autocomplete, Enter/comma/backspace UX, and space-to-hyphen sanitization. Backend: - routes/notes.py: create reads tags from JSON; update accepts explicit tags (omit = keep existing); append_tag writes to tags array with dedup; suggest-tags accepts current_tags filter; remove extract_tags - routes/tasks.py: same — explicit tags on create/update; remove extract_tags - services/tag_suggestions.py: current_tags param replaces body extraction - services/tools.py: create_note tool schema adds tags param; executor passes it - services/llm.py: system prompt tells LLM to use tags param, not embed #tag in body Frontend: - components/TagInput.vue: new chip-based tag input (autocomplete, keyboard UX) - NoteEditorView.vue / TaskEditorView.vue: tags ref loaded from note.tags; TagInput placed between title and body; save/autosave include tags; suggest now adds chips; fetchTagSuggestions passes current_tags; dirty tracks tags - TiptapEditor.vue: remove fetchTags prop and TagSuggestion extension; keep TagDecoration for legacy inline #tag highlighting No DB migration needed — tags column already correct. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -337,7 +337,7 @@ async def build_context(
|
||||
tool_lines.append("When the user says 'remind me' with a time before an event, use the reminder_minutes parameter.")
|
||||
tool_lines.append("Use search_todos to find a specific CalDAV todo by keyword when list_todos would return too many results.")
|
||||
tool_lines.append("For relative dates like 'Friday' or 'next week', resolve them to YYYY-MM-DD format.")
|
||||
tool_lines.append("When writing #tags in note bodies, use hyphens for multi-word tags (e.g. #science-fiction, #space-travel). Never use spaces inside a tag.")
|
||||
tool_lines.append("When creating notes, use the `tags` parameter — do not embed #tag text in the note body.")
|
||||
tool_lines.append(
|
||||
"Use update_note to edit/expand an existing note OR to update a task's status/priority/due_date. "
|
||||
"Use create_note ONLY for genuinely new notes with a different title. "
|
||||
|
||||
@@ -8,16 +8,15 @@ from fabledassistant.config import Config
|
||||
from fabledassistant.services.llm import generate_completion
|
||||
from fabledassistant.services.notes import get_all_tags
|
||||
from fabledassistant.services.settings import get_setting
|
||||
from fabledassistant.utils.tags import extract_tags
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def suggest_tags(user_id: int, title: str, body: str) -> list[str]:
|
||||
async def suggest_tags(user_id: int, title: str, body: str, current_tags: list[str] | None = None) -> list[str]:
|
||||
"""Suggest relevant tags for a note/task based on its content.
|
||||
|
||||
Returns a list of tag strings (without # prefix), excluding tags
|
||||
already present in the body.
|
||||
already present in current_tags.
|
||||
"""
|
||||
if not title.strip() and not body.strip():
|
||||
return []
|
||||
@@ -54,9 +53,9 @@ async def suggest_tags(user_id: int, title: str, body: str) -> list[str]:
|
||||
|
||||
tags = _parse_tag_list(response)
|
||||
|
||||
# Filter out tags already in the body
|
||||
body_tags = set(extract_tags(body))
|
||||
tags = [t for t in tags if t not in body_tags]
|
||||
# Filter out tags already applied
|
||||
existing = set(current_tags or [])
|
||||
tags = [t for t in tags if t not in existing]
|
||||
|
||||
return tags[:5]
|
||||
|
||||
|
||||
@@ -81,6 +81,11 @@ _CORE_TOOLS = [
|
||||
"type": "string",
|
||||
"description": "The note content in markdown",
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Tags for the note (without # prefix, hyphens for multi-word: [\"science-fiction\", \"story/idea\"]). Do NOT embed #tags in the note body.",
|
||||
},
|
||||
},
|
||||
"required": ["title"],
|
||||
},
|
||||
@@ -710,10 +715,12 @@ async def execute_tool(user_id: int, tool_name: str, arguments: dict) -> dict:
|
||||
elif tool_name == "create_note":
|
||||
note_title = arguments.get("title", "Untitled Note")
|
||||
note_body = arguments.get("body", "")
|
||||
note_tags = arguments.get("tags", [])
|
||||
note = await create_note(
|
||||
user_id=user_id,
|
||||
title=note_title,
|
||||
body=note_body,
|
||||
tags=note_tags,
|
||||
)
|
||||
suggested = await suggest_tags(user_id, note_title, note_body)
|
||||
_schedule_embedding(note.id, user_id, note_title, note_body)
|
||||
|
||||
Reference in New Issue
Block a user