Update note management from db vector to index
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
"""Note agent — Markdown note management (list, get, create, update, delete)."""
|
||||
"""Note agent — Markdown note management (list, get, create, update, propose edit)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.tools import tool
|
||||
|
||||
from app.core.llm import embed
|
||||
from app.core.note_summarizer import generate_note_summary
|
||||
from app.core.ws_context import execute_on_client
|
||||
|
||||
_UUID_RE = re.compile(
|
||||
@@ -19,9 +20,21 @@ def _is_uuid(value: str) -> bool:
|
||||
return bool(_UUID_RE.match(value))
|
||||
|
||||
|
||||
def _fmt_summary(row: dict) -> str:
|
||||
summary = (row.get("aiSummary") or row.get("ai_summary") or "").strip()
|
||||
if summary:
|
||||
return f" — {summary}"
|
||||
snippet = (row.get("content") or "")[:120].replace("\n", " ").strip()
|
||||
return f" — {snippet}" if snippet else ""
|
||||
|
||||
|
||||
@tool
|
||||
async def list_notes(project_id: str = "") -> str:
|
||||
"""List notes, optionally scoped to a project by project_id."""
|
||||
"""List notes with AI summaries, optionally scoped to a project by project_id.
|
||||
|
||||
Returns id, title, and ai_summary for each note so you can decide which
|
||||
note to read in full with get_note before creating or updating.
|
||||
"""
|
||||
normalized_project_id = project_id if (project_id and _is_uuid(project_id)) else ""
|
||||
result = await execute_on_client(
|
||||
action="select",
|
||||
@@ -31,7 +44,7 @@ async def list_notes(project_id: str = "") -> str:
|
||||
rows = result.get("rows", [])
|
||||
if not rows:
|
||||
return "No notes found."
|
||||
lines = [f"- {r['title']} (id: {r['id']})" for r in rows]
|
||||
lines = [f" - [{r['id']}] {r['title']}{_fmt_summary(r)}" for r in rows]
|
||||
return f"Found {len(rows)} note(s):\n" + "\n".join(lines)
|
||||
|
||||
|
||||
@@ -66,14 +79,10 @@ async def create_note(
|
||||
},
|
||||
)
|
||||
row = result["row"]
|
||||
# Index the note content in the vector store.
|
||||
vector = await embed(content)
|
||||
await execute_on_client(
|
||||
action="vector_upsert",
|
||||
data={"id": row["id"], "projectId": row.get("projectId"), "content": content},
|
||||
vector=vector,
|
||||
)
|
||||
return f"Note created: '{row['title']}' (id: {row['id']})."
|
||||
note_id: str = row["id"]
|
||||
# Generate summary asynchronously — fire-and-forget.
|
||||
asyncio.create_task(_refresh_summary(note_id, title, content))
|
||||
return f"Note created: '{row['title']}' (id: {note_id})."
|
||||
|
||||
|
||||
@tool
|
||||
@@ -82,7 +91,8 @@ async def update_note(
|
||||
title: str = "",
|
||||
content: str = "",
|
||||
) -> str:
|
||||
"""Update an existing note. Only pass fields that should change.
|
||||
"""Update an existing note directly (no approval required).
|
||||
Use propose_note_edit instead when human review is needed.
|
||||
note_id: UUID of the note (required)
|
||||
If you need to preserve existing content, call get_note first.
|
||||
"""
|
||||
@@ -97,17 +107,63 @@ async def update_note(
|
||||
data={"id": note_id, "updates": updates},
|
||||
)
|
||||
row = result["row"]
|
||||
# Re-index if content changed.
|
||||
if content:
|
||||
vector = await embed(content)
|
||||
await execute_on_client(
|
||||
action="vector_upsert",
|
||||
data={"id": note_id, "projectId": row.get("projectId"), "content": content},
|
||||
vector=vector,
|
||||
)
|
||||
new_title = title or row.get("title", "")
|
||||
asyncio.create_task(_refresh_summary(note_id, new_title, content))
|
||||
return f"Note updated: '{row['title']}' (id: {row['id']})."
|
||||
|
||||
|
||||
@tool
|
||||
async def propose_note_edit(
|
||||
note_id: str,
|
||||
edit_type: str,
|
||||
proposed_content: str,
|
||||
reasoning: str = "",
|
||||
anchor_before: str = "",
|
||||
anchor_text: str = "",
|
||||
agent_id: str = "",
|
||||
run_id: str = "",
|
||||
) -> str:
|
||||
"""Propose an AI edit to an existing note, pending human approval.
|
||||
|
||||
Use this instead of update_note when review_required is true.
|
||||
The user will see the proposal highlighted before it is merged.
|
||||
|
||||
note_id: UUID of the target note (required)
|
||||
edit_type: 'append' | 'insert' | 'replace'
|
||||
- append: adds proposed_content at the end of the note
|
||||
- insert: inserts proposed_content immediately after anchor_before text
|
||||
- replace: replaces the first occurrence of anchor_text with proposed_content
|
||||
proposed_content: the new Markdown text to add or substitute (required)
|
||||
reasoning: brief explanation shown to the user (recommended)
|
||||
anchor_before: for 'insert' — the text snippet that precedes the insertion point
|
||||
anchor_text: for 'replace' — the exact text to be replaced
|
||||
agent_id: agent identifier (for traceability)
|
||||
run_id: run identifier (for traceability)
|
||||
"""
|
||||
if edit_type not in ("append", "insert", "replace"):
|
||||
return f"Invalid edit_type '{edit_type}'. Use 'append', 'insert', or 'replace'."
|
||||
|
||||
result = await execute_on_client(
|
||||
action="propose_note_edit",
|
||||
data={
|
||||
"noteId": note_id,
|
||||
"type": edit_type,
|
||||
"proposedContent": proposed_content,
|
||||
"reasoning": reasoning or None,
|
||||
"anchorBefore": anchor_before or None,
|
||||
"anchorText": anchor_text or None,
|
||||
"agentId": agent_id or None,
|
||||
"runId": run_id or None,
|
||||
},
|
||||
)
|
||||
edit_id = result.get("id", "?")
|
||||
return (
|
||||
f"Edit proposal created (id: {edit_id}) for note {note_id}. "
|
||||
f"Status: pending user approval."
|
||||
)
|
||||
|
||||
|
||||
@tool
|
||||
async def delete_note(note_id: str) -> str:
|
||||
"""Delete a note permanently by its UUID."""
|
||||
@@ -115,11 +171,32 @@ async def delete_note(note_id: str) -> str:
|
||||
return f"Note {note_id} deleted."
|
||||
|
||||
|
||||
async def _refresh_summary(note_id: str, title: str, content: str) -> None:
|
||||
"""Generate and persist the AI summary for a note. Fire-and-forget."""
|
||||
try:
|
||||
summary = await generate_note_summary(title, content)
|
||||
if summary:
|
||||
await execute_on_client(
|
||||
action="update",
|
||||
table="notes",
|
||||
data={
|
||||
"id": note_id,
|
||||
"updates": {
|
||||
"aiSummary": summary,
|
||||
"aiSummaryUpdatedAt": int(__import__("time").time() * 1000),
|
||||
},
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
pass # fire-and-forget; errors logged by generate_note_summary
|
||||
|
||||
|
||||
NOTE_TOOLS: list[Any] = [
|
||||
list_notes,
|
||||
get_note,
|
||||
create_note,
|
||||
update_note,
|
||||
propose_note_edit,
|
||||
delete_note,
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user