Metadata-Version: 2.5
Name: pagesystems
Version: 0.4.0
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; 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
```

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, message: str):
    # The session stores the conversation's history, so the same id continues it.
    session = SQLiteSession(conversation_id, "conversations.db")
    result = await Runner.run(make_agent(conversation_id), message, 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.message is "[Page Systems] The person submitted ...: {answer}. Continue the conversation."
        background.add_task(take_turn, event.agent_ref["sessionId"], event.message)

    status, body = await handle_callback_async(await request.body(), 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`, `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).

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.

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`.

## 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.
