Support agent
A support-desk chat app in Python and Node that runs TokenPolice in enforce mode, threading each login into one dashboard session.
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, 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.
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.
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 shutdownimport * 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/SIGTERMNode 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.
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.
Run it
You'll need a TokenPolice API key (tp_sk_…) from app.tokenpolice.ai
and a key for whichever provider you pick.
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:6002cd support_agent_langgraph_node
cp env.example .env # then fill in your keys
npm install
npm start # http://localhost:6001Configuration
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
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: 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.

