Polish email and cookbook flows

This commit is contained in:
pewdiepie-archdaemon
2026-06-02 22:38:55 +09:00
parent 15a2662119
commit ff93a6c63b
22 changed files with 1492 additions and 218 deletions

View File

@@ -115,6 +115,7 @@ _API_AGENT_RULES = """\
- Keep answers concise unless the user asks for depth.
- For long code or content, use document tools instead of pasting large blocks into chat.
- Editing an existing document: ALWAYS use `edit_document` with find/replace. Only use `update_document` for genuine full rewrites (>50% changed) — do NOT echo the entire file back for small edits.
- If the active editor document is an email draft/compose window, treat that open email as the target for "write this", "write the email", "reply with...", "make it say...", "draft this", and similar requests. Do NOT create another document, search/list/manage documents, or open a different reply unless the user explicitly asks. Edit the open email draft with `edit_document` or `update_document`; preserve To/Cc/Bcc/Subject/In-Reply-To/References/X-* header lines unless the user asks to change them.
- "Give suggestions / feedback / review / how can I improve this / what would make it better" about the OPEN document → call `suggest_document`, do NOT write a prose list of ideas in chat. It creates inline accept/reject bubbles on the doc. Give concrete `find`/`replace`/`reason` items. To suggest an ADDITION (e.g. "add a bow to the SVG", a new section), set `find` to a short existing anchor snippet and `replace` to that same snippet PLUS the new content. Only answer in prose when no document is open, or the request is purely conceptual with no concrete change to propose.
- BIAS TOWARD ACTION on edit requests. If the user says "edit out X", "remove the Y paragraph", "change Z" — call the edit tool with your best interpretation. Don't ask for clarification on minor ambiguity. The user can undo.
- AFTER A TOOL SUCCEEDS, do not second-guess. A success response means it worked. Reply in ONE short sentence confirming what was done. No verification thinking, no re-analyzing — move on.
@@ -642,6 +643,7 @@ def _build_system_prompt(
f'ACTIVE EMAIL DRAFT (open in editor — the user is looking at this right now)\n'
f'Title: "{active_document.title}"\n'
f'```\n{_doc_raw}\n```\n\n'
f'This is the current email compose window, not a normal document library item. If the user says "write", "draft", "reply", "make it say", or "write the email" without naming another target, edit THIS email draft.\n\n'
f'When the user asks you to write, reply to, or improve this email:\n'
f'1. Use `update_document` to replace the ENTIRE content — keep all the header lines (To, Subject, In-Reply-To, References, X-Source-UID, X-Source-Folder, X-Attachments) and the `---` separator EXACTLY as they are.\n'
f'2. Replace ONLY the body text (the part after `---`). If there is a quoted original email (lines starting with `>`), keep that quoted block unchanged BELOW your new reply.\n'
@@ -792,7 +794,7 @@ def _build_system_prompt(
# When creating email documents, instruct the AI on the format
if relevant_tools and (_EMAIL_TOOL_HINTS & set(relevant_tools)):
agent_prompt += (
'\n\n📧 EMAIL DOCUMENT FORMAT: When drafting email replies, use create_document with language="email". '
'\n\n📧 EMAIL DOCUMENT FORMAT: If no email draft is already open and you need to create an email draft, use create_document with language="email". '
'The content format is:\n'
'To: recipient@example.com\n'
'Subject: Re: Original subject\n'
@@ -800,8 +802,8 @@ def _build_system_prompt(
'References: <original-message-id>\n'
'---\n'
'Body text here...\n\n'
'The user can then edit and click Send or Draft in the editor. For an already-open email draft, '
'edit the current document instead of creating another one.'
'The user can then edit and click Send or Draft in the editor. If an email draft is already open, '
'that open draft is the target: use update_document/edit_document on it instead of creating another document.'
)
# Inject relevant skills based on the user's last message. The

View File

@@ -603,44 +603,98 @@ def _sanitize_llm_messages(messages: List[Dict]) -> List[Dict]:
elif "content" in item:
cleaned.append(item)
# Merge consecutive user messages to satisfy strict role alternation requirements.
merged = []
for item in cleaned:
# Repair tool-call adjacency before sending to any OpenAI-compatible
# provider. Trimming/compaction/retries can leave `role:"tool"` messages
# without their immediately-preceding assistant `tool_calls` parent, which
# DeepSeek rejects with:
# "Messages with role 'tool' must be a response to a preceding message with
# 'tool_calls'". Also strip unanswered assistant tool_calls; some providers
# reject those as incomplete conversations.
repaired: List[Dict] = []
i = 0
while i < len(cleaned):
msg = cleaned[i]
role = msg.get("role")
if role == "tool":
# Orphan tool result. There is no valid assistant tool_calls parent
# immediately before this batch, so it cannot be sent.
logger.debug("Dropping orphan tool message before provider request")
i += 1
continue
tool_calls = msg.get("tool_calls") if role == "assistant" else None
if not tool_calls:
repaired.append(msg)
i += 1
continue
call_ids = [
str(tc.get("id"))
for tc in tool_calls
if isinstance(tc, dict) and tc.get("id")
]
expected = set(call_ids)
answered_ids = []
tool_batch = []
j = i + 1
while j < len(cleaned) and cleaned[j].get("role") == "tool":
tid = str(cleaned[j].get("tool_call_id") or "")
if tid in expected and tid not in answered_ids:
answered_ids.append(tid)
tool_batch.append(cleaned[j])
else:
logger.debug("Dropping unmatched/duplicate tool message before provider request")
j += 1
if not tool_batch:
plain = {k: v for k, v in msg.items() if k != "tool_calls"}
if (plain.get("content") or "").strip():
repaired.append(plain)
else:
logger.debug("Dropping unanswered assistant tool_calls before provider request")
i = j
continue
answered = set(answered_ids)
pruned_calls = [
tc for tc in tool_calls
if isinstance(tc, dict) and str(tc.get("id")) in answered
]
fixed = dict(msg)
fixed["tool_calls"] = pruned_calls
if "content" not in fixed:
fixed["content"] = None
repaired.append(fixed)
repaired.extend(tool_batch)
if len(pruned_calls) != len(tool_calls):
logger.debug("Pruned unanswered assistant tool_calls before provider request")
i = j
# Merge consecutive user messages to satisfy strict role alternation
# requirements after invalid tool-call fragments have been removed.
merged: List[Dict] = []
for item in repaired:
if not merged:
merged.append(item)
continue
last = merged[-1]
if last["role"] == "user" and item["role"] == "user":
if last.get("role") == "user" and item.get("role") == "user":
last_copy = dict(last)
# Content:
last_content = last_copy.get("content")
item_content = item.get("content")
# Convert contents to string if they exist, or keep None/empty
last_str = str(last_content) if last_content is not None else ""
item_str = str(item_content) if item_content is not None else ""
if last_str and item_str:
new_content = f"{last_str}\n\n{item_str}"
elif last_str:
new_content = last_str
else:
new_content = item_str if item_str else None
if new_content is not None:
last_str = str(last_copy.get("content")) if last_copy.get("content") is not None else ""
item_str = str(item.get("content")) if item.get("content") is not None else ""
new_content = "\n\n".join(part for part in (last_str, item_str) if part)
if new_content:
last_copy["content"] = new_content
elif "content" in last_copy:
del last_copy["content"]
else:
last_copy.pop("content", None)
merged[-1] = last_copy
else:
merged.append(item)
return merged
def _normalize_anthropic_url(url: str) -> str:
"""Ensure Anthropic URL points to /v1/messages."""
url = url.rstrip("/")

View File

@@ -65,7 +65,7 @@ BUILTIN_TOOL_DESCRIPTIONS: Dict[str, str] = {
"web_fetch": "Fetch and read the text content of a specific URL/website the user names (e.g. 'check example.com', 'open this link'). Use when you have a concrete URL; for open-ended lookups use web_search instead.",
"read_file": "Read a file from disk and return its contents. View source code, config files, logs.",
"write_file": "Write content to a file on disk. Create new files, save output, update configs.",
"create_document": "Create a new document in the editor panel. For code, articles, text content longer than 15 lines. Specify title, language, and content.",
"create_document": "Create a new document in the editor panel. For code, articles, text content longer than 15 lines, unless an already-open document/email draft is the obvious target. If an email compose draft is open, edit that draft instead of creating another document.",
"edit_document": "Preferred tool for editing an existing document — targeted find-and-replace. Use for any small change: add a function, fix a bug, tweak a section, rename things.",
"update_document": "Replace the entire active document content. ONLY for full rewrites (>50% changed). Do not use for small edits — use edit_document instead.",
"suggest_document": "Suggest changes to the active document with explanations. For code review, proofreading, feedback requests.",

View File

@@ -111,7 +111,7 @@ FUNCTION_TOOL_SCHEMAS = [
"type": "function",
"function": {
"name": "create_document",
"description": "Create a new document in the editor panel. ALWAYS use this when the user asks to write, create, build, or generate code, scripts, programs, games, apps, or any substantial content (>15 lines). NEVER put large code blocks directly in chat — use this tool instead.",
"description": "Create a new document in the editor panel. Use this when the user asks to write, create, build, or generate code, scripts, programs, games, apps, or any substantial content (>15 lines) AND there is no already-open document/email draft that the request refers to. If an email compose draft is open, edit that draft instead of creating another document. NEVER put large code blocks directly in chat — use this tool instead.",
"parameters": {
"type": "object",
"properties": {