Tools for models¶
These turn results and errors into plain data for the result of a model's tool call, in the same shapes and value formats whatever the call. They work with any tool-calling framework, as json.dumps() always accepts what they return. Table.operation() makes the tools themselves.
Each example makes its own client from the environment, with bqtasks123 standing for the ID of the Job Tasks table. Job Tasks has Title, Status, Hours, Due Date, Code, which is unique, and Attachment. It starts with three records, and each example starts from those again. A live test runs every example with both clients and checks its output.
Results¶
For the full picture, see Using pyquickbase from an AI agent.
Return a result to a model¶
Describe a result for a model, as the result of its tool call.
page = await tasks.query_page(limit=50)
return tool_result(page, compact=True)
The result is {"ok": true, ...}, which json.dumps() always accepts, with a key for what it holds:
| Value | Keys |
|---|---|
Page |
records, or columns and rows with compact, and page |
| A list of records or dictionaries | The same. page.total is null, as it isn't known |
Record or None |
record, which is null for None |
WriteResult |
created_ids, updated_ids, and unchanged_ids |
A list of SummaryRow |
groups, each with group, count, and its totals, and page |
A summarize operation's result |
As for a list of SummaryRow, sorted and paged as asked |
UpdatePreview |
changes, unchanged_ids, and missing_ids |
A preview_update operation's result |
As for an UpdatePreview, with the approval keys |
A preview_delete operation's result |
As for a Page, with the approval keys |
SchemaPlan |
changes and has_changes, and the approval keys |
SchemaChangeResult |
applied and already_done, and failed and not_attempted too |
A list of TableSchema |
tables, each with fields and relationships |
FormulaResult |
value and type |
A count operation's result |
count |
A delete operation's result |
deleted_count |
| Text, a number, a date, or a bool | value |
TableInfo, Field, or ReportInfo |
table, field, or report, without raw |
| A list of one of those | tables, fields, or reports |
FileDownload |
file, with file_name, content_type, size, and text or binary |
A file's text is its contents if they're UTF-8 text, and otherwise it's null and binary is true, as a model can't read bytes.
page holds returned, total, skip, next_skip, and has_more, so a model can tell whether it has every record, and ask for the next page. An empty list counts as no records. For a summary, page counts groups. A query operation that left out some of the table's default fields, because it has more than its max_default_fields, adds fields_left_out with how many and a hint.
A preview's approval keys are blockers, what would stop the change, warnings, what else it would do, and, if nothing stops it, the confirmation the change needs and the UTC time it expires_at. A preview_delete operation's result has a blocker when more records match than the operation's max_records, as a delete with the same filter would be refused. Give both operations the same max_records for this to be right.
Values are written the same way whatever their field: a date as YYYY-MM-DD, a timestamp in UTC as 2026-03-04T05:06:07Z, a Decimal as a number when that's exact and as text otherwise, and every kind of empty value, such as "" or an empty file, as null. Records are keyed by label.
Parameters
valueAny, required
The result to describe.compactbool, defaultFalse
Send each label once, ascolumns, with a list of values for each record inrows, rather than repeating every label on every record. A file value becomes the latest file's name. Use it when a tool returns many records, to save tokens.max_recordsint | None, optional
The most records to include. Any more are left out, andpage.has_moreis true, so a model knows it doesn't have them all.
2 more: max_text_chars, max_chars
max_text_charsint | None, optional
The most characters to include of each text value. A longer one is cut and ends by saying how much was left out, andtext_truncatedis true.max_charsint | None, optional
About the most characters the result may take as compact JSON, so that one call can't fill a model's context. Records and groups are included from the first until the next wouldn't fit, thenpage.has_moreis true andpage.next_skipsays where to carry on, so paging still reaches every one. At least one is always included: if it's too large on its own, its text is cut shorter until it fits, down to 50 characters a value. Other keys, such as a file's text, aren't limited by it.
Returns dict[str, Any]
The result, as plain data.
Raises
TypeError: The value isn't one of these, such as anAppInfo, or holds a value of another type.ValueError:max_records,max_text_chars, ormax_charsisn't a positive whole number.
tool_result(
value: Any,
*,
compact: bool = False,
max_records: int | None = None,
max_text_chars: int | None = None,
max_chars: int | None = None,
) -> dict[str, Any]
Example
import json
from pyquickbase import Quickbase, Policy, tool_result
with Quickbase.from_env(
policy=Policy.read_only()
) as qb:
tasks = qb.app("bq1234567").table("bqtasks123")
page = tasks.query_page(
select=["Title", "Due Date"], limit=2
)
result = tool_result(page, compact=True)
print(json.dumps(result, indent=2))
import json
from pyquickbase import AsyncQuickbase, Policy, tool_result
async with AsyncQuickbase.from_env(
policy=Policy.read_only()
) as qb:
tasks = qb.app("bq1234567").table("bqtasks123")
page = await tasks.query_page(
select=["Title", "Due Date"], limit=2
)
result = tool_result(page, compact=True)
print(json.dumps(result, indent=2))
Output
{
"ok": true,
"columns": [
"Record ID#",
"Title",
"Due Date"
],
"rows": [
[
1,
"Pour slab",
"2027-01-15"
],
[
2,
"Frame walls",
"2027-02-01"
]
],
"page": {
"returned": 2,
"total": 3,
"skip": 0,
"next_skip": 2,
"has_more": true
}
}
Errors¶
For the full picture, see Using pyquickbase from an AI agent.
Return an error to a model¶
Describe an exception for a model, as the result of its tool call.
try:
page = await tasks.query_page(
where=where, limit=50
)
return tool_result(page)
except QuickbaseError as error:
return tool_error(error)
The result is {"ok": false, "error": {...}}, which json.dumps() always accepts. error holds code, message, hint, retryable, and data_changed, and more for some codes:
codeis one ofToolErrorCode, which doesn't change between 1.x versions, unlike the wording ofmessage.hintsays what the model should do next, in fixed words for each code. A failed schema change has its own, as it has no failed rows, and so does apartial_writewhosedata_changedisunknown, as its failed rows may have been written.retryableis true when the same call can succeed later, such as after a rate limit or a timeout.retry_afteris added when Quickbase said how long to wait.data_changedisnoif nothing was changed,partlyif part of a write was, andunknownif a write may have been applied, such as after a timeout. A model should check the data before repeating a call whosedata_changedisn'tno.- A
WriteErroraddswritten, with the IDs that were created, updated, and left unchanged, andfailed_rows, with each row'sindex,reason,field_id, and Quickbase'smessages. - An
APIErroraddsstatus, the HTTP status. - A
TooManyRecordsErroraddsmatched, how many records match, andlimit, the most the summary may read.
An exception from outside the SDK gets the code internal and a message that says nothing about it, so a bug's details and any values in it don't reach the model. Log it yourself.
Parameters
errorBaseException, required
The exception, usually aQuickbaseError.
Returns dict[str, Any]
The error, as plain data.
tool_error(error: BaseException) -> dict[str, Any]
Example
from pyquickbase import (
Quickbase,
Policy,
QuickbaseError,
tool_error,
)
with Quickbase.from_env(
policy=Policy.read_only()
) as qb:
tasks = qb.app("bq1234567").table("bqtasks123")
try:
tasks.insert([{"Title": "Paint walls"}])
except QuickbaseError as error:
found = tool_error(error)["error"]
print(found["code"], found["data_changed"])
print(found["hint"])
from pyquickbase import (
AsyncQuickbase,
Policy,
QuickbaseError,
tool_error,
)
async with AsyncQuickbase.from_env(
policy=Policy.read_only()
) as qb:
tasks = qb.app("bq1234567").table("bqtasks123")
try:
await tasks.insert([{"Title": "Paint walls"}])
except QuickbaseError as error:
found = tool_error(error)["error"]
print(found["code"], found["data_changed"])
print(found["hint"])
Output
write_refused no
This tool may not make that change. Don't try it another way. Tell the user.
ToolErrorCode¶
The code in what tool_error returns. Codes don't change within 1.x, so a tool can act on them.
ToolErrorCode = Literal[
"misconfigured",
"needs_confirmation",
"invalid_arguments",
"too_many_records",
"invalid_value",
"read_refused",
"write_refused",
"unknown_table",
"unknown_field",
"unknown_report",
"unsupported_report",
"schema_mismatch",
"partial_write",
"write_failed",
"write_permission_denied",
"authentication",
"permission_denied",
"not_found",
"bad_request",
"rate_limited",
"server_error",
"api_error",
"timeout",
"network_error",
"invalid_response",
"quickbase_error",
"internal",
]
Operations¶
For the full picture, see Using pyquickbase from an AI agent.
OperationName¶
What Table.operation() can offer a model, each running the table or report method of the same name.
OperationName = Literal[
"query",
"count",
"summarize",
"insert",
"update",
"delete",
"preview_update",
"preview_delete",
"run_report",
"run_summary_report",
"run_formula",
"download_file",
"delete_file_version",
]
SchemaOperationName¶
What App.operation() can offer a model.
SchemaOperationName = Literal[
"describe_schema",
"preview_schema_changes",
"apply_schema_changes",
]