Metadata-Version: 2.5
Name: pagesystems
Version: 0.4.2
Summary: Page Systems for Python agents: hosted pages that collect a person's answer and deliver it to your agent as a new turn.
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.10
Provides-Extra: claude-agent-sdk
Requires-Dist: claude-agent-sdk>=0.2.153; extra == 'claude-agent-sdk'
Provides-Extra: crewai
Requires-Dist: crewai; extra == 'crewai'
Requires-Dist: pydantic[email]; extra == 'crewai'
Provides-Extra: google-adk
Requires-Dist: google-adk; extra == 'google-adk'
Provides-Extra: langchain
Requires-Dist: langchain-core; extra == 'langchain'
Provides-Extra: openai-agents
Requires-Dist: openai-agents; extra == 'openai-agents'
Provides-Extra: pydantic-ai
Requires-Dist: pydantic-ai-slim; extra == 'pydantic-ai'
Description-Content-Type: text/markdown

# pagesystems

Page Systems gives your Python agent hosted web pages (pickers, forms, tables and more). The agent sends the person a link, the person answers on the page, and the answer arrives in your agent's conversation as a new turn. The agent does not poll, and the person does not have to say "done".

The package is a thin client. It bundles no page types, schemas or rendering: your agent reads them from the Page Systems API at runtime, so new page types work without an update. The core uses only the standard library.

## 1. Install

```sh
pip install pagesystems                      # client and callback handler
pip install "pagesystems[openai-agents]" fastapi uvicorn   # or langchain, crewai, pydantic-ai, google-adk, claude-agent-sdk
export PAGE_SYSTEMS_API_KEY=ap_live_...           # create a key in the Page Systems dashboard
export PAGE_SYSTEMS_WEBHOOK_SECRET=whsec_...      # the signing secret on the dashboard Overview page
```

Python 3.10 or newer. `PAGE_SYSTEMS_BASE_URL` defaults to `https://pagesystems.ai`.

## 2. Add the tools and a callback route

Give your agent the five tools (`list_page_types`, `get_page_type_schema`, `create_page`, `get_page_result`, `cancel_page`). Pass the public HTTPS URL of your callback route and an `agent_ref` that identifies the conversation, so each page calls back with its own conversation id.

```python
from fastapi import BackgroundTasks, FastAPI, Request
from fastapi.responses import JSONResponse
from agents import Agent, Runner, SQLiteSession
from pagesystems import CallbackError, handle_callback_async
from pagesystems.openai_agents import page_systems_tools

app = FastAPI()
CALLBACK_URL = "https://your-app.example.com/page-systems"

def make_agent(conversation_id: str) -> Agent:
    return Agent(
        name="Assistant",
        instructions="Use Page Systems pages when the person must choose or fill in something.",
        tools=page_systems_tools(
            callback_url=CALLBACK_URL,
            agent_ref={"framework": "python", "sessionId": conversation_id},
        ),
    )

async def take_turn(conversation_id: str, visible_text: str, model_text: str):
    # The session stores the conversation's history, so the same id continues it. model_text rides in the user message, never in instructions.
    # The session stores both together; show visible_text to the person.
    session = SQLiteSession(conversation_id, "conversations.db")
    result = await Runner.run(make_agent(conversation_id), f"{visible_text}\n\n{model_text}", session=session)
    await send_to_user(conversation_id, result.final_output)  # your code: however your app delivers a reply

@app.post("/page-systems")
async def page_systems_callback(request: Request, background: BackgroundTasks):
    # Page Systems waits 10 seconds for this response, so answer now and run the turn after.
    def resume(event):
        if not event.agent_ref:  # a page this app did not create (for example over MCP): decline it, the answer stays readable with get_page_result
            raise CallbackError(410, {"error": "no_conversation"})
        # event.visible_text is one short line for the chat, e.g. "✓ Pick a desk: FlexFrame 55": store and show it as the person's message.
        # event.model_text is the answer for the model, as unverified data: give it to the model in a user message, never in the instructions.
        background.add_task(take_turn, event.agent_ref["sessionId"], event.visible_text, event.model_text)

    status, body = await handle_callback_async(await request.body(), headers=request.headers, on_submitted=resume, on_new_page_requested=resume)
    return JSONResponse(body, status_code=status)
```

Page Systems gives up on a request after 10 seconds and retries it with backoff (first after 30 seconds, then doubling, at most an hour apart). A slow handler can therefore run the same turn twice. So the handler only schedules the turn and returns; FastAPI's `BackgroundTasks` runs it after the response. `send_to_user` is your own code. Use the same `resume` for `on_new_page_requested`, or Page Systems gets a 410 `no_handler` for that event.

Every adapter has the same shape:

