Skip to main content
3 toolsOAuth bearer
gdocs_example.py
Read a document, create a blank one, and edit one. Three endpoints is the whole Docs API, and the third carries everything: a single update call taking a thirty-three member union.

Authenticating

This pack takes a Google OAuth bearer token, and configure() is optional when $GOOGLE_ACCESS_TOKEN is set. Which credential provider you hand it depends on whose account the calls run as.
Refer to Google’s provider page for the “GOOGLE” constant the snippets below name, the scopes these 3 tools ask for, and this server’s refresh behaviour.

A token you hold

For a script, or a notebook. EnvTokenProvider re-reads the variable on every call, so a token rotated beside the process is picked up without a restart; StaticTokenProvider takes one you already hold as a string. Neither renews anything, so the calls stop when the token expires.
gdocs_script.py

One account, refreshed

For an agent or a server acting as you. OAuth2Client turns a client registration and a stored refresh token into an access token, and renews it before it lapses.
gdocs_agent.py
No refresh token yet? Your own account is the one-time consent flow that hands you one.

Many end users

For a product whose users each connect their own account. SubjectProvider builds one credential per user through a factory you write, and use_subject names the user a call acts for. Your users’ accounts is the consent route inside your app; serving many users is the per-subject cache and its eviction.
gdocs_server.py
documents.readonly is enough for documents_get alone, but the pack declares the write scope because two of its three tools need it.

The client

oauth_tool_factory is the whole client: a thin wrapper over httpx that attaches your token and these endpoint constants to each request. google-api-python-client and google-auth do not enter your dependency tree.
credentials is whichever of the three you built in Authenticating. A pack takes it through configure(); a client you build takes the same object as credential_provider, and has no configure() of its own.

Tools

Each is a Tool, called with ainvoke as in the snippet above. The name links to its parameters, its response and what it costs.

batchUpdate is a union of thirty-three edits

Every change you can make to a document — insert text, style a range, merge table cells, pin header rows — is one member of a Request oneof, and a batch is a list of them. All thirty-three are modelled, with the oneof declared as a model_validator on Request and on the eleven edits that carry a oneof of their own.
gdocs_insert_text.py
Those validators are the reason this pack matters. The LLM view of a schema is derived rather than reused, and it now carries the source schema’s validators and its extra="forbid" across — so a request setting two members of the union is rejected against the model’s input, not only against the wire. Before that, the one input that most needed checking was the one input never checked. See tests/test_schema.py, and conformance for the property that keeps it true.

Gotchas

batchUpdate applies its requests in order against a document that changes under them. Insert text at index 1 and every later index moves; the API will not warn you. When inserting at several positions, order the requests from the highest index down. This is the single most common way a correct-looking batch produces a scrambled document, and it is documented on the requests field for the model’s benefit as well as yours.
Its JSON schema is roughly 31KB — about 8,000 tokens — before the model has read a word of the actual task. That is what complete coverage of a union this wide costs. Mode markers plus a tool-level mode narrow the union to the edits one agent needs without giving up the schema; see the mode system.
A Document is a deeply nested structural document, and Charter models requests. documents_get returns Google’s payload as it arrived, with no response handler. For long documents, write one that extracts the text — the Gmail pack’s extract_thread_text is the shape to copy.
A document may have several tabs, and body reflects only the first. Pass include_tabs_content=true to documents_get for the whole thing. The argument is snake_case in the schema and reaches Google as includeTabsContent, which is what the camel query casing on this factory is for.
The document is created empty whatever else you put in the body. Adding content is a second call to documents_batch_update with the returned documentId.
  • Google — consent screen, scopes, refresh
  • The mode system — narrowing a union the model does not need
  • Conformance — the check that keeps validators from being dropped