工具协议与内置技能
支持原生函数调用的模型通过服务商 API 收到已启用的工具定义。配置为 tool_mode=prompt 的模型则收到这些工具的名称、描述、Schema 文本,以及严格调用协议。实现见 prompt_tools.go。
提示词模式前导
以下内容追加到普通 system prompt;<工具定义> 只包含本轮实际允许的工具。
## Available tools
You can call any of the tools below by emitting EXACTLY this format and then STOPPING:
<tool_call>{"name": "<tool>", "arguments": <args>}</tool_call>
Important rules:
- After emitting `</tool_call>` STOP. Do not write anything else and do not invent the result.
- The orchestrator will execute the tool and reply with a `<tool_result>` block. Continue from there.
- Never repeat a tool call with the same arguments. If the available evidence is sufficient, answer immediately.
- When a tool schema offers a queries or urls array, batch independent requests that are already known.
- If you don't need a tool, just answer the user directly.
Tools:
### <工具名称>
<工具描述>
Input schema: <JSON_SCHEMA>
服务商停止序列准确为:
</tool_call>
模型每轮只能发出一个调用,调用标记和 JSON 不会显示给用户。提示词模式有调用轮数上限,格式错误最多重试两次。
注入提示词的内置工具定义
启用对应工具时,以下描述和 Schema 会原样注入;管理员 MCP 工具和管理员技能属于运行时私有配置,不在此公开。
aivory_web_search
Search the public web for current information. Use query for one search or queries to batch several known searches into one tool call. Returns titled snippets with URLs.
Input schema: {"type":"object","properties":{"query":{"type":"string","description":"One search query. Use either query or queries."},"queries":{"type":"array","items":{"type":"string"},"maxItems":5,"description":"Up to 5 independent search queries. Prefer this when several searches are known in advance."},"top_k":{"type":"integer","minimum":1,"maximum":10,"description":"Maximum results per query."}}}
web_fetch
Fetch the main text content of web pages. Use url for one page or urls to batch several known pages into one tool call. SSRF-guarded: internal IPs are blocked.
Input schema: {"type":"object","properties":{"url":{"type":"string","description":"One web URL. Use either url or urls."},"urls":{"type":"array","items":{"type":"string"},"maxItems":4,"description":"Up to 4 web URLs to fetch in one tool call."}}}
fetch_image
Download an image from a public HTTP(S) URL into this conversation's Python sandbox. Returns a stable path under /workspace/downloads/ that python_execute can open with Pillow or use in documents.
Input schema: {"type":"object","properties":{"url":{"type":"string"},"filename":{"type":"string"}},"required":["url"]}
python_execute
Run Python in a persistent sandbox for math, data analysis, image editing, plotting, spreadsheet/CSV processing, editing existing PDF/Office documents, and generating downloadable files (PDF/PPTX/DOCX/XLSX/PNG). The session and its /workspace persist across calls AND across turns in this conversation, so call it several times in a row — inspect the inputs first, then edit or compute, and read again differently if the first attempt doesn't fit. Every conversation upload, including the original PDF/DOCX/PPTX/XLSX file, is staged without format conversion in /workspace/uploads/; prior image-generation outputs are staged there too, and public images fetched with fetch_image are stored in /workspace/downloads/. Run `import os; os.listdir('/workspace/uploads')` and inspect /workspace/downloads when needed, then use the real paths (for example python-docx/python-pptx/pypdf for documents, Pillow for images, and pandas for tables). Preserve the original file's layout and formatting when the user asks for a targeted edit. Write outputs, including edited images, plots, and documents, to /workspace/outputs to return them as downloadable artifacts. Produced files are attached to the assistant message automatically: refer to them by filename and never emit sandbox: or /workspace/outputs paths as download links. Stdout/stderr is returned.
Input schema: {"type":"object","properties":{"code":{"type":"string"}},"required":["code"]}
image_generate
Generate a new image or faithfully edit one existing image. You must explicitly choose action=generate or action=edit from the user's intent. Generate never sends conversation images to the image API. For edit, select exactly one authoritative base_image: previous_generation for the nearest generated image on the active branch, or current_attachment plus its 1-based base_image_index for an image uploaded this turn. Other current-turn images become edit references. Do not invent file ids.
Input schema: {"type":"object","properties":{"prompt":{"type":"string","description":"The requested new image or the exact edit instruction. For edits, describe only the requested change and preserve everything else."},"action":{"type":"string","enum":["generate","edit"],"description":"Use generate only when the user wants a new image. Use edit only when the user wants to modify an existing image."},"base_image":{"type":"string","enum":["none","previous_generation","current_attachment"],"description":"For generate use none. For edit, choose previous_generation only when continuing the prior generated result, or current_attachment when an image uploaded this turn is the authoritative base."},"base_image_index":{"type":"integer","minimum":1,"description":"Use only with base_image=current_attachment. This is the 1-based attachment position; omit it for previous_generation."},"n":{"type":"integer","default":1},"size":{"type":"string","description":"Optional explicit output size. OpenAI-format requests recognize the aspect ratio and the 1K, 2K, or 4K resolution tier from the exact user instruction; omitted resolution defaults to 2K. The final GPT Image 2 WIDTHxHEIGHT is normalized to legal multiples of 16. Edits preserve the selected base image's ratio unless the user requests another ratio. GPT Image 1.x is mapped to its supported fixed sizes."}},"required":["prompt","action","base_image"]}
use_skill
Load the full instructions for one of the skills the user/admin has registered (returned text contains the skill's complete how-to).
Input schema: {"type":"object","properties":{"name":{"type":"string"}},"required":["name"]}
save_memory
Save a durable fact about the user into long-term memory. Use ONLY when the user explicitly says "remember…" or asks you to. Status defaults to ACTIVE.
Input schema: {"type":"object","properties":{"memory_text":{"type":"string"},"slot":{"type":"string"},"value":{"type":"string"}},"required":["memory_text"]}
源码:builtins.go。
工具结果与错误包装
调用成功后,下一条用户层消息准确为:
<tool_result name="<工具名称>">
<不可信工具输出>
</tool_result>
Continue from here.
执行错误使用 tool_error:
<tool_error name="<工具名称>">
<公开错误输出>
</tool_error>
Continue from here.
主 system prompt 的信任边界把工具输出定义为参考资料。完整工具结果会保存到服务商无关的内部压缩信封,但适配器不会把该信封作为服务商原生历史重放。
模型输出的调用 JSON 无法解析时,收到以下有界重试消息:
<tool_error>
Your <tool_call> JSON failed to parse: <解析错误>
Re-emit the tool call as ONE valid JSON object: <tool_call>{"name": "<tool>", "arguments": {...}}</tool_call>
</tool_error>
两次重试仍失败后,把调用标记前的可见文本作为最终回答,不再无限循环。
工具停止与最终回答
调用次数、时间或轮数预算耗尽时,最后一次无工具请求收到:
The tool execution budget is exhausted. Do not call or request any tools. Based only on the conversation and tool results already available, provide the best possible final answer now. Do not discuss the tool budget unless it prevents you from answering.
编排器检测到重复或无进展调用时使用:
Further tool calls would not add new evidence. Do not call or request any tools. Based only on the conversation and tool results already available, provide the best possible final answer now. Do not discuss this internal stopping condition unless it prevents you from answering.
最终回答轮会移除工具声明。模型仍调用工具或没有返回回答时,本轮记录 tool_budget_exceeded 或 tool_no_progress,不会开启新循环。
发给模型的工具输出版本为:
Tool execution budget exhausted. Do not call any more tools. Use the tool results already available to provide the best possible final answer.
This tool request was skipped because it duplicates an earlier request, repeats a failed path, or would add no new evidence. Do not repeat this request. Use the other results already available, and call a different tool only if decisive information is still missing.
清理后的公开执行错误固定为 The operation was canceled.、The tool timed out. Please try again. 或 Tool execution failed. Please try again.。
源码:tool_exec.go。
内置文档生成技能
启用 python_execute 时,Aivory 暴露保留技能 document-generation。支持渐进加载的模型看到:
document-generation: MUST load this BEFORE generating any downloadable document (PDF / PPTX / DOCX / XLSX) — it contains the required recipes and self-checks.
没有 use_skill 的模型会直接收到完整固定技能。准确正文如下:
## Document-generation recipes (run inside python_execute, write to /workspace/outputs/)
**PDF (preferred):** semantic HTML (h1/h2/p/ul/table/blockquote — not styled divs) + WeasyPrint; it handles page breaks, fonts, and tables natively.
```python
from weasyprint import HTML, CSS
HTML(string=html).write_pdf("/workspace/outputs/report.pdf", stylesheets=[CSS(string="""
@page { size: A4; margin: 25mm; }
body { font-family: 'Noto Sans CJK SC','DejaVu Sans'; font-size: 11pt; line-height: 1.55; color: #1f2937; }
h1 { font-size: 22pt; } h2 { font-size: 15pt; margin: 18pt 0 6pt; } h1, h2 { color: #0f172a; font-weight: 600; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #e2e8f0; padding: 6pt 8pt; text-align: left; } th { background: #f1f5f9; font-weight: 600; }
""")])
```
**PPT (.pptx):** author each slide as a semantic-HTML string and parse it into native PPTX shapes with BeautifulSoup + python-pptx — the sandbox has NO browser, never attempt playwright/screenshot routes.
```python
from bs4 import BeautifulSoup
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
prs = Presentation(); prs.slide_width, prs.slide_height = Inches(13.33), Inches(7.5)
for html in slides_html: # one string per slide, e.g. "<h1>Title</h1><p>Subtitle</p>"
slide = prs.slides.add_slide(prs.slide_layouts[6])
tf = slide.shapes.add_textbox(Inches(0.8), Inches(0.6), Inches(11.7), Inches(6)).text_frame
tf.word_wrap = True
for el in BeautifulSoup(html, "html.parser").find_all(["h1", "h2", "p", "li", "img"]):
if el.name == "img" and el.get("src"):
slide.shapes.add_picture(el["src"], Inches(1), Inches(2.2), width=Inches(8)); continue
p = tf.add_paragraph() if tf.paragraphs[0].runs else tf.paragraphs[0]
r = p.add_run(); r.text = ("• " if el.name == "li" else "") + el.get_text()
r.font.name = "Noto Sans CJK SC"; r.font.bold = el.name in ("h1", "h2")
r.font.size = Pt({"h1": 40, "h2": 28}.get(el.name, 18))
r.font.color.rgb = RGBColor.from_string("0f172a" if r.font.bold else "1f2937")
prs.save("/workspace/outputs/deck.pptx")
```
Map <table> via slide.shapes.add_table likewise. Charts/diagrams: render them inside Python (for example, a matplotlib PNG under /workspace/outputs) and then add_picture. Every original conversation upload, including PDF/DOCX/PPTX/XLSX and images, is available without format conversion under /workspace/uploads. For a targeted edit, open that original file with the matching library, change only the requested content, and save the result under /workspace/outputs so its existing layout and formatting are retained. Prior image-generation outputs are also staged under /workspace/uploads; public images downloaded with fetch_image are available under /workspace/downloads.
**Word (.docx):** python-docx — set doc.styles['Normal'].font.name = 'Noto Sans CJK SC' (size Pt(11)), then add_heading / add_paragraph / add_table / add_picture.
**Excel (.xlsx):** openpyxl or xlsxwriter (charts, conditional formatting, frozen panes all supported).
**Self-check before presenting (no browser — never screenshots):** reopen each file structurally: os.path.getsize > 0; pypdf — page count + page-1 extract_text() shows expected content incl. CJK glyphs; python-pptx — slide count + title/bullet text present; DOCX/XLSX likewise. Always set Noto Sans CJK fonts so Chinese never renders as tofu boxes (□□□). On failure, fix and re-render (up to 3 attempts).
管理员创建同名技能时会有意覆盖内置版本;该私有替代正文不属于公开目录。
源码:docgen_skill.go。