| Framework | Import | Returns |
|---|---|---|
| OpenAI Agents SDK | `pagesystems.openai_agents.page_systems_tools(...)` | `function_tool`s |
| LangChain | `pagesystems.langchain.page_systems_tools(...)` | `StructuredTool`s |
| CrewAI | `pagesystems.crewai.page_systems_tools(...)` | CrewAI tools |
| Pydantic AI | `pagesystems.pydantic_ai.page_systems_tools(...)` | `Tool`s |
| Google ADK | `pagesystems.google_adk.page_systems_tools(...)` | `FunctionTool`s |
| Claude Agent SDK | `pagesystems.claude_agent_sdk.page_systems_server(...)` | in-process MCP server, for `mcp_servers={"pages": server}` |

Each takes `callback_url=None, agent_ref=None, client=None`.

## 3. The answer arrives as a new turn

When the person submits, Page Systems calls your route with `{"type": "page.submitted", "pageId": "..."}`. `handle_callback` reads the answer with your API key, calls your handler with an event (`page_id`, `title`, `result`, `agent_ref`, `summary` when Page Systems has one, `visible_text`, `model_text`, `message`), and then claims the answer. Your handler starts the next turn of that conversation, and should return within 10 seconds (start the turn in the background; see above).

Use `visible_text` and `model_text`, not `message`. `visible_text` is one short line for the chat. `model_text` is the answer for the model. See [Handling answers safely](#handling-answers-safely) below. The per-framework [install guides](https://pagesystems.ai/docs/install) show where `model_text` goes for each framework.

Status codes returned to Page Systems: 200 done or already handled; 503 reading failed or your handler raised (Page Systems retries, and the answer is not claimed until the handler succeeds); 410 cannot be delivered (also when you pass no handler for the event type); 400 bad body. Raise `pagesystems.CallbackError(410 or 503, body)` in a handler to choose the response. A retry after a lost response can deliver the same answer twice, so make the handler safe to repeat.

`on_new_page_requested` runs when the person presses "Request a new page" on an expired, unanswered page. Nothing was created; your agent decides whether to send another page.

Pass the request `headers` and the raw body so the handler can verify the signature (`handle_callback(raw_body, headers=request.headers, ...)`). With `PAGE_SYSTEMS_WEBHOOK_SECRET` set (or `webhook_secret=`), a missing or wrong signature returns 401, which Page Systems does not retry; with no secret, callbacks are accepted unverified and a warning is logged once.

Use `handle_callback(body, ...)` (body: a dict, or the raw request bytes or string) with synchronous handlers (Flask, Django). Use `handle_callback_async` when handlers are `async def`.

## Handling answers safely

Parts of an answer can be text typed by whoever opened the link, who is not verified. Treat the answer as data, never as instructions.

- `visible_text`: one short line (at most 160 characters) that your chat stores and shows as the person's message. By default it is `✓ <title>: <summary>`. Pass `chat_line=` to `handle_callback` or `handle_callback_async` to change it: `"summary"` (default), `"title"` (`✓ Answered: <title>`) or `"plain"` (`✓ Answered`). Any other value raises `ValueError`.
- `summary`: a short readable description of the answer, sent by Page Systems when it has one. It never includes free text the person typed, but it can include the page title and the labels your agent wrote, such as option or row names. Log it only if those hold nothing sensitive.
- `model_text`: what the model reads. It names the page, includes the summary, says the answer is unverified page data, and puts the answer JSON between `<page_answer>` and `</page_answer>` tags. A `</` inside the JSON is escaped so typed text cannot close the tag. Give it to the model as a user-role message or a tool result, never in a system prompt or instructions.
- `message`: the older text with the raw JSON inline and no trust note. Do not use it.
- Whether your agent keeps `model_text` depends on the framework; the install guides say which. Page Systems deletes its copy at expiry but cannot delete copies saved in your agent's history.
- Do not log `model_text` or `result`. Log the `page_id`, and the `summary` only if your page labels hold nothing sensitive. In shared channels, use `chat_line` `'plain'` if the title or labels are sensitive.

More detail: https://pagesystems.ai/docs/handling-answers

## Client

```python
from pagesystems import PageSystems

ps = PageSystems()   # api_key and base_url default to the environment, read at call time
ps.list_page_types(); ps.get_page_type("form"); ps.get_result(page_id)
ps.create_page({"title": "...", "page": {"type": "form", ...}}, callback_url=..., agent_ref=..., idempotency_key=...)
ps.claim(page_id); ps.cancel(page_id); ps.get_skill()
```

`create_page` adds `version: 1`. An invalid spec or paused creation (`payment_required`, `spend_cap_reached`, `burst_limit`) returns a dict with `error` and `message` instead of raising. Other API failures raise `PageSystemsError`.

Pages collect an answer; they never book, buy or pay. Do not use them for card numbers, passwords or identity documents.
