datasette/datasette-agent
GitHub: datasette/datasette-agent
Datasette-agent 是一个由大语言模型驱动的 Datasette 插件,提供基于聊天的数据库交互、SQL 生成与保存、后台数据探索及可扩展的工具注册机制。
Stars: 107 | Forks: 6
# datasette-agent
[](https://pypi.org/project/datasette-agent/)
[](https://github.com/datasette/datasette-agent/releases)
[](https://github.com/datasette/datasette-agent/actions/workflows/test.yml)
[](https://github.com/datasette/datasette-agent/blob/main/LICENSE)
一个由 LLM 驱动的 Datasette 代理助手
请参阅 [Datasette Agent:一个可扩展的 Datasette AI 助手](https://datasette.io/blog/2026/datasette-agent/) 了解有关此项目的更多信息,包括在你自己的机器上运行它的提示。
## 安装
在与 Datasette 相同的环境中安装此插件。
```
datasette install datasette-agent
```
## 用法
访问 `/-/agent` 以开始与聊天助手的对话。
该代理使用 [datasette-llm](https://github.com/datasette/datasette-llm) 来调用语言模型。在访问 `/-/agent` 之前为其配置默认模型,例如在 `datasette.yml` 中:
```
plugins:
datasette-llm:
default_model: gpt-5.4-mini
```
出现在数据库和表格操作菜单中的“使用 AI 代理探索”条目会启动一个后台代理,它会探索选定的数据库或表格并编写报告。报告位于 `/-/agent/explore/` 下。
访问 `/-/agent/background` 可直接启动后台代理。每个代理都会被分配一个目标,并在无需进一步输入的情况下运行以实现该目标。列表中包含一个停止按钮,用于取消仍在运行的代理。
### 保存查询
该代理具有一个内置的 `save_query` 工具,可将它编写的 SQL 保存为 [Datasette 存储查询](https://docs.datasette.io/en/latest/sql_queries.html)。查询可以是只读或写入 SQL - Datasette 会对其进行分析以决定类型,并且命名的 `:parameters` 将成为保存查询页面上的表单字段。
保存始终需要人工批准:代理会向你展示完整的 SQL 以及建议的名称、数据库和可见性,在你点击“是”之前不会存储任何内容。验证和持久化通过 Datasette 自身的 `/-/queries/analyze` 和 `/-/queries/store` 端点作为请求的执行者运行,因此执行者需要对目标数据库具有 `execute-sql` 和 `store-query` 权限(对于写入查询,还需要相关的行权限)——这与查询创建 Web UI 的规则相同。保存的查询默认为私有。
### 执行写入 SQL
代理还具有一个内置的 `execute_write_sql` 工具,可以针对可变数据库运行一个或多个有序的写入 SQL 语句。它会首先分析每条语句,并在运行任何内容之前在聊天中请求用户明确批准。
批准提示会显示 SQL、参数、所需权限和破坏性操作警告。执行通过 Datasette 自身的 `/-/execute-write` 端点作为请求的执行者运行,因此执行者需要对目标数据库具有 `execute-write-sql` 权限,以及执行操作所需的 Datasette 写入权限。语句按顺序运行;如果其中一条失败,则跳过后续语句,且不会回滚之前已成功的语句。请使用 `sql_query` 进行只读 SQL 操作。
### 权限
此插件注册了三个独立的权限:
- `datasette-agent` —— 在 `/-/agent` 下使用聊天助手所必需。
- `datasette-agent-explore` —— 在数据库/表格操作菜单中查看“使用 AI 代理探索”条目以及使用 `/-/agent/explore/` 下的探索器路由所必需。
- `datasette-agent-background` —— 在聊天中使用 `spawn_background_agent` 和 `check_background_agent` 工具,以及访问 `/-/agent/background` 页面和 `/-/agent/api/background/*` 端点所必需。后台代理端点需要同时具备 `datasette-agent` 和 `datasette-agent-background` 权限。
这三个权限是独立的:执行者可以持有其中的任意组合。`--root` 用户拥有全部权限。
## 从插件注册其他工具
其他 Datasette 插件可以使用 `register_agent_tools` 插件钩子为代理注册其他工具。
### 定义工具
创建一个实现 `register_agent_tools` 钩子的 Datasette 插件,返回一个 `AgentTool` 实例列表:
```
from datasette import hookimpl
from datasette_agent.tools import AgentTool
@hookimpl
def register_agent_tools(datasette):
return [
AgentTool(
name="my_tool",
description="Description of what this tool does, used by the LLM to decide when to call it.",
input_schema={
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The query to run",
},
"style": {
"type": "string",
"enum": ["brief", "detailed"],
"description": "Output style",
},
},
"required": ["query"],
},
fn=my_tool_handler,
# Optional: name a Datasette permission action that gates this tool.
# required_permission="myplugin-write",
),
]
```
### 使用权限控制工具
`AgentTool` 接受一个可选的 `required_permission: str | None` 字段。设置后,代理框架在将工具列表发送给 LLM 之前,会为当前执行者调用 `datasette.allowed(action=required_permission, actor=actor)`。如果执行者缺少该权限,该工具将从列表中过滤掉——模型永远不会看到它,也无法调用它。你的 `fn` 不需要处理任何运行时的“权限拒绝”分支。
你的插件负责通过 Datasette 的 `register_actions` 插件钩子注册操作:
```
from datasette import hookimpl
from datasette.permissions import Action
from datasette_agent.tools import AgentTool
@hookimpl
def register_actions():
return [
Action(
name="myplugin-write",
description="Allow my plugin's write tools",
),
]
@hookimpl
def register_agent_tools(datasette):
return [
AgentTool(
name="my_write_tool",
description="Writes things",
input_schema={"type": "object", "properties": {}},
fn=my_write_handler,
required_permission="myplugin-write",
),
]
```
有关工作示例,请参阅此插件自身的 `spawn_background_agent` 和 `check_background_agent` 工具,它们使用 `required_permission="datasette-agent-background"`。
### 工具处理函数
每个工具的 `fn` 必须是一个异步函数,它接受 `datasette` 和 `actor` 作为关键字参数,以及 `input_schema` 中定义的任何参数。它必须返回一个 JSON 字符串:
```
import json
async def my_tool_handler(datasette, actor, query, style=None):
# Do work here...
return json.dumps({
"result": "Tool output that the LLM will see",
})
```
要在聊天 UI 中内联呈现丰富的 HTML,请在返回的 JSON 中包含一个 `_html` 键。在将工具结果发送给 LLM 之前,任何名称以 `_` 开头的顶级键都会被删除,因此 HTML 会显示给用户,但不会传递回模型:
```
return json.dumps({
"_html": '',
"summary": "Widget rendered successfully",
})
```
### 从工具向用户提问
工具可以在执行过程中暂停并向人类用户提问。在你的处理程序上声明一个 `context` 参数并调用 `await context.ask_user(...)`:
```
import json
async def edit_files(datasette, actor, context, path):
ok = await context.ask_user(
"Is it OK to edit files in {}?".format(path)
)
if not ok:
return json.dumps({"cancelled": True})
mode = await context.ask_user(
"How should I apply this?", options=["dry-run", "apply"]
)
note = await context.ask_user("Any notes?", free_text=True)
# ... do the work ...
return json.dumps({"edited": path, "mode": mode, "note": note})
```
支持三种类型的问题:
- `await context.ask_user("Approve?")` - 是/否,返回一个 `bool`
- `await context.ask_user("Which?", options=["a", "b"])` - 多项选择,返回所选的 `str`
- `await context.ask_user("Describe it", free_text=True)` - 自由格式,返回一个 `str`
传入 `html=` 以在问题上方显示受信任的 HTML - 使用此选项可以向用户准确展示他们正在批准的内容,例如 `
"""
```
你的 `html` 中任何位置的 `__DATASETTE_TASK_ID__` 字面量字符串都会在呈现时替换为真实的任务 ID。(经典非模块脚本也可以通过 `document.currentScript.closest("[data-task-id]").dataset.taskId` 在结构上找到它——模块脚本应使用占位符,因为 `document.currentScript` 在其中为 `null`。)
API:
- `claimTask(taskId)` - 原子性地声明任务,并**每个任务仅一次**解析为 `{ok: true, payload, timeoutMs}`。任何后续或并发的声明——重复的标签页、重新呈现对话历史的重新加载页面——都会解析为 `{ok: false, state}`,调用者应停止操作。正是这个声明门控使得带有脚本的 HTML 能够安全地重新呈现:无论标记出现多少次,执行最多只发生一次。
- `completeTask(taskId, envelope)` - 回传 `{ok, result?, error?}`。首次写入有效。暂停的回合立即恢复,其事件流会在同一连接上流入记录。
- `cancelTask(taskId)` - 相当于用户点击“跳过”。
一旦任务离开挂起状态,其 HTML 就会被拆除并且永远不会再次呈现——重新加载对话会显示一个无操作的单行记录。保持任务 HTML 独立:呈现、执行、完成、拆除。需要进行多轮浏览器工作的工具应该按顺序发出多个 `browser_task()` 调用。
#### 能力检测和测试
`context.supports_browser_tasks` 在 Web 聊天中为 `True`,在后台代理和 CLI 聊天中为 `False`,在这些情况下调用 `browser_task()` 会引发 `BrowserTasksNotSupported` 异常——像 `QuestionsNotSupported` 一样作为工具错误呈现给模型。如果你的工具有无浏览器的后备方案,请先检查此标志。
对于测试,请在构造 `ToolContext` 时传入 `browser_task_callback`(或通过 `make_llm_tools()`):它接收 `{tool_name, html, payload, label, timeout_ms}` 并直接同步或作为协程返回信封,而无需接触数据库或任何浏览器。以这种方式返回的信封会像真实完成情况一样被规范化为 `outcome: "completed"`,因此你的工具无法区分执行器。
## 从工具呈现自定义 HTML
工具插件可以通过返回带有 `_html` 键的 JSON 对象,在聊天 UI 中呈现丰富的 HTML。HTML 直接在对话中呈现。其余键将作为工具结果返回给 LLM,任何名称以 `_` 开头的键都会先被删除。
工具实现示例:
```
import json
async def _render_widget(datasette, actor, database, sql):
html = (
'\n'
'` 标签内查询的完整 SQL。自行转义任何插值内容(例如使用 `html.escape()`);该字符串将按原样呈现在聊天 UI 中。
传入 `text=` 可以为终端上下文(例如 `datasette agent chat`)提供纯文本版本。如果提供了 `text=` 而没有 `html=`,Web 聊天也会显示该文本,并在 `` 块内进行 HTML 转义。CLI 在可用时显示 `text=`;如果仅提供了 `html=`,它将直接打印该 HTML。
当 `ask_user()` 尚未有答案时,它会暂停代理回合:问题在聊天 UI 中呈现为表单,并持久化到内部数据库中,因此即使服务器重启它也能保留——重新加载对话页面时表单会重新呈现。一旦用户回答,工具函数将**从头开始重新执行**;先前回答的问题会立即返回其存储的答案,并且执行会越过 `ask_user()` 调用继续进行。由于这种重播模型,你应该在执行副作用*之前*调用 `ask_user()`。
`context` 对象还公开了 `context.actor`、`context.conversation_id`、`context.tool_name`、`context.arguments` 和 `context.tool_call_id`。
在没有人关注的场景下——例如后台代理,或者终端输入不可用的 CLI 聊天——`ask_user()` 会引发 `QuestionsNotSupported` 异常,该异常作为工具错误呈现给模型,以便它可以在没有输入的情况下继续执行。仅声明 `datasette` 和 `actor` 的工具不受此影响。
### 通过工具在用户的浏览器中运行工作
某些工具需要代码在用户的浏览器中运行——截取呈现页面的屏幕截图、针对活动的 DOM 执行 JavaScript、测量布局。**浏览器任务**是实现此目的的原语:工具向用户的浏览器交付一个工作单元,代理回合暂停,当浏览器回传结果时工具接收该结果——即使这需要几分钟、页面重新加载或服务器重启。
在你的处理程序上声明一个 `context` 参数并调用 `await context.browser_task(...)`:
```
import json
async def measure_page(datasette, actor, context, url):
outcome = await context.browser_task(
html=BROWSER_HARNESS_HTML,
payload={"url": url, "token": make_capability_token(url)},
label="Measuring {} in your browser".format(url),
timeout_ms=30_000,
)
if not outcome["ok"]:
return json.dumps({"error": outcome["error"], "outcome": outcome["outcome"]})
return json.dumps({"measurements": outcome["result"]})
```
四个参数:
- `html` - **受信任的、服务器端编写的 HTML**,呈现到聊天页面中。与问题的 `html=` 不同,它的 `