Chatbot Sessions
Group multi-turn chatbot traces into a single session by passing a session id per turn.
This guide shows how to instrument a multi-turn chatbot so every conversation turn appears as its own trace and all turns for a conversation are grouped together in the neatlogs platform.
The key idea: each turn is its own trace, and you pass the same session_id (your conversation id) on every turn so Neatlogs groups them. See Sessions for the full model.
Setup
import os
import neatlogs
neatlogs.init(
api_key=os.environ["NEATLOGS_API_KEY"],
workflow_name="support-chatbot",
instrumentations=["openai"],
)
from openai import OpenAI
client = OpenAI()Instrument the turn
Decorate the per-turn agent function with @neatlogs.span(kind="WORKFLOW") and pass session_id (your conversation id) on it. Each call produces one trace; all turns that pass the same session_id group into one session. Set the end_user_id here too if you track users.
from neatlogs import SystemPromptTemplate, UserPromptTemplate
system_tpl = SystemPromptTemplate([
{"role": "system", "content": "You are a helpful support assistant. Use the conversation history to give consistent answers."},
])
user_tpl = UserPromptTemplate([
{"role": "user", "content": "{{message}}"},
])
def chatbot_turn(message: str, history: list, conversation_id: str, user_id: str) -> str:
@neatlogs.span(
kind="WORKFLOW",
name="chatbot_turn",
session_id=conversation_id, # same value every turn → one session
end_user_id=user_id, # one end-user across the conversation
)
def _turn() -> str:
with neatlogs.trace("respond", kind="LLM",
system_prompt_template=system_tpl,
user_prompt_template=user_tpl):
system_msgs = system_tpl.compile()
user_msgs = user_tpl.compile(message=message)
messages = system_msgs + history + user_msgs
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
)
return response.choices[0].message.content
return _turn()Run a multi-turn conversation
Generate one conversation id at the start of the chat and pass it on every turn:
import uuid
conversation_id = f"conv_{uuid.uuid4().hex[:12]}" # one per conversation
user_id = "u_demo"
history = []
while True:
user_input = input("You: ")
if user_input.lower() in ("exit", "quit"):
break
reply = chatbot_turn(user_input, history, conversation_id, user_id)
print(f"Bot: {reply}")
history.append({"role": "user", "content": user_input})
history.append({"role": "assistant", "content": reply})
neatlogs.flush()
neatlogs.shutdown()On a server (many users)
On a web server, the conversation id comes from your request (a thread/conversation row in your DB), not a local variable — read it per request and pass it on the turn's root, exactly as above. If your turn handler only uses a wrapped client and opens no root of its own, bind identity with identify() instead:
client = neatlogs.wrap(OpenAI())
@app.post("/chat")
def chat(req):
with neatlogs.identify(session_id=req.conversation_id, end_user_id=str(req.user.id)):
return client.chat.completions.create(...) # auto-root inherits bothWhat you'll see in the dashboard
A session timeline showing all conversation turns in order. Each turn is an individual trace with its own WORKFLOW root and LLM sub-span. Because they share a session_id, the dashboard groups them under one session view, attributed to one end-user.
