| name | autoplay-core |
| description | Sets up the Autoplay SDK for real-time event streaming into any chatbot or AI agent. Covers install, credentials, stream wiring, LLM guardrails, and the two most critical concepts — session scoping and conversation scoping — that must be correct regardless of which chatbot or activity source is used. Use when starting any Autoplay integration, when the user mentions autoplay-sdk, AsyncConnectorClient, ActionsPayload, or asks how to wire real-time user events into a chatbot. |
| disable-model-invocation | true |
Autoplay Core
Install
pip install autoplay-sdk
Upgrading an existing integration? Install the migration helper skill and ask your agent to rewrite deprecated imports:
autoplay-install-skills --migrate
Credentials come from the Autoplay dashboard after running onboard_product:
CONNECTOR_URL — stream URL: https://your-connector.onrender.com/stream/YOUR_PRODUCT_ID
API_TOKEN — Bearer token (Unkey key)
⚠️ Critical: Session scoping & conversation scoping
This is the most important concept. Get this wrong and context will be empty, mixed between users, or silently dropped.
Recommended path — AutoplayChatbotManager
AutoplayChatbotManager handles all session lifecycle automatically. Use it unless you need custom Redis-backed stores or non-standard delivery logic.
from autoplay_sdk import AutoplayChatbotManager, BaseChatbotWriter
class MyWriter(BaseChatbotWriter):
async def _post_note(self, conversation_id: str, text: str) -> None:
...
manager = AutoplayChatbotManager(writer=MyWriter())
async def on_actions(payload):
await manager.on_actions(payload)
async def on_webhook(data):
session_id = data["custom_attributes"]["session_id"]
conversation_id = data["id"]
await manager.on_chatbot_event(session_id, conversation_id)
That's it. The manager handles:
- Creating
SessionState the moment a session_id is first seen (PostHog is the ground truth)
- Buffering actions before a conversation is linked
- Auto-detecting
NEW vs REPLY_EXISTING from the link store
- Flushing buffered actions the moment the link is established
- Routing all future delivery to
conversation_id permanently
session_id comes from PostHog. conversation_id comes from the chatbot platform. They are married together by on_chatbot_event. After that call, all proactive triggers, reactive notes, and future actions route to that conversation for the lifetime of the session.
Low-level building blocks (advanced use)
Use these directly only when you need custom Redis-backed stores or non-standard delivery.
from autoplay_sdk import (
InMemorySessionStateStore,
InMemoryConversationLinkStore,
link_conversation,
)
from autoplay_sdk.agent_state.v2 import SessionState
session_store = InMemorySessionStateStore()
link_store = InMemoryConversationLinkStore()
async def on_actions(payload):
if not payload.session_id:
return
state = await session_store.get_or_create(payload.session_id)
await writer.write_actions(...)
async def on_webhook(data):
session_id = data["custom_attributes"]["session_id"]
conversation_id = data["id"]
state = await session_store.get_or_create(session_id)
link_conversation(state=state, store=link_store, conversation_id=conversation_id)
await writer.on_session_linked(session_id, conversation_id)
SessionState fields:
state.session_id — mandatory required field; SessionState(session_id=...) — construction without it is a TypeError
state.metadata — optional extras (user_id, email, any future fields)
state.conversation_linked / state.conversation_id — linked chatbot conversation
state.current_state — FSM (THINKING / PROACTIVE / REACTIVE)
Production: swap to Redis-backed store
InMemorySessionStateStore loses all state on restart. For production, use RedisSessionStateStore — sessions survive restarts and scale across instances. It is a drop-in replacement:
from autoplay_sdk import AutoplayChatbotManager, RedisSessionStateStore
manager = AutoplayChatbotManager(
writer=MyWriter(),
session_store=RedisSessionStateStore(redis_url=os.environ["REDIS_URL"]),
)
Key pattern: autoplay:session_state:{session_id}. Default TTL: 24 h (configurable via ttl_s). On Redis errors, falls back gracefully and logs a warning — never crashes.
What breaks without correct scoping:
- Wrong user's events delivered to wrong conversation
- Empty context because retrieval key doesn't match storage key
- Pre-link actions silently dropped
Fastest SDK-native path (recommended)
import asyncio
from autoplay_sdk import AsyncConnectorClient, compose_chat_pipeline
from autoplay_sdk.context.user_index import UserSessionIndex
CONNECTOR_URL = "https://your-connector.onrender.com/stream/YOUR_PRODUCT_ID"
API_TOKEN = "your-api-token"
async def llm(prompt: str) -> str:
"""Wire your preferred LLM here.
The SDK only needs an async callable: (str) -> str.
Any provider works — OpenAI, Anthropic, Gemini, Mistral, a local model, etc.
OpenAI example:
from openai import AsyncOpenAI
_client = AsyncOpenAI()
r = await _client.chat.completions.create(model="gpt-4o-mini", messages=[{"role":"user","content":prompt}])
return r.choices[0].message.content
"""
raise NotImplementedError("Replace with your LLM client")
pipeline = compose_chat_pipeline(
llm=llm,
threshold=20,
lookback_seconds=300,
max_actions=20,
write_actions=None,
overwrite_with_summary=None,
)
user_index = UserSessionIndex(pipeline.context_store, lookback_seconds=300)
async def run():
async with AsyncConnectorClient(url=CONNECTOR_URL, token=API_TOKEN) as client:
async def on_actions(payload):
await pipeline.on_actions(payload)
user_index.add(payload)
client.on_actions(on_actions)
await client.run()
asyncio.run(run())
If you skip compose_chat_pipeline, summaries are never produced.
The on_summary / overwrite_with_summary callback requires the pipeline's
summarizer to be wired into the stream. There is no implicit fallback —
omitting this call means overwrite_with_summary never fires, regardless
of the threshold value.
When your chatbot gets a user_id, call:
activity = user_index.get_user_activity(user_id)
This avoids hand-rolled user_id -> session_id indexing and keeps product_id-aware lookups correct.
Analytics-only recipe (no chatbot)
If you only need to receive enriched events and forward them to your own
pipeline — no chatbot, no notes, no session linking — you need none of the
chatbot-shaped machinery above. Just the client:
import asyncio
from autoplay_sdk import AsyncConnectorClient
CONNECTOR_URL = "https://your-connector.onrender.com/stream/YOUR_PRODUCT_ID"
API_TOKEN = "your-api-token"
async def run():
async with AsyncConnectorClient(url=CONNECTOR_URL, token=API_TOKEN) as client:
async def on_actions(payload):
if not payload.session_id:
return
your_downstream(payload)
client.on_actions(on_actions)
await client.run()
asyncio.run(run())
No BaseChatbotWriter, no compose_chat_pipeline, no session store required.
Optional one-line HTTP bridge
If your chatbot runtime is in another process (Rasa/Botpress/Twilio/custom webhook), prefer the built-in FastAPI factory:
from autoplay_sdk.api import build_copilot_app
app = build_copilot_app(
stream_url=CONNECTOR_URL,
token=API_TOKEN,
llm=llm,
summary_threshold=20,
lookback_seconds=300,
)
Default endpoints:
GET /healthz
GET /context/{user_id}?query=...
GET /reply/{user_id}?query=...
POST /admin/reset/{user_id}
LLM guardrails
Use versioned prompt metadata with your provider client. The SDK does not currently ship a call_llm(...) helper.
MY_PROMPT = {
"name": "Support Answer Prompt",
"version": "0.1",
"description": "Answer user questions with product-aware context",
"content": "You are a helpful assistant...\n\n{context}",
}
messages = [{"role": "system", "content": MY_PROMPT["content"].format(context=context_text)}]
response = await openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
)
response = await openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "system", "content": "You are a helpful assistant..."}],
)
Prompt files live in src/llm/ only. Each exports:
MY_PROMPT = {
"name": "My Prompt",
"version": "0.1",
"description": "...",
"content": "...",
}
Log: prompt name + version, model, token usage, latency, errors. Never log full session payloads.
Integration Self-Reasoning Checklist
Run through this checklist before considering any chatbot integration complete. Reason through each question — do not skip.
1. Session identity
- Is
session_id the first thing set in SessionState?
state = SessionState(session_id=...) — SessionState() without session_id is a TypeError.
- Is PostHog the ground truth for
session_id? It must come from ActionsPayload.session_id, never from the chatbot platform.
- Is
await session_store.get_or_create(session_id) the very first await in every handler — before any processing, delivery, or API call?
- Does
state.metadata carry all optional identity context (user_id, email, etc.)? Nothing important should live outside SessionState.
2. Race conditions
- Webhook fires before stream:
get_or_create handles this — it creates the state with session_id set. Actions that arrive later from the stream route correctly because the state already exists.
- Stream fires before webhook: Actions buffer in
BaseChatbotWriter._pending until on_session_linked is called. Nothing is dropped.
- Concurrent webhooks for the same session:
InMemorySessionStateStore.get_or_create is a synchronous dict operation — race-safe. RedisSessionStateStore does a GET-before-SET — acceptable for session context (last writer wins on the rare collision).
- Session state missing after restart: If using
InMemorySessionStateStore in production, state is lost. Switch to RedisSessionStateStore.
3. Chatbot linking
- Is
link_conversation(state=state, store=store, conversation_id=conv_id) called with no event= argument? Let it auto-detect NEW vs REPLY_EXISTING from the store.
- Is
await writer.on_session_linked(session_id, conversation_id) called immediately after link_conversation? This is the flush trigger — without it, buffered actions are never delivered.
- Is the link sticky? Once
state.conversation_linked == True, the same session must not be re-linked to a different conversation_id. The REPLY_EXISTING semantics handle this automatically — but verify the webhook handler is not calling link_conversation again on subsequent replies.
- If using
RedisSessionStateStore: is await session_store.save(state) called after linking? AutoplayChatbotManager.on_chatbot_event does this automatically — verify if using the low-level path.
4. Proactive triggers
5. Persistence (production)
Smoke test — verify the full hop before going to production
python -m autoplay_sdk.smoke_test \
--url "$CONNECTOR_URL" \
--token "$API_TOKEN"
If session_id is empty at this stage, PostHog identify() has not fired yet —
see activity-posthog Step 2.
Reference