feat(agent): v5.11.0 — auto-title per request, history hover preview, write_blocks normalization, Notion-style steps accordion
This commit is contained in:
@@ -479,17 +479,90 @@ class UpdatePage(Tool):
|
||||
class WriteBlocks(Tool):
|
||||
name = "write_blocks"
|
||||
description = ("Met à jour un document (page éditeur `pages`) : remplace son contenu en blocs "
|
||||
"et/ou renomme son titre.")
|
||||
"et/ou renomme son titre. Chaque bloc est un objet JSON "
|
||||
"{\"type\": \"...\", \"content\": \"texte du bloc\"}. Types autorisés : "
|
||||
"paragraph, heading_1, heading_2, heading_3, heading_4, bulleted_list, "
|
||||
"numbered_list, to_do, quote, divider, toggle, callout, code. "
|
||||
"Utilise `type: \"code\"` (avec `language`) pour un script ou du code "
|
||||
"multi-lignes, `heading_1`/`heading_2`/… pour les titres. "
|
||||
"Le champ contenant le texte s'appelle `content`, PAS `text`.")
|
||||
parameters = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"page_id": {"type": "integer", "description": "id du document (page éditeur)"},
|
||||
"blocks": {"type": "array", "description": "nouveau contenu en blocs (remplace le contenu)"},
|
||||
"blocks": {"type": "array",
|
||||
"description": "nouveau contenu en blocs (remplace le contenu). "
|
||||
"Exemple: [{\"type\": \"heading_1\", \"content\": \"Titre\"}, "
|
||||
"{\"type\": \"code\", \"content\": \"Write-Host 'hi'\", \"language\": \"powershell\"}]"},
|
||||
"title": {"type": "string", "description": "nouveau titre (optionnel)"},
|
||||
},
|
||||
"required": ["page_id"],
|
||||
}
|
||||
|
||||
_VALID_BLOCK_TYPES = frozenset({
|
||||
"paragraph", "heading_1", "heading_2", "heading_3", "heading_4",
|
||||
"bulleted_list", "numbered_list", "to_do", "toggle", "quote",
|
||||
"callout", "table_of_contents", "math", "columns", "divider",
|
||||
"code", "table", "button", "image", "embed", "bookmark",
|
||||
"video", "audio", "meeting",
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def _normalize_blocks(cls, blocks: list) -> list:
|
||||
"""Normalize blocks coming from the LLM so the editor can render them.
|
||||
|
||||
Common LLM mistakes handled:
|
||||
- ``text`` instead of ``content``
|
||||
- ``heading`` instead of ``heading_1`` / ``heading_2`` / …
|
||||
- ``bullet`` / ``list`` instead of ``bulleted_list``
|
||||
- ``numbered`` instead of ``numbered_list``
|
||||
- ``check`` / ``checkbox`` instead of ``to_do``
|
||||
- ``code_block`` instead of ``code``
|
||||
- ``h1`` … ``h6`` shorthand
|
||||
- Missing ``id`` fields (editor assigns them on load but we add them
|
||||
to keep the JSON self-contained)
|
||||
"""
|
||||
import uuid
|
||||
|
||||
_TYPE_MAP = {
|
||||
"heading": "heading_1", "h1": "heading_1", "h2": "heading_2",
|
||||
"h3": "heading_3", "h4": "heading_4", "h5": "heading_4",
|
||||
"h6": "heading_4",
|
||||
"bullet": "bulleted_list", "bullets": "bulleted_list", "list": "bulleted_list",
|
||||
"numbered": "numbered_list", "numbered_list": "numbered_list",
|
||||
"check": "to_do", "checkbox": "to_do", "task": "to_do",
|
||||
"code_block": "code",
|
||||
"hr": "divider", "horizontal_rule": "divider", "horizontal rule": "divider",
|
||||
}
|
||||
|
||||
out = []
|
||||
for block in (blocks or []):
|
||||
if not isinstance(block, dict):
|
||||
continue
|
||||
b = dict(block)
|
||||
# --- normalize type ---
|
||||
raw_type = (b.get("type") or "paragraph").strip().lower()
|
||||
btype = _TYPE_MAP.get(raw_type, raw_type)
|
||||
if btype not in cls._VALID_BLOCK_TYPES:
|
||||
btype = "paragraph"
|
||||
b["type"] = btype
|
||||
# --- normalize content field (LLM often sends "text" instead of "content") ---
|
||||
if "content" not in b and "text" in b:
|
||||
b["content"] = b.pop("text")
|
||||
elif "content" not in b:
|
||||
# Try other common fields
|
||||
for alt in ("value", "body", "source"):
|
||||
if alt in b:
|
||||
b["content"] = b.pop(alt)
|
||||
break
|
||||
else:
|
||||
b.setdefault("content", "")
|
||||
# --- generate id if missing ---
|
||||
if not b.get("id"):
|
||||
b["id"] = f"b_{uuid.uuid4().hex[:12]}"
|
||||
out.append(b)
|
||||
return out
|
||||
|
||||
async def execute(self, args, *, user_id=None) -> ToolResult:
|
||||
pid = args.get("page_id")
|
||||
has_blocks = "blocks" in args
|
||||
@@ -506,9 +579,10 @@ class WriteBlocks(Tool):
|
||||
"title": page["title"]}
|
||||
final_title = new_title or page["title"]
|
||||
if has_blocks:
|
||||
normalized = self._normalize_blocks(args["blocks"])
|
||||
conn.execute(
|
||||
"UPDATE pages SET content=?, content_format='blocks', title=?, updated_at=CURRENT_TIMESTAMP WHERE id=?",
|
||||
(json.dumps(args["blocks"], ensure_ascii=False), final_title, pid),
|
||||
(json.dumps(normalized, ensure_ascii=False), final_title, pid),
|
||||
)
|
||||
else:
|
||||
conn.execute("UPDATE pages SET title=?, updated_at=CURRENT_TIMESTAMP WHERE id=?",
|
||||
|
||||
Reference in New Issue
Block a user