Skip to content

Commit 6ab2856

Browse files
authored
feat(llmo): Use default PH client for LangChain (#293)
* feat(llmo): Use default PH client for langchain * chore: Run formatter * feat: Test the CallbackHandler without any PH client * chore: Run formatter
1 parent 7a8b091 commit 6ab2856

File tree

2 files changed

+38
-13
lines changed

2 files changed

+38
-13
lines changed

posthog/ai/langchain/callbacks.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from langchain_core.outputs import ChatGeneration, LLMResult
3434
from pydantic import BaseModel
3535

36-
from posthog import default_client
36+
from posthog import setup
3737
from posthog.ai.utils import get_model_params, with_privacy_mode
3838
from posthog.client import Client
3939

@@ -81,7 +81,7 @@ class CallbackHandler(BaseCallbackHandler):
8181
The PostHog LLM observability callback handler for LangChain.
8282
"""
8383

84-
_client: Client
84+
_ph_client: Client
8585
"""PostHog client instance."""
8686

8787
_distinct_id: Optional[Union[str, int, UUID]]
@@ -127,10 +127,7 @@ def __init__(
127127
privacy_mode: Whether to redact the input and output of the trace.
128128
groups: Optional additional PostHog groups to use for the trace.
129129
"""
130-
posthog_client = client or default_client
131-
if posthog_client is None:
132-
raise ValueError("PostHog client is required")
133-
self._client = posthog_client
130+
self._ph_client = client or setup()
134131
self._distinct_id = distinct_id
135132
self._trace_id = trace_id
136133
self._properties = properties or {}
@@ -481,7 +478,7 @@ def _capture_trace_or_span(
481478
event_properties = {
482479
"$ai_trace_id": trace_id,
483480
"$ai_input_state": with_privacy_mode(
484-
self._client, self._privacy_mode, run.input
481+
self._ph_client, self._privacy_mode, run.input
485482
),
486483
"$ai_latency": run.latency,
487484
"$ai_span_name": run.name,
@@ -497,13 +494,13 @@ def _capture_trace_or_span(
497494
event_properties["$ai_is_error"] = True
498495
elif outputs is not None:
499496
event_properties["$ai_output_state"] = with_privacy_mode(
500-
self._client, self._privacy_mode, outputs
497+
self._ph_client, self._privacy_mode, outputs
501498
)
502499

503500
if self._distinct_id is None:
504501
event_properties["$process_person_profile"] = False
505502

506-
self._client.capture(
503+
self._ph_client.capture(
507504
distinct_id=self._distinct_id or run_id,
508505
event=event_name,
509506
properties=event_properties,
@@ -550,14 +547,16 @@ def _capture_generation(
550547
"$ai_provider": run.provider,
551548
"$ai_model": run.model,
552549
"$ai_model_parameters": run.model_params,
553-
"$ai_input": with_privacy_mode(self._client, self._privacy_mode, run.input),
550+
"$ai_input": with_privacy_mode(
551+
self._ph_client, self._privacy_mode, run.input
552+
),
554553
"$ai_http_status": 200,
555554
"$ai_latency": run.latency,
556555
"$ai_base_url": run.base_url,
557556
}
558557
if run.tools:
559558
event_properties["$ai_tools"] = with_privacy_mode(
560-
self._client,
559+
self._ph_client,
561560
self._privacy_mode,
562561
run.tools,
563562
)
@@ -589,7 +588,7 @@ def _capture_generation(
589588
_extract_raw_esponse(generation) for generation in generation_result
590589
]
591590
event_properties["$ai_output_choices"] = with_privacy_mode(
592-
self._client, self._privacy_mode, completions
591+
self._ph_client, self._privacy_mode, completions
593592
)
594593

595594
if self._properties:
@@ -598,7 +597,7 @@ def _capture_generation(
598597
if self._distinct_id is None:
599598
event_properties["$process_person_profile"] = False
600599

601-
self._client.capture(
600+
self._ph_client.capture(
602601
distinct_id=self._distinct_id or trace_id,
603602
event="$ai_generation",
604603
properties=event_properties,

posthog/test/ai/langchain/test_callbacks.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1727,3 +1727,29 @@ def test_openai_reasoning_tokens(mock_client):
17271727
assert call["properties"]["$ai_reasoning_tokens"] is not None
17281728
assert call["properties"]["$ai_input_tokens"] is not None
17291729
assert call["properties"]["$ai_output_tokens"] is not None
1730+
1731+
1732+
def test_callback_handler_without_client():
1733+
"""Test that CallbackHandler works properly when no PostHog client is passed."""
1734+
with patch("posthog.ai.langchain.callbacks.setup") as mock_setup:
1735+
mock_client = mock_setup.return_value
1736+
1737+
callbacks = CallbackHandler()
1738+
1739+
# Verify that setup() was called
1740+
mock_setup.assert_called_once()
1741+
1742+
# Verify that the client was set to the result of setup()
1743+
assert callbacks._ph_client == mock_client
1744+
1745+
# Test that the callback handler works with a simple chain
1746+
prompt = ChatPromptTemplate.from_messages([("user", "Foo")])
1747+
model = FakeMessagesListChatModel(responses=[AIMessage(content="Bar")])
1748+
chain = prompt | model
1749+
1750+
# This should work and call the mock client
1751+
result = chain.invoke({}, config={"callbacks": [callbacks]})
1752+
assert result.content == "Bar"
1753+
1754+
# Verify that the mock client was used for capturing events
1755+
assert mock_client.capture.call_count == 3

0 commit comments

Comments
 (0)