# Support agent (/docs/sample-apps/support-agent)



A support-desk chat you can run locally: a login, a chat UI, and one LLM call per message. It's
the **steady-state** TokenPolice pattern — add the firewall, leave it in `enforce`, and every turn
is checked before the provider call. The *same* app ships in two languages so you can pick the one
you work in:

| Folder                            | Stack               | Port |
| --------------------------------- | ------------------- | ---- |
| `support_agent_langgraph_python/` | FastAPI + LangGraph | 6002 |
| `support_agent_langgraph_node/`   | Express + LangGraph | 6001 |

Every login gets a stable `session_id`, so the whole conversation shows up as **one session** in
your [dashboard](https://app.tokenpolice.ai), and each turn is a **trace** attributed to the user
id you logged in as — named `longrun_support_agent_langgraph_python` or
`longrun_support_agent_langgraph_node`.

<Cards>
  <Card title="Sample apps on GitHub" href="https://github.com/tokenpolice" description="Our GitHub org — this app’s source (Python and Node side by side) is published there as the repos go public." />
</Cards>

## What TokenPolice adds [#what-tokenpolice-adds]

One `init()` before the LLM imports, plus a workflow wrapper that names each turn and attaches the
user + session identity. Nothing changes in how the model is called.

<Tabs items="[&#x22;Python&#x22;, &#x22;Node&#x22;]">
  <Tab value="Python">
    ```python
    import token_police as tp

    tp.init(
        api_key=os.environ.get("TOKEN_POLICE_API_KEY", "tp_sk_test"),
        base_url=os.environ.get("TOKEN_POLICE_URL", "http://localhost:3001"),
        firewall="enforce",
        log_errors=True,
    )

    import agent   # imports LangChain — must come after tp.init

    # …in agent.py, each turn is wrapped so the dashboard groups it by user + session:
    @tp.workflow(name="longrun_support_agent_langgraph_python", kind="agent")
    async def run_turn(user_id, session_id, provider, mode, message, history) -> dict:
        ...

    tp.flush_sync()   # on shutdown
    ```
  </Tab>

  <Tab value="Node">
    ```typescript
    import * as tp from 'token-police';
    // @langchain/core ships dual CJS+ESM builds as distinct module records.
    // Static-import the ESM ones so the SDK can patch them before any langchain code runs.
    import * as lcChatModels from '@langchain/core/language_models/chat_models';
    import * as lcCallbacks from '@langchain/core/callbacks/manager';

    tp.init({
        apiKey: process.env.TOKEN_POLICE_API_KEY || 'tp_sk_test',
        baseUrl: process.env.TOKEN_POLICE_URL || 'http://localhost:3001',
        firewall: 'enforce',
        logErrors: true,
        instrumentModules: {
            langChain: { chatModelsModule: lcChatModels, callbackManagerModule: lcCallbacks },
        },
    });

    // Imported AFTER tp.init — agent.ts has no top-level langchain imports.
    import { runTurn } from './agent.js';

    const runSupportAgent = tp.workflow(
        { name: 'longrun_support_agent_langgraph_node', kind: 'agent' },
        async (opts: { userId: string; sessionId: string; /* … */ }) => runTurn({ /* … */ }),
    );

    await tp.shutdown();   // on SIGINT/SIGTERM
    ```
  </Tab>
</Tabs>

<Callout type="warn" title="Node needs the LangChain companion package">
  The Node app depends on **both** `token-police` and `token-police-langchain`. Without the
  companion package installed, the app still boots and still enforces — but no model usage is
  recorded at all. The two static `@langchain/core` imports above must also come **before**
  `tp.init()`. See [LangChain](/docs/integrations/langchain).
</Callout>

<Callout title="Let your coding agent wire it">
  You don't have to add those lines by hand. Point your coding agent at the TokenPolice skill and
  it does it for you — see [Start with your coding
  agent](/docs/get-started/coding-agent/overview).
</Callout>

## Run it [#run-it]

You'll need a TokenPolice API key (`tp_sk_…`) from [app.tokenpolice.ai](https://app.tokenpolice.ai)
and a key for whichever provider you pick.

<Tabs items="[&#x22;Python&#x22;, &#x22;Node&#x22;]">
  <Tab value="Python">
    ```bash
    cd support_agent_langgraph_python
    cp env.example .env       # then fill in your keys
    bash setup.sh
    source .venv/bin/activate
    python main.py            # http://localhost:6002
    ```
  </Tab>

  <Tab value="Node">
    ```bash
    cd support_agent_langgraph_node
    cp env.example .env       # then fill in your keys
    npm install
    npm start                 # http://localhost:6001
    ```
  </Tab>
</Tabs>

### Configuration [#configuration]

Everything is read from `.env` at startup:

| Variable                       | What it does                                                                                                                                                          |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TOKEN_POLICE_API_KEY`         | Your `tp_sk_…` key                                                                                                                                                    |
| `TOKEN_POLICE_URL`             | The collector. `env.example` ships pointing at a local stack (`http://localhost:3001`) — set it to `https://collect.tokenpolice.ai` to use the hosted service         |
| `PROVIDER`                     | `openai` \| `anthropic` \| `gemini` (default `openai`). Fill in the matching `*_API_KEY` and, optionally, `*_MODEL_NAME`                                              |
| `MODE`                         | `prebuilt` \| `custom` (default `prebuilt`) — whether the app uses LangGraph's prebuilt ReAct agent or a hand-built graph. TokenPolice behaves identically either way |
| `SESSION_IDLE_TIMEOUT_SECONDS` | Default `300`. How long a login's session survives with no activity before the app ends it                                                                            |
| `APP_PORT`                     | Default `6002` (Python) / `6001` (Node)                                                                                                                               |

## What you'll see [#what-youll-see]

A single-page support desk: a header showing the active provider and model, a login field for a
user id (e.g. `usr_pro_001`), and a chat transcript. Send a few messages, then open your
[dashboard](https://app.tokenpolice.ai): the whole conversation is grouped under **one session**,
each turn is its own **trace** attributed to that user id, and cost and token usage accrue per
turn — all checked before the provider call, with the firewall in `enforce`.

Because the app runs in enforce, any rule you promote to Enforce takes effect on the very next
turn. Try a per-session cap and watch it bite; the sibling RAG app below does exactly that, and
handles the block gracefully.

## Related [#related]

<Cards>
  <Card title="Dry-run vs enforce" href="/docs/concepts/dry-run-vs-enforce" description="The two dials this app leaves on enforce." />

  <Card title="Sessions and traces" href="/docs/concepts/sessions-and-traces" description="How one login becomes one dashboard session." />

  <Card title="See the firewall block" href="/docs/sample-apps/budget-aware-rag" description="The sibling app that drives a session past its budget." />
</Cards>
