From cc7c4c4591e88df4f0bbfe232961f448ecb57273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Blond=20Daugaard?= Date: Thu, 14 May 2026 09:12:20 -0400 Subject: [PATCH 1/3] Require runs for recorded observations --- README.md | 31 ++++++----- examples/failure_model_error.py | 5 +- examples/failure_retrieval_miss.py | 9 ++-- examples/failure_tool_loop.py | 11 ++-- examples/manual_trace.py | 9 ++-- src/sessionbat/__init__.py | 3 +- src/sessionbat/client.py | 59 +++++++++++++------- src/sessionbat/langchain.py | 83 +++++++++++++++++++++-------- tests/integration/test_langchain.py | 23 ++++++++ tests/test_client.py | 34 +++++++----- tests/test_langchain.py | 5 ++ 11 files changed, 189 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index b1112fd..dbd35a4 100644 --- a/README.md +++ b/README.md @@ -68,9 +68,11 @@ session = client.session( }, ) -session.user_message("I am locked out of my account") +run = session.run(run_id="run_123") -session.retrieval( +run.user_message("I am locked out of my account") + +run.retrieval( query="reset password locked out", documents=[ { @@ -83,7 +85,7 @@ session.retrieval( metrics={"latency_ms": 81, "documents_found": 1}, ) -session.tool_call( +run.tool_call( tool_name="lookup_account", input={"account_id": "acct_987"}, output={"status": "locked", "password_reset_available": True}, @@ -91,7 +93,7 @@ session.tool_call( metrics={"latency_ms": 117, "http_status": 200}, ) -session.assistant_response( +run.assistant_response( model="gpt-5.4-mini", request={"messages": [{"role": "user", "content": "I am locked out of my account"}]}, response={"text": "I found your account. Use the reset link and follow the email prompt."}, @@ -105,6 +107,7 @@ That emits structured events like: { "type": "llm", "session_id": "thread_123", + "run_id": "run_123", "tags": ["production"], "context": {"environment": "prod", "user_id": "user_123", "workspace_id": "ws_456"}, "observation": { @@ -119,7 +122,7 @@ That emits structured events like: Import the main types from `sessionbat`: ```python -from sessionbat import SessionBat, Session, LangChainCallbackHandler +from sessionbat import SessionBat, Session, Run, LangChainCallbackHandler ``` ### `SessionBat` @@ -134,8 +137,8 @@ client = SessionBat( ) ``` -Use `client.session(...)` to create a session and record observations against a -stable `session_id`. +Use `client.session(...)` to create a session with a stable `session_id`, then +use `session.run(...)` to record observations against a specific turn or run. The SDK sends events to SessionBat ingestion by default. Pass `api_key` directly or set `SESSIONBAT_API_KEY`. For tests or local debugging, pass an explicit @@ -147,15 +150,15 @@ bounded backoff, and queued events are flushed automatically during interpreter shutdown. Call `client.flush()` or `client.close()` when you need to wait for delivery before exiting a short-lived process. -### `Session` +### `Session` and `Run` -A `Session` records completed observations: +A `Session` groups runs. A `Run` records completed observations: -- `session.user_message(content)` -- `session.message(role=..., content=...)` -- `session.assistant_response(...)` -- `session.tool_call(...)` -- `session.retrieval(...)` +- `run.user_message(content)` +- `run.message(role=..., content=...)` +- `run.assistant_response(...)` +- `run.tool_call(...)` +- `run.retrieval(...)` Each call returns the generated observation id. diff --git a/examples/failure_model_error.py b/examples/failure_model_error.py index a4b0d85..706116e 100644 --- a/examples/failure_model_error.py +++ b/examples/failure_model_error.py @@ -19,10 +19,11 @@ def main() -> None: "plan": "starter", }, ) + run = session.run(run_id="run_model_error") - session.user_message("What invoices are overdue for Acme Corp?") + run.user_message("What invoices are overdue for Acme Corp?") - session.assistant_response( + run.assistant_response( model="gpt-5.4-mini", request={ "messages": [ diff --git a/examples/failure_retrieval_miss.py b/examples/failure_retrieval_miss.py index e800e0f..cface35 100644 --- a/examples/failure_retrieval_miss.py +++ b/examples/failure_retrieval_miss.py @@ -19,10 +19,11 @@ def main() -> None: "plan": "pro", }, ) + run = session.run(run_id="run_retrieval_miss") - session.user_message("How do I export audit logs for SSO users?") + run.user_message("How do I export audit logs for SSO users?") - session.retrieval( + run.retrieval( query="export audit logs for sso users", documents=[], metadata={ @@ -33,7 +34,7 @@ def main() -> None: metrics={"latency_ms": 63, "documents_found": 0}, ) - session.assistant_response( + run.assistant_response( model="gpt-5.4-mini", request={ "messages": [ @@ -46,7 +47,7 @@ def main() -> None: metadata={"provider": "openai"}, metrics={"latency_ms": 704, "input_tokens": 118, "output_tokens": 24}, ) - session.user_message( + run.user_message( "Can an admin download SAML access logs anywhere?", metadata={"sequence": "follow_up"}, ) diff --git a/examples/failure_tool_loop.py b/examples/failure_tool_loop.py index e2594f0..50e6e23 100644 --- a/examples/failure_tool_loop.py +++ b/examples/failure_tool_loop.py @@ -19,10 +19,11 @@ def main() -> None: "plan": "enterprise", }, ) + run = session.run(run_id="run_tool_loop") - session.user_message("Why is my workspace usage total wrong?") + run.user_message("Why is my workspace usage total wrong?") - session.tool_call( + run.tool_call( tool_name="get_workspace_usage", input={"workspace_id": "ws_999"}, output={"total_tokens": 192044, "cached": False}, @@ -30,7 +31,7 @@ def main() -> None: metrics={"latency_ms": 141, "http_status": 200, "attempt": 1}, ) - session.tool_call( + run.tool_call( tool_name="get_workspace_usage", input={"workspace_id": "ws_999"}, output={"total_tokens": 192044, "cached": False}, @@ -38,7 +39,7 @@ def main() -> None: metrics={"latency_ms": 136, "http_status": 200, "attempt": 2}, ) - session.tool_call( + run.tool_call( tool_name="get_workspace_usage", input={"workspace_id": "ws_999"}, output={"total_tokens": 192044, "cached": False}, @@ -46,7 +47,7 @@ def main() -> None: metrics={"latency_ms": 139, "http_status": 200, "attempt": 3}, ) - session.assistant_response( + run.assistant_response( model="gpt-5.4-mini", request={ "messages": [ diff --git a/examples/manual_trace.py b/examples/manual_trace.py index 0db23d3..b641475 100644 --- a/examples/manual_trace.py +++ b/examples/manual_trace.py @@ -19,10 +19,11 @@ def main() -> None: "plan": "pro", }, ) + run = session.run(run_id="run_123") - session.user_message("I am locked out of my account") + run.user_message("I am locked out of my account") - session.retrieval( + run.retrieval( query="reset password locked out", documents=[ { @@ -39,7 +40,7 @@ def main() -> None: metrics={"latency_ms": 81, "documents_found": 1}, ) - session.tool_call( + run.tool_call( tool_name="lookup_account", input={"account_id": "acct_987"}, output={"status": "locked", "password_reset_available": True}, @@ -47,7 +48,7 @@ def main() -> None: metrics={"latency_ms": 117, "http_status": 200}, ) - session.assistant_response( + run.assistant_response( model="gpt-5.4-mini", request={"messages": [{"role": "user", "content": "I am locked out of my account"}]}, response={"text": "I found your account. Use the reset link and follow the email prompt."}, diff --git a/src/sessionbat/__init__.py b/src/sessionbat/__init__.py index 9ef5864..0a77670 100644 --- a/src/sessionbat/__init__.py +++ b/src/sessionbat/__init__.py @@ -1,9 +1,10 @@ -from .client import Session, SessionBat +from .client import Run, Session, SessionBat from .langchain import LangChainCallbackHandler, SessionBatCallbackHandler __all__ = [ "SessionBat", "Session", + "Run", "LangChainCallbackHandler", "SessionBatCallbackHandler", ] diff --git a/src/sessionbat/client.py b/src/sessionbat/client.py index 0f0e348..bbcc290 100644 --- a/src/sessionbat/client.py +++ b/src/sessionbat/client.py @@ -107,6 +107,44 @@ class Session: tags: list[str] = field(default_factory=list) context: dict[str, Any] = field(default_factory=dict) + def run( + self, + *, + run_id: str, + tags: list[str] | None = None, + context: dict[str, Any] | None = None, + ) -> Run: + return Run( + session=self, + run_id=run_id, + tags=_merge_tags(self.tags, tags), + context=_merge_dicts(self.context, context), + ) + + def langchain_callback( + self, + *, + tags: list[str] | None = None, + context: dict[str, Any] | None = None, + metadata: dict[str, Any] | None = None, + ) -> Any: + from .langchain import LangChainCallbackHandler + + return LangChainCallbackHandler( + self, + tags=tags, + context=context, + metadata=metadata, + ) + + +@dataclass(slots=True) +class Run: + session: Session + run_id: str + tags: list[str] = field(default_factory=list) + context: dict[str, Any] = field(default_factory=dict) + def message( self, *, @@ -213,22 +251,6 @@ def retrieval( context=context, ) - def langchain_callback( - self, - *, - tags: list[str] | None = None, - context: dict[str, Any] | None = None, - metadata: dict[str, Any] | None = None, - ) -> Any: - from .langchain import LangChainCallbackHandler - - return LangChainCallbackHandler( - self, - tags=tags, - context=context, - metadata=metadata, - ) - def _record( self, *, @@ -252,7 +274,8 @@ def _record( payload = envelope.as_dict() payload.update( { - "session_id": self.session_id, + "session_id": self.session.session_id, + "run_id": self.run_id, "observation": { "kind": kind, "name": name, @@ -265,5 +288,5 @@ def _record( }, } ) - self.client._send(payload) + self.session.client._send(payload) return observation_id diff --git a/src/sessionbat/langchain.py b/src/sessionbat/langchain.py index 5d31053..b2b0e2d 100644 --- a/src/sessionbat/langchain.py +++ b/src/sessionbat/langchain.py @@ -7,6 +7,7 @@ from typing import Any from uuid import UUID +from .client import Run as SessionBatRun from .client import Session, SessionBat try: # LangChain is an optional integration dependency. @@ -22,7 +23,7 @@ class _RunState: kind: str name: str - session: Session + run: SessionBatRun input: dict[str, Any] metadata: dict[str, Any] = field(default_factory=dict) tags: list[str] = field(default_factory=list) @@ -67,6 +68,7 @@ def __init__( self.metadata = metadata or {} self._runs: dict[str, _RunState] = {} self._run_session_ids: dict[str, str] = {} + self._root_run_ids: dict[str, str] = {} self._sessions: dict[str, Session] = {} def on_chain_start( @@ -80,7 +82,7 @@ def on_chain_start( metadata: dict[str, Any] | None = None, **kwargs: Any, ) -> None: - self._remember_run_session(run_id, parent_run_id=parent_run_id, metadata=metadata) + self._remember_run(run_id, parent_run_id=parent_run_id, metadata=metadata) def on_llm_start( self, @@ -93,11 +95,11 @@ def on_llm_start( metadata: dict[str, Any] | None = None, **kwargs: Any, ) -> None: - session = self._session_for(run_id, parent_run_id=parent_run_id, metadata=metadata) + run = self._run_for(run_id, parent_run_id=parent_run_id, metadata=metadata) self._runs[_run_id(run_id)] = _RunState( kind="llm", name=_serialized_name(serialized, "llm"), - session=session, + run=run, input={ "prompts": _jsonable(prompts), "serialized": _jsonable(serialized), @@ -126,7 +128,7 @@ def on_chat_model_start( metadata: dict[str, Any] | None = None, **kwargs: Any, ) -> None: - session = self._session_for(run_id, parent_run_id=parent_run_id, metadata=metadata) + run = self._run_for(run_id, parent_run_id=parent_run_id, metadata=metadata) callback_metadata = self._metadata( run_id=run_id, parent_run_id=parent_run_id, @@ -137,14 +139,14 @@ def on_chat_model_start( ) self._record_messages_from_chat_input( messages, - session=session, + run=run, metadata=callback_metadata, tags=tags, ) self._runs[_run_id(run_id)] = _RunState( kind="llm", name=_serialized_name(serialized, "chat_model"), - session=session, + run=run, input={ "messages": _jsonable(messages), "serialized": _jsonable(serialized), @@ -168,7 +170,7 @@ def on_llm_end( state = self._pop_run(run_id, kind="llm", name="llm", parent_run_id=parent_run_id) response_payload = _llm_response_payload(response) model_metadata = _merge_dicts(state.metadata, metadata) - state.session.assistant_response( + state.run.assistant_response( model=_extract_model(response=response, metadata=model_metadata), request=state.input, response=response_payload, @@ -198,7 +200,7 @@ def on_llm_error( **kwargs: Any, ) -> None: state = self._pop_run(run_id, kind="llm", name="llm", parent_run_id=parent_run_id) - state.session.assistant_response( + state.run.assistant_response( model=_extract_model(metadata=_merge_dicts(state.metadata, metadata)), request=state.input, response=_jsonable(kwargs.get("response")), @@ -230,11 +232,11 @@ def on_tool_start( inputs: dict[str, Any] | None = None, **kwargs: Any, ) -> None: - session = self._session_for(run_id, parent_run_id=parent_run_id, metadata=metadata) + run = self._run_for(run_id, parent_run_id=parent_run_id, metadata=metadata) self._runs[_run_id(run_id)] = _RunState( kind="tool", name=_serialized_name(serialized, "tool"), - session=session, + run=run, input=_tool_input(input_str, inputs), metadata=self._metadata( run_id=run_id, @@ -259,7 +261,7 @@ def on_tool_end( **kwargs: Any, ) -> None: state = self._pop_run(run_id, kind="tool", name="tool", parent_run_id=parent_run_id) - state.session.tool_call( + state.run.tool_call( tool_name=state.name, input=state.input, output=_output_payload(output), @@ -289,7 +291,7 @@ def on_tool_error( **kwargs: Any, ) -> None: state = self._pop_run(run_id, kind="tool", name="tool", parent_run_id=parent_run_id) - state.session.tool_call( + state.run.tool_call( tool_name=state.name, input=state.input, error=_error_payload(error), @@ -319,11 +321,11 @@ def on_retriever_start( metadata: dict[str, Any] | None = None, **kwargs: Any, ) -> None: - session = self._session_for(run_id, parent_run_id=parent_run_id, metadata=metadata) + run = self._run_for(run_id, parent_run_id=parent_run_id, metadata=metadata) self._runs[_run_id(run_id)] = _RunState( kind="retrieval", name=_serialized_name(serialized, "retriever"), - session=session, + run=run, input={"query": query, "serialized": _jsonable(serialized)}, metadata=self._metadata( run_id=run_id, @@ -354,7 +356,7 @@ def on_retriever_end( parent_run_id=parent_run_id, ) docs = [_document_payload(document) for document in documents] - state.session.retrieval( + state.run.retrieval( query=str(state.input.get("query", "")), documents=docs, metadata=_merge_dicts( @@ -388,7 +390,7 @@ def on_retriever_error( name="retriever", parent_run_id=parent_run_id, ) - state.session.retrieval( + state.run.retrieval( query=str(state.input.get("query", "")), error=_error_payload(error), metadata=_merge_dicts( @@ -434,7 +436,7 @@ def _pop_run( _RunState( kind=kind, name=name, - session=self._session_for(run_id, parent_run_id=parent_run_id), + run=self._run_for(run_id, parent_run_id=parent_run_id), input={}, metadata=self._callback_metadata(run_id=run_id, parent_run_id=parent_run_id), parent_run_id=_optional_run_id(parent_run_id), @@ -495,7 +497,7 @@ def _record_messages_from_chat_input( self, message_batches: list[list[Any]], *, - session: Session, + run: SessionBatRun, metadata: dict[str, Any], tags: list[str] | None, ) -> None: @@ -503,7 +505,7 @@ def _record_messages_from_chat_input( role = _message_role(message) if role is None: continue - session.message( + run.message( role=role, content=_message_content(message), metadata=_merge_dicts( @@ -517,6 +519,21 @@ def _record_messages_from_chat_input( context=self.context, ) + def _run_for( + self, + run_id: UUID, + *, + parent_run_id: UUID | None = None, + metadata: dict[str, Any] | None = None, + ) -> SessionBatRun: + session = self._session_for( + run_id, + parent_run_id=parent_run_id, + metadata=metadata, + ) + sessionbat_run_id = self._sessionbat_run_id_for(run_id, parent_run_id=parent_run_id) + return session.run(run_id=sessionbat_run_id) + def _session_for( self, run_id: UUID, @@ -525,7 +542,9 @@ def _session_for( metadata: dict[str, Any] | None = None, ) -> Session: if self.session is not None: - return self.session + session = self.session + self._remember_run(run_id, parent_run_id=parent_run_id, metadata=metadata) + return session session_id = self._session_id_for( run_id, @@ -536,10 +555,10 @@ def _session_for( if session is None: session = self.client.session(session_id=session_id) self._sessions[session_id] = session - self._run_session_ids[_run_id(run_id)] = session_id + self._remember_run(run_id, parent_run_id=parent_run_id, metadata=metadata) return session - def _remember_run_session( + def _remember_run( self, run_id: UUID, *, @@ -552,8 +571,26 @@ def _remember_run_session( metadata=metadata, ) self._run_session_ids[_run_id(run_id)] = session_id + self._root_run_ids[_run_id(run_id)] = self._sessionbat_run_id_for( + run_id, + parent_run_id=parent_run_id, + ) return session_id + def _sessionbat_run_id_for( + self, + run_id: UUID, + *, + parent_run_id: UUID | None = None, + ) -> str: + if parent_run_id is None: + return _run_id(run_id) + + parent_root_run_id = self._root_run_ids.get(_run_id(parent_run_id)) + if parent_root_run_id: + return parent_root_run_id + return _run_id(parent_run_id) + def _session_id_for( self, run_id: UUID, diff --git a/tests/integration/test_langchain.py b/tests/integration/test_langchain.py index 4fb1e57..faf2516 100644 --- a/tests/integration/test_langchain.py +++ b/tests/integration/test_langchain.py @@ -120,6 +120,7 @@ def test_integrates_with_langchain_tool_runnable(self) -> None: assert event["type"] == "tool" assert event["session_id"] == "thread_tool" + assert event["run_id"] assert event["tags"] == ["development", "langchain", "tool"] assert observation["kind"] == "tool" assert observation["name"] == "lookup_account" @@ -154,6 +155,7 @@ def test_integrates_with_langchain_runnable_and_fake_chat_model(self) -> None: assert result.content == "Use the password reset link." assert len(self.transport.events) == 3 assert len({event["session_id"] for event in self.transport.events}) == 1 + assert len({event["run_id"] for event in self.transport.events}) == 1 system_event = self.transport.events[0] system_observation = system_event["observation"] @@ -232,6 +234,7 @@ def test_integrates_with_langchain_retrieval_chain(self) -> None: assert result.content == "Use the password reset link from the sign-in page." assert len(self.transport.events) == 4 assert len({event["session_id"] for event in self.transport.events}) == 1 + assert len({event["run_id"] for event in self.transport.events}) == 1 retrieval_observation = self.transport.events[0]["observation"] system_observation = self.transport.events[1]["observation"] @@ -284,6 +287,26 @@ def test_uses_langchain_metadata_session_id_when_present(self) -> None: ) assert {event["session_id"] for event in self.transport.events} == {"thread_123"} + assert len({event["run_id"] for event in self.transport.events}) == 1 + + def test_separate_langchain_invocations_get_separate_run_ids(self) -> None: + handler = self.client.langchain_callback(tags=["langchain"]) + chain = ChatPromptTemplate.from_messages([("human", "{question}")]) | FakeListChatModel( + responses=["First answer.", "Second answer."], + name="FakeSupportChat", + ) + + chain.invoke({"question": "First"}, config={"callbacks": [handler]}) + first_run_ids = {event["run_id"] for event in self.transport.events} + + self.transport.events.clear() + + chain.invoke({"question": "Second"}, config={"callbacks": [handler]}) + second_run_ids = {event["run_id"] for event in self.transport.events} + + assert len(first_run_ids) == 1 + assert len(second_run_ids) == 1 + assert first_run_ids != second_run_ids def _format_documents(documents: list[Document]) -> str: diff --git a/tests/test_client.py b/tests/test_client.py index 8648bce..c697ac4 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -29,9 +29,10 @@ def setup_method(self) -> None: tags=["support", "password-reset"], context={"workspace_id": "ws_123", "user_id": "user_123"}, ) + self.run = self.session.run(run_id="run_123", tags=["turn-1"]) def test_records_user_message_payload(self) -> None: - observation_id = self.session.user_message( + observation_id = self.run.user_message( "I am locked out", tags=["urgent", "support"], context={"locale": "en-US"}, @@ -44,7 +45,8 @@ def test_records_user_message_payload(self) -> None: assert event["id"] == observation_id assert event["type"] == "message" assert event["session_id"] == "thread_123" - assert event["tags"] == ["production", "support", "password-reset", "urgent"] + assert event["run_id"] == "run_123" + assert event["tags"] == ["production", "support", "password-reset", "turn-1", "urgent"] assert event["context"] == { "environment": "prod", "workspace_id": "ws_123", @@ -63,7 +65,7 @@ def test_records_user_message_payload(self) -> None: json.dumps(event) def test_records_assistant_response_payload(self) -> None: - self.session.assistant_response( + self.run.assistant_response( model="gpt-test", request={"messages": [{"role": "user", "content": "Help"}]}, response={"text": "Use the reset link."}, @@ -81,7 +83,7 @@ def test_records_assistant_response_payload(self) -> None: assert observation["metrics"] == {"input_tokens": 10, "output_tokens": 6} def test_records_tool_call_payload(self) -> None: - self.session.tool_call( + self.run.tool_call( tool_name="lookup_account", input={"account_id": "acct_123"}, output={"status": "locked"}, @@ -92,6 +94,7 @@ def test_records_tool_call_payload(self) -> None: observation = event["observation"] assert event["type"] == "tool" + assert event["run_id"] == "run_123" assert observation["kind"] == "tool" assert observation["name"] == "lookup_account" assert observation["input"] == {"account_id": "acct_123"} @@ -99,7 +102,7 @@ def test_records_tool_call_payload(self) -> None: assert observation["metrics"] == {"latency_ms": 117} def test_records_retrieval_payload(self) -> None: - self.session.retrieval( + self.run.retrieval( query="reset password", documents=[{"id": "doc_123", "score": 0.93}], metadata={"index": "support_articles"}, @@ -116,7 +119,7 @@ def test_records_retrieval_payload(self) -> None: assert observation["metadata"] == {"index": "support_articles"} def test_records_errors_on_failed_operations(self) -> None: - self.session.tool_call( + self.run.tool_call( tool_name="send_email", input={"template": "reset"}, error={"type": "TimeoutError", "message": "email service timed out"}, @@ -131,6 +134,12 @@ def test_records_errors_on_failed_operations(self) -> None: "message": "email service timed out", } + def test_session_records_observations_only_through_runs(self) -> None: + assert not hasattr(self.session, "user_message") + assert not hasattr(self.session, "assistant_response") + assert not hasattr(self.session, "tool_call") + assert not hasattr(self.session, "retrieval") + class _RecordingHandler(BaseHTTPRequestHandler): requests: list[dict] = [] @@ -201,8 +210,8 @@ def test_uses_environment_api_key( monkeypatch.setenv("SESSIONBAT_API_KEY", "sbat_ingest_env") client = SessionBat(endpoint=ingestion_server) - session = client.session(session_id="thread_123") - session.tool_call(tool_name="lookup_account", input={"account_id": "acct_123"}) + run = client.session(session_id="thread_123").run(run_id="run_123") + run.tool_call(tool_name="lookup_account", input={"account_id": "acct_123"}) assert client.flush(timeout=1.0) request = _RecordingHandler.requests[0] @@ -210,13 +219,13 @@ def test_uses_environment_api_key( def test_posts_sdk_payload_with_bearer_auth(self, ingestion_server: str) -> None: client = SessionBat(api_key="sbat_ingest_test", endpoint=ingestion_server) - session = client.session( + run = client.session( session_id="thread_123", tags=["support"], context={"user_id": "user_123"}, - ) + ).run(run_id="run_123", tags=["turn-1"]) - observation_id = session.tool_call( + observation_id = run.tool_call( tool_name="lookup_account", input={"account_id": "acct_123"}, output={"status": "locked"}, @@ -232,7 +241,8 @@ def test_posts_sdk_payload_with_bearer_auth(self, ingestion_server: str) -> None assert payload["id"] == observation_id assert payload["type"] == "tool" assert payload["session_id"] == "thread_123" - assert payload["tags"] == ["support"] + assert payload["run_id"] == "run_123" + assert payload["tags"] == ["support", "turn-1"] assert payload["context"] == {"user_id": "user_123"} assert payload["observation"]["kind"] == "tool" assert payload["observation"]["name"] == "lookup_account" diff --git a/tests/test_langchain.py b/tests/test_langchain.py index 129a10e..b9c32e4 100644 --- a/tests/test_langchain.py +++ b/tests/test_langchain.py @@ -78,6 +78,7 @@ def test_records_llm_completion_as_assistant_response(self) -> None: assert event["type"] == "llm" assert event["session_id"] == "thread_123" + assert event["run_id"] == str(run_id) assert event["tags"] == ["development", "support-bot", "langchain"] assert event["context"] == {"environment": "test", "user_id": "user_123"} assert observation["kind"] == "llm" @@ -131,6 +132,8 @@ def test_records_tool_completion_and_error(self) -> None: assert self.transport.events[0]["type"] == "tool" assert self.transport.events[1]["type"] == "tool" + assert self.transport.events[0]["run_id"] == str(success_run_id) + assert self.transport.events[1]["run_id"] == str(error_run_id) assert success["kind"] == "tool" assert success["name"] == "lookup_account" assert success["input"]["inputs"] == {"account_id": "acct_123"} @@ -158,6 +161,8 @@ def test_records_retrieval_completion_and_error(self) -> None: assert self.transport.events[0]["type"] == "retrieval" assert self.transport.events[1]["type"] == "retrieval" + assert self.transport.events[0]["run_id"] == str(success_run_id) + assert self.transport.events[1]["run_id"] == str(error_run_id) assert success["kind"] == "retrieval" assert success["input"] == {"query": "reset password"} assert success["output"]["documents"][0]["id"] == "doc_reset_password" From 464a8da6a3e2a20f3178990deaecbeee862d21a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Blond=20Daugaard?= Date: Thu, 14 May 2026 09:28:22 -0400 Subject: [PATCH 2/3] Rename runs to interactions --- README.md | 36 ++++++++--------- examples/failure_model_error.py | 6 +-- examples/failure_retrieval_miss.py | 10 ++--- examples/failure_tool_loop.py | 12 +++--- examples/manual_trace.py | 10 ++--- src/sessionbat/__init__.py | 4 +- src/sessionbat/client.py | 16 ++++---- src/sessionbat/langchain.py | 63 +++++++++++++++-------------- tests/integration/test_langchain.py | 20 ++++----- tests/test_client.py | 34 +++++++++------- tests/test_langchain.py | 10 ++--- 11 files changed, 114 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index dbd35a4..201ad93 100644 --- a/README.md +++ b/README.md @@ -68,11 +68,11 @@ session = client.session( }, ) -run = session.run(run_id="run_123") +interaction = session.interaction(interaction_id="interaction_123") -run.user_message("I am locked out of my account") +interaction.user_message("I am locked out of my account") -run.retrieval( +interaction.retrieval( query="reset password locked out", documents=[ { @@ -85,7 +85,7 @@ run.retrieval( metrics={"latency_ms": 81, "documents_found": 1}, ) -run.tool_call( +interaction.tool_call( tool_name="lookup_account", input={"account_id": "acct_987"}, output={"status": "locked", "password_reset_available": True}, @@ -93,7 +93,7 @@ run.tool_call( metrics={"latency_ms": 117, "http_status": 200}, ) -run.assistant_response( +interaction.assistant_response( model="gpt-5.4-mini", request={"messages": [{"role": "user", "content": "I am locked out of my account"}]}, response={"text": "I found your account. Use the reset link and follow the email prompt."}, @@ -107,7 +107,7 @@ That emits structured events like: { "type": "llm", "session_id": "thread_123", - "run_id": "run_123", + "interaction_id": "interaction_123", "tags": ["production"], "context": {"environment": "prod", "user_id": "user_123", "workspace_id": "ws_456"}, "observation": { @@ -122,7 +122,7 @@ That emits structured events like: Import the main types from `sessionbat`: ```python -from sessionbat import SessionBat, Session, Run, LangChainCallbackHandler +from sessionbat import SessionBat, Session, Interaction, LangChainCallbackHandler ``` ### `SessionBat` @@ -138,7 +138,7 @@ client = SessionBat( ``` Use `client.session(...)` to create a session with a stable `session_id`, then -use `session.run(...)` to record observations against a specific turn or run. +use `session.interaction(...)` to record observations against a specific turn or interaction. The SDK sends events to SessionBat ingestion by default. Pass `api_key` directly or set `SESSIONBAT_API_KEY`. For tests or local debugging, pass an explicit @@ -150,15 +150,15 @@ bounded backoff, and queued events are flushed automatically during interpreter shutdown. Call `client.flush()` or `client.close()` when you need to wait for delivery before exiting a short-lived process. -### `Session` and `Run` +### `Session` and `Interaction` -A `Session` groups runs. A `Run` records completed observations: +A `Session` groups interactions. A `Interaction` records completed observations: -- `run.user_message(content)` -- `run.message(role=..., content=...)` -- `run.assistant_response(...)` -- `run.tool_call(...)` -- `run.retrieval(...)` +- `interaction.user_message(content)` +- `interaction.message(role=..., content=...)` +- `interaction.assistant_response(...)` +- `interaction.tool_call(...)` +- `interaction.retrieval(...)` Each call returns the generated observation id. @@ -178,9 +178,9 @@ SessionBat keeps the shape intentionally small: ## Development ```bash -uv run pytest -uv run ruff check . -uv run ruff format --check . +uv interaction pytest +uv interaction ruff check . +uv interaction ruff format --check . ``` ## Repository layout diff --git a/examples/failure_model_error.py b/examples/failure_model_error.py index 706116e..89b7354 100644 --- a/examples/failure_model_error.py +++ b/examples/failure_model_error.py @@ -19,11 +19,11 @@ def main() -> None: "plan": "starter", }, ) - run = session.run(run_id="run_model_error") + interaction = session.interaction(interaction_id="interaction_model_error") - run.user_message("What invoices are overdue for Acme Corp?") + interaction.user_message("What invoices are overdue for Acme Corp?") - run.assistant_response( + interaction.assistant_response( model="gpt-5.4-mini", request={ "messages": [ diff --git a/examples/failure_retrieval_miss.py b/examples/failure_retrieval_miss.py index cface35..e02df68 100644 --- a/examples/failure_retrieval_miss.py +++ b/examples/failure_retrieval_miss.py @@ -19,11 +19,11 @@ def main() -> None: "plan": "pro", }, ) - run = session.run(run_id="run_retrieval_miss") + interaction = session.interaction(interaction_id="interaction_retrieval_miss") - run.user_message("How do I export audit logs for SSO users?") + interaction.user_message("How do I export audit logs for SSO users?") - run.retrieval( + interaction.retrieval( query="export audit logs for sso users", documents=[], metadata={ @@ -34,7 +34,7 @@ def main() -> None: metrics={"latency_ms": 63, "documents_found": 0}, ) - run.assistant_response( + interaction.assistant_response( model="gpt-5.4-mini", request={ "messages": [ @@ -47,7 +47,7 @@ def main() -> None: metadata={"provider": "openai"}, metrics={"latency_ms": 704, "input_tokens": 118, "output_tokens": 24}, ) - run.user_message( + interaction.user_message( "Can an admin download SAML access logs anywhere?", metadata={"sequence": "follow_up"}, ) diff --git a/examples/failure_tool_loop.py b/examples/failure_tool_loop.py index 50e6e23..b1c797f 100644 --- a/examples/failure_tool_loop.py +++ b/examples/failure_tool_loop.py @@ -19,11 +19,11 @@ def main() -> None: "plan": "enterprise", }, ) - run = session.run(run_id="run_tool_loop") + interaction = session.interaction(interaction_id="interaction_tool_loop") - run.user_message("Why is my workspace usage total wrong?") + interaction.user_message("Why is my workspace usage total wrong?") - run.tool_call( + interaction.tool_call( tool_name="get_workspace_usage", input={"workspace_id": "ws_999"}, output={"total_tokens": 192044, "cached": False}, @@ -31,7 +31,7 @@ def main() -> None: metrics={"latency_ms": 141, "http_status": 200, "attempt": 1}, ) - run.tool_call( + interaction.tool_call( tool_name="get_workspace_usage", input={"workspace_id": "ws_999"}, output={"total_tokens": 192044, "cached": False}, @@ -39,7 +39,7 @@ def main() -> None: metrics={"latency_ms": 136, "http_status": 200, "attempt": 2}, ) - run.tool_call( + interaction.tool_call( tool_name="get_workspace_usage", input={"workspace_id": "ws_999"}, output={"total_tokens": 192044, "cached": False}, @@ -47,7 +47,7 @@ def main() -> None: metrics={"latency_ms": 139, "http_status": 200, "attempt": 3}, ) - run.assistant_response( + interaction.assistant_response( model="gpt-5.4-mini", request={ "messages": [ diff --git a/examples/manual_trace.py b/examples/manual_trace.py index b641475..9d4738e 100644 --- a/examples/manual_trace.py +++ b/examples/manual_trace.py @@ -19,11 +19,11 @@ def main() -> None: "plan": "pro", }, ) - run = session.run(run_id="run_123") + interaction = session.interaction(interaction_id="interaction_123") - run.user_message("I am locked out of my account") + interaction.user_message("I am locked out of my account") - run.retrieval( + interaction.retrieval( query="reset password locked out", documents=[ { @@ -40,7 +40,7 @@ def main() -> None: metrics={"latency_ms": 81, "documents_found": 1}, ) - run.tool_call( + interaction.tool_call( tool_name="lookup_account", input={"account_id": "acct_987"}, output={"status": "locked", "password_reset_available": True}, @@ -48,7 +48,7 @@ def main() -> None: metrics={"latency_ms": 117, "http_status": 200}, ) - run.assistant_response( + interaction.assistant_response( model="gpt-5.4-mini", request={"messages": [{"role": "user", "content": "I am locked out of my account"}]}, response={"text": "I found your account. Use the reset link and follow the email prompt."}, diff --git a/src/sessionbat/__init__.py b/src/sessionbat/__init__.py index 0a77670..f4b755f 100644 --- a/src/sessionbat/__init__.py +++ b/src/sessionbat/__init__.py @@ -1,10 +1,10 @@ -from .client import Run, Session, SessionBat +from .client import Interaction, Session, SessionBat from .langchain import LangChainCallbackHandler, SessionBatCallbackHandler __all__ = [ "SessionBat", "Session", - "Run", + "Interaction", "LangChainCallbackHandler", "SessionBatCallbackHandler", ] diff --git a/src/sessionbat/client.py b/src/sessionbat/client.py index bbcc290..0b8d297 100644 --- a/src/sessionbat/client.py +++ b/src/sessionbat/client.py @@ -107,16 +107,16 @@ class Session: tags: list[str] = field(default_factory=list) context: dict[str, Any] = field(default_factory=dict) - def run( + def interaction( self, *, - run_id: str, + interaction_id: str, tags: list[str] | None = None, context: dict[str, Any] | None = None, - ) -> Run: - return Run( + ) -> Interaction: + return Interaction( session=self, - run_id=run_id, + interaction_id=interaction_id, tags=_merge_tags(self.tags, tags), context=_merge_dicts(self.context, context), ) @@ -139,9 +139,9 @@ def langchain_callback( @dataclass(slots=True) -class Run: +class Interaction: session: Session - run_id: str + interaction_id: str tags: list[str] = field(default_factory=list) context: dict[str, Any] = field(default_factory=dict) @@ -275,7 +275,7 @@ def _record( payload.update( { "session_id": self.session.session_id, - "run_id": self.run_id, + "interaction_id": self.interaction_id, "observation": { "kind": kind, "name": name, diff --git a/src/sessionbat/langchain.py b/src/sessionbat/langchain.py index b2b0e2d..64febaf 100644 --- a/src/sessionbat/langchain.py +++ b/src/sessionbat/langchain.py @@ -7,7 +7,7 @@ from typing import Any from uuid import UUID -from .client import Run as SessionBatRun +from .client import Interaction as SessionBatInteraction from .client import Session, SessionBat try: # LangChain is an optional integration dependency. @@ -23,7 +23,7 @@ class _RunState: kind: str name: str - run: SessionBatRun + interaction: SessionBatInteraction input: dict[str, Any] metadata: dict[str, Any] = field(default_factory=dict) tags: list[str] = field(default_factory=list) @@ -68,7 +68,7 @@ def __init__( self.metadata = metadata or {} self._runs: dict[str, _RunState] = {} self._run_session_ids: dict[str, str] = {} - self._root_run_ids: dict[str, str] = {} + self._root_interaction_ids: dict[str, str] = {} self._sessions: dict[str, Session] = {} def on_chain_start( @@ -95,11 +95,11 @@ def on_llm_start( metadata: dict[str, Any] | None = None, **kwargs: Any, ) -> None: - run = self._run_for(run_id, parent_run_id=parent_run_id, metadata=metadata) + interaction = self._interaction_for(run_id, parent_run_id=parent_run_id, metadata=metadata) self._runs[_run_id(run_id)] = _RunState( kind="llm", name=_serialized_name(serialized, "llm"), - run=run, + interaction=interaction, input={ "prompts": _jsonable(prompts), "serialized": _jsonable(serialized), @@ -128,7 +128,7 @@ def on_chat_model_start( metadata: dict[str, Any] | None = None, **kwargs: Any, ) -> None: - run = self._run_for(run_id, parent_run_id=parent_run_id, metadata=metadata) + interaction = self._interaction_for(run_id, parent_run_id=parent_run_id, metadata=metadata) callback_metadata = self._metadata( run_id=run_id, parent_run_id=parent_run_id, @@ -139,14 +139,14 @@ def on_chat_model_start( ) self._record_messages_from_chat_input( messages, - run=run, + interaction=interaction, metadata=callback_metadata, tags=tags, ) self._runs[_run_id(run_id)] = _RunState( kind="llm", name=_serialized_name(serialized, "chat_model"), - run=run, + interaction=interaction, input={ "messages": _jsonable(messages), "serialized": _jsonable(serialized), @@ -170,7 +170,7 @@ def on_llm_end( state = self._pop_run(run_id, kind="llm", name="llm", parent_run_id=parent_run_id) response_payload = _llm_response_payload(response) model_metadata = _merge_dicts(state.metadata, metadata) - state.run.assistant_response( + state.interaction.assistant_response( model=_extract_model(response=response, metadata=model_metadata), request=state.input, response=response_payload, @@ -200,7 +200,7 @@ def on_llm_error( **kwargs: Any, ) -> None: state = self._pop_run(run_id, kind="llm", name="llm", parent_run_id=parent_run_id) - state.run.assistant_response( + state.interaction.assistant_response( model=_extract_model(metadata=_merge_dicts(state.metadata, metadata)), request=state.input, response=_jsonable(kwargs.get("response")), @@ -232,11 +232,11 @@ def on_tool_start( inputs: dict[str, Any] | None = None, **kwargs: Any, ) -> None: - run = self._run_for(run_id, parent_run_id=parent_run_id, metadata=metadata) + interaction = self._interaction_for(run_id, parent_run_id=parent_run_id, metadata=metadata) self._runs[_run_id(run_id)] = _RunState( kind="tool", name=_serialized_name(serialized, "tool"), - run=run, + interaction=interaction, input=_tool_input(input_str, inputs), metadata=self._metadata( run_id=run_id, @@ -261,7 +261,7 @@ def on_tool_end( **kwargs: Any, ) -> None: state = self._pop_run(run_id, kind="tool", name="tool", parent_run_id=parent_run_id) - state.run.tool_call( + state.interaction.tool_call( tool_name=state.name, input=state.input, output=_output_payload(output), @@ -291,7 +291,7 @@ def on_tool_error( **kwargs: Any, ) -> None: state = self._pop_run(run_id, kind="tool", name="tool", parent_run_id=parent_run_id) - state.run.tool_call( + state.interaction.tool_call( tool_name=state.name, input=state.input, error=_error_payload(error), @@ -321,11 +321,11 @@ def on_retriever_start( metadata: dict[str, Any] | None = None, **kwargs: Any, ) -> None: - run = self._run_for(run_id, parent_run_id=parent_run_id, metadata=metadata) + interaction = self._interaction_for(run_id, parent_run_id=parent_run_id, metadata=metadata) self._runs[_run_id(run_id)] = _RunState( kind="retrieval", name=_serialized_name(serialized, "retriever"), - run=run, + interaction=interaction, input={"query": query, "serialized": _jsonable(serialized)}, metadata=self._metadata( run_id=run_id, @@ -356,7 +356,7 @@ def on_retriever_end( parent_run_id=parent_run_id, ) docs = [_document_payload(document) for document in documents] - state.run.retrieval( + state.interaction.retrieval( query=str(state.input.get("query", "")), documents=docs, metadata=_merge_dicts( @@ -390,7 +390,7 @@ def on_retriever_error( name="retriever", parent_run_id=parent_run_id, ) - state.run.retrieval( + state.interaction.retrieval( query=str(state.input.get("query", "")), error=_error_payload(error), metadata=_merge_dicts( @@ -436,7 +436,7 @@ def _pop_run( _RunState( kind=kind, name=name, - run=self._run_for(run_id, parent_run_id=parent_run_id), + interaction=self._interaction_for(run_id, parent_run_id=parent_run_id), input={}, metadata=self._callback_metadata(run_id=run_id, parent_run_id=parent_run_id), parent_run_id=_optional_run_id(parent_run_id), @@ -497,7 +497,7 @@ def _record_messages_from_chat_input( self, message_batches: list[list[Any]], *, - run: SessionBatRun, + interaction: SessionBatInteraction, metadata: dict[str, Any], tags: list[str] | None, ) -> None: @@ -505,7 +505,7 @@ def _record_messages_from_chat_input( role = _message_role(message) if role is None: continue - run.message( + interaction.message( role=role, content=_message_content(message), metadata=_merge_dicts( @@ -519,20 +519,23 @@ def _record_messages_from_chat_input( context=self.context, ) - def _run_for( + def _interaction_for( self, run_id: UUID, *, parent_run_id: UUID | None = None, metadata: dict[str, Any] | None = None, - ) -> SessionBatRun: + ) -> SessionBatInteraction: session = self._session_for( run_id, parent_run_id=parent_run_id, metadata=metadata, ) - sessionbat_run_id = self._sessionbat_run_id_for(run_id, parent_run_id=parent_run_id) - return session.run(run_id=sessionbat_run_id) + sessionbat_interaction_id = self._sessionbat_interaction_id_for( + run_id, + parent_run_id=parent_run_id, + ) + return session.interaction(interaction_id=sessionbat_interaction_id) def _session_for( self, @@ -571,13 +574,13 @@ def _remember_run( metadata=metadata, ) self._run_session_ids[_run_id(run_id)] = session_id - self._root_run_ids[_run_id(run_id)] = self._sessionbat_run_id_for( + self._root_interaction_ids[_run_id(run_id)] = self._sessionbat_interaction_id_for( run_id, parent_run_id=parent_run_id, ) return session_id - def _sessionbat_run_id_for( + def _sessionbat_interaction_id_for( self, run_id: UUID, *, @@ -586,9 +589,9 @@ def _sessionbat_run_id_for( if parent_run_id is None: return _run_id(run_id) - parent_root_run_id = self._root_run_ids.get(_run_id(parent_run_id)) - if parent_root_run_id: - return parent_root_run_id + parent_root_interaction_id = self._root_interaction_ids.get(_run_id(parent_run_id)) + if parent_root_interaction_id: + return parent_root_interaction_id return _run_id(parent_run_id) def _session_id_for( diff --git a/tests/integration/test_langchain.py b/tests/integration/test_langchain.py index faf2516..2e93b8b 100644 --- a/tests/integration/test_langchain.py +++ b/tests/integration/test_langchain.py @@ -120,7 +120,7 @@ def test_integrates_with_langchain_tool_runnable(self) -> None: assert event["type"] == "tool" assert event["session_id"] == "thread_tool" - assert event["run_id"] + assert event["interaction_id"] assert event["tags"] == ["development", "langchain", "tool"] assert observation["kind"] == "tool" assert observation["name"] == "lookup_account" @@ -155,7 +155,7 @@ def test_integrates_with_langchain_runnable_and_fake_chat_model(self) -> None: assert result.content == "Use the password reset link." assert len(self.transport.events) == 3 assert len({event["session_id"] for event in self.transport.events}) == 1 - assert len({event["run_id"] for event in self.transport.events}) == 1 + assert len({event["interaction_id"] for event in self.transport.events}) == 1 system_event = self.transport.events[0] system_observation = system_event["observation"] @@ -234,7 +234,7 @@ def test_integrates_with_langchain_retrieval_chain(self) -> None: assert result.content == "Use the password reset link from the sign-in page." assert len(self.transport.events) == 4 assert len({event["session_id"] for event in self.transport.events}) == 1 - assert len({event["run_id"] for event in self.transport.events}) == 1 + assert len({event["interaction_id"] for event in self.transport.events}) == 1 retrieval_observation = self.transport.events[0]["observation"] system_observation = self.transport.events[1]["observation"] @@ -287,9 +287,9 @@ def test_uses_langchain_metadata_session_id_when_present(self) -> None: ) assert {event["session_id"] for event in self.transport.events} == {"thread_123"} - assert len({event["run_id"] for event in self.transport.events}) == 1 + assert len({event["interaction_id"] for event in self.transport.events}) == 1 - def test_separate_langchain_invocations_get_separate_run_ids(self) -> None: + def test_separate_langchain_invocations_get_separate_interaction_ids(self) -> None: handler = self.client.langchain_callback(tags=["langchain"]) chain = ChatPromptTemplate.from_messages([("human", "{question}")]) | FakeListChatModel( responses=["First answer.", "Second answer."], @@ -297,16 +297,16 @@ def test_separate_langchain_invocations_get_separate_run_ids(self) -> None: ) chain.invoke({"question": "First"}, config={"callbacks": [handler]}) - first_run_ids = {event["run_id"] for event in self.transport.events} + first_interaction_ids = {event["interaction_id"] for event in self.transport.events} self.transport.events.clear() chain.invoke({"question": "Second"}, config={"callbacks": [handler]}) - second_run_ids = {event["run_id"] for event in self.transport.events} + second_interaction_ids = {event["interaction_id"] for event in self.transport.events} - assert len(first_run_ids) == 1 - assert len(second_run_ids) == 1 - assert first_run_ids != second_run_ids + assert len(first_interaction_ids) == 1 + assert len(second_interaction_ids) == 1 + assert first_interaction_ids != second_interaction_ids def _format_documents(documents: list[Document]) -> str: diff --git a/tests/test_client.py b/tests/test_client.py index c697ac4..0964d82 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -29,10 +29,12 @@ def setup_method(self) -> None: tags=["support", "password-reset"], context={"workspace_id": "ws_123", "user_id": "user_123"}, ) - self.run = self.session.run(run_id="run_123", tags=["turn-1"]) + self.interaction = self.session.interaction( + interaction_id="interaction_123", tags=["turn-1"] + ) def test_records_user_message_payload(self) -> None: - observation_id = self.run.user_message( + observation_id = self.interaction.user_message( "I am locked out", tags=["urgent", "support"], context={"locale": "en-US"}, @@ -45,7 +47,7 @@ def test_records_user_message_payload(self) -> None: assert event["id"] == observation_id assert event["type"] == "message" assert event["session_id"] == "thread_123" - assert event["run_id"] == "run_123" + assert event["interaction_id"] == "interaction_123" assert event["tags"] == ["production", "support", "password-reset", "turn-1", "urgent"] assert event["context"] == { "environment": "prod", @@ -65,7 +67,7 @@ def test_records_user_message_payload(self) -> None: json.dumps(event) def test_records_assistant_response_payload(self) -> None: - self.run.assistant_response( + self.interaction.assistant_response( model="gpt-test", request={"messages": [{"role": "user", "content": "Help"}]}, response={"text": "Use the reset link."}, @@ -83,7 +85,7 @@ def test_records_assistant_response_payload(self) -> None: assert observation["metrics"] == {"input_tokens": 10, "output_tokens": 6} def test_records_tool_call_payload(self) -> None: - self.run.tool_call( + self.interaction.tool_call( tool_name="lookup_account", input={"account_id": "acct_123"}, output={"status": "locked"}, @@ -94,7 +96,7 @@ def test_records_tool_call_payload(self) -> None: observation = event["observation"] assert event["type"] == "tool" - assert event["run_id"] == "run_123" + assert event["interaction_id"] == "interaction_123" assert observation["kind"] == "tool" assert observation["name"] == "lookup_account" assert observation["input"] == {"account_id": "acct_123"} @@ -102,7 +104,7 @@ def test_records_tool_call_payload(self) -> None: assert observation["metrics"] == {"latency_ms": 117} def test_records_retrieval_payload(self) -> None: - self.run.retrieval( + self.interaction.retrieval( query="reset password", documents=[{"id": "doc_123", "score": 0.93}], metadata={"index": "support_articles"}, @@ -119,7 +121,7 @@ def test_records_retrieval_payload(self) -> None: assert observation["metadata"] == {"index": "support_articles"} def test_records_errors_on_failed_operations(self) -> None: - self.run.tool_call( + self.interaction.tool_call( tool_name="send_email", input={"template": "reset"}, error={"type": "TimeoutError", "message": "email service timed out"}, @@ -134,7 +136,7 @@ def test_records_errors_on_failed_operations(self) -> None: "message": "email service timed out", } - def test_session_records_observations_only_through_runs(self) -> None: + def test_session_records_observations_only_through_interactions(self) -> None: assert not hasattr(self.session, "user_message") assert not hasattr(self.session, "assistant_response") assert not hasattr(self.session, "tool_call") @@ -210,8 +212,10 @@ def test_uses_environment_api_key( monkeypatch.setenv("SESSIONBAT_API_KEY", "sbat_ingest_env") client = SessionBat(endpoint=ingestion_server) - run = client.session(session_id="thread_123").run(run_id="run_123") - run.tool_call(tool_name="lookup_account", input={"account_id": "acct_123"}) + interaction = client.session(session_id="thread_123").interaction( + interaction_id="interaction_123" + ) + interaction.tool_call(tool_name="lookup_account", input={"account_id": "acct_123"}) assert client.flush(timeout=1.0) request = _RecordingHandler.requests[0] @@ -219,13 +223,13 @@ def test_uses_environment_api_key( def test_posts_sdk_payload_with_bearer_auth(self, ingestion_server: str) -> None: client = SessionBat(api_key="sbat_ingest_test", endpoint=ingestion_server) - run = client.session( + interaction = client.session( session_id="thread_123", tags=["support"], context={"user_id": "user_123"}, - ).run(run_id="run_123", tags=["turn-1"]) + ).interaction(interaction_id="interaction_123", tags=["turn-1"]) - observation_id = run.tool_call( + observation_id = interaction.tool_call( tool_name="lookup_account", input={"account_id": "acct_123"}, output={"status": "locked"}, @@ -241,7 +245,7 @@ def test_posts_sdk_payload_with_bearer_auth(self, ingestion_server: str) -> None assert payload["id"] == observation_id assert payload["type"] == "tool" assert payload["session_id"] == "thread_123" - assert payload["run_id"] == "run_123" + assert payload["interaction_id"] == "interaction_123" assert payload["tags"] == ["support", "turn-1"] assert payload["context"] == {"user_id": "user_123"} assert payload["observation"]["kind"] == "tool" diff --git a/tests/test_langchain.py b/tests/test_langchain.py index b9c32e4..9489c8d 100644 --- a/tests/test_langchain.py +++ b/tests/test_langchain.py @@ -78,7 +78,7 @@ def test_records_llm_completion_as_assistant_response(self) -> None: assert event["type"] == "llm" assert event["session_id"] == "thread_123" - assert event["run_id"] == str(run_id) + assert event["interaction_id"] == str(run_id) assert event["tags"] == ["development", "support-bot", "langchain"] assert event["context"] == {"environment": "test", "user_id": "user_123"} assert observation["kind"] == "llm" @@ -132,8 +132,8 @@ def test_records_tool_completion_and_error(self) -> None: assert self.transport.events[0]["type"] == "tool" assert self.transport.events[1]["type"] == "tool" - assert self.transport.events[0]["run_id"] == str(success_run_id) - assert self.transport.events[1]["run_id"] == str(error_run_id) + assert self.transport.events[0]["interaction_id"] == str(success_run_id) + assert self.transport.events[1]["interaction_id"] == str(error_run_id) assert success["kind"] == "tool" assert success["name"] == "lookup_account" assert success["input"]["inputs"] == {"account_id": "acct_123"} @@ -161,8 +161,8 @@ def test_records_retrieval_completion_and_error(self) -> None: assert self.transport.events[0]["type"] == "retrieval" assert self.transport.events[1]["type"] == "retrieval" - assert self.transport.events[0]["run_id"] == str(success_run_id) - assert self.transport.events[1]["run_id"] == str(error_run_id) + assert self.transport.events[0]["interaction_id"] == str(success_run_id) + assert self.transport.events[1]["interaction_id"] == str(error_run_id) assert success["kind"] == "retrieval" assert success["input"] == {"query": "reset password"} assert success["output"]["documents"][0]["id"] == "doc_reset_password" From 75ad58b389c953d79c718d62815a8039d9e0a540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Blond=20Daugaard?= Date: Thu, 14 May 2026 09:30:22 -0400 Subject: [PATCH 3/3] Fix SDK README development commands --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 201ad93..90aeae2 100644 --- a/README.md +++ b/README.md @@ -178,9 +178,9 @@ SessionBat keeps the shape intentionally small: ## Development ```bash -uv interaction pytest -uv interaction ruff check . -uv interaction ruff format --check . +uv run pytest +uv run ruff check . +uv run ruff format --check . ``` ## Repository layout