Skip to content

Commit a23a34f

Browse files
authored
fix(langchain): parse tool_calls from AIMessage (#1316)
1 parent c514628 commit a23a34f

File tree

4 files changed

+35
-257
lines changed

4 files changed

+35
-257
lines changed

langfuse/_client/client.py

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55

66
import logging
77
import os
8-
import warnings
98
import re
109
import urllib.parse
10+
import warnings
1111
from datetime import datetime
1212
from hashlib import sha256
1313
from time import time_ns
@@ -17,8 +17,8 @@
1717
List,
1818
Literal,
1919
Optional,
20-
Union,
2120
Type,
21+
Union,
2222
cast,
2323
overload,
2424
)
@@ -36,6 +36,13 @@
3636
from packaging.version import Version
3737

3838
from langfuse._client.attributes import LangfuseOtelSpanAttributes
39+
from langfuse._client.constants import (
40+
ObservationTypeGenerationLike,
41+
ObservationTypeLiteral,
42+
ObservationTypeLiteralNoEvent,
43+
ObservationTypeSpanLike,
44+
get_observation_types_list,
45+
)
3946
from langfuse._client.datasets import DatasetClient, DatasetItemClient
4047
from langfuse._client.environment_variables import (
4148
LANGFUSE_DEBUG,
@@ -47,25 +54,18 @@
4754
LANGFUSE_TRACING_ENABLED,
4855
LANGFUSE_TRACING_ENVIRONMENT,
4956
)
50-
from langfuse._client.constants import (
51-
ObservationTypeLiteral,
52-
ObservationTypeLiteralNoEvent,
53-
ObservationTypeGenerationLike,
54-
ObservationTypeSpanLike,
55-
get_observation_types_list,
56-
)
5757
from langfuse._client.resource_manager import LangfuseResourceManager
5858
from langfuse._client.span import (
59-
LangfuseEvent,
60-
LangfuseGeneration,
61-
LangfuseSpan,
6259
LangfuseAgent,
63-
LangfuseTool,
6460
LangfuseChain,
65-
LangfuseRetriever,
66-
LangfuseEvaluator,
6761
LangfuseEmbedding,
62+
LangfuseEvaluator,
63+
LangfuseEvent,
64+
LangfuseGeneration,
6865
LangfuseGuardrail,
66+
LangfuseRetriever,
67+
LangfuseSpan,
68+
LangfuseTool,
6969
)
7070
from langfuse._utils import _get_timestamp
7171
from langfuse._utils.parse_error import handle_fern_exception
@@ -2996,11 +2996,10 @@ def _url_encode(self, url: str, *, is_url_param: Optional[bool] = False) -> str:
29962996
# we need add safe="" to force escaping of slashes
29972997
# This is necessary for prompts in prompt folders
29982998
return urllib.parse.quote(url, safe="")
2999-
3000-
def clear_prompt_cache(self):
3001-
"""
3002-
Clear the entire prompt cache, removing all cached prompts.
3003-
2999+
3000+
def clear_prompt_cache(self) -> None:
3001+
"""Clear the entire prompt cache, removing all cached prompts.
3002+
30043003
This method is useful when you want to force a complete refresh of all
30053004
cached prompts, for example after major updates or when you need to
30063005
ensure the latest versions are fetched from the server.

langfuse/_utils/prompt_cache.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@
22

33
import atexit
44
import logging
5+
import os
56
from datetime import datetime
67
from queue import Empty, Queue
78
from threading import Thread
89
from typing import Callable, Dict, List, Optional, Set
9-
import os
1010

11-
from langfuse.model import PromptClient
1211
from langfuse._client.environment_variables import (
13-
LANGFUSE_PROMPT_CACHE_DEFAULT_TTL_SECONDS
12+
LANGFUSE_PROMPT_CACHE_DEFAULT_TTL_SECONDS,
1413
)
14+
from langfuse.model import PromptClient
1515

16-
DEFAULT_PROMPT_CACHE_TTL_SECONDS = int(os.getenv(LANGFUSE_PROMPT_CACHE_DEFAULT_TTL_SECONDS, 60))
16+
DEFAULT_PROMPT_CACHE_TTL_SECONDS = int(
17+
os.getenv(LANGFUSE_PROMPT_CACHE_DEFAULT_TTL_SECONDS, 60)
18+
)
1719

1820
DEFAULT_PROMPT_CACHE_REFRESH_WORKERS = 1
1921

langfuse/langchain/CallbackHandler.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -939,9 +939,17 @@ def __join_tags_and_metadata(
939939
def _convert_message_to_dict(self, message: BaseMessage) -> Dict[str, Any]:
940940
# assistant message
941941
if isinstance(message, HumanMessage):
942-
message_dict = {"role": "user", "content": message.content}
942+
message_dict: Dict[str, Any] = {"role": "user", "content": message.content}
943943
elif isinstance(message, AIMessage):
944944
message_dict = {"role": "assistant", "content": message.content}
945+
946+
if (
947+
hasattr(message, "tool_calls")
948+
and message.tool_calls is not None
949+
and len(message.tool_calls) > 0
950+
):
951+
message_dict["tool_calls"] = message.tool_calls
952+
945953
elif isinstance(message, SystemMessage):
946954
message_dict = {"role": "system", "content": message.content}
947955
elif isinstance(message, ToolMessage):

tests/test_prompt.py

Lines changed: 1 addition & 232 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
from langfuse.model import ChatPromptClient, TextPromptClient
1414
from tests.utils import create_uuid, get_api
1515

16-
import os
17-
from langfuse._client.environment_variables import LANGFUSE_PROMPT_CACHE_DEFAULT_TTL_SECONDS
1816

1917
def test_create_prompt():
2018
langfuse = Langfuse()
@@ -681,25 +679,11 @@ def test_prompt_end_to_end():
681679

682680
@pytest.fixture
683681
def langfuse():
684-
langfuse_instance = Langfuse(
685-
public_key="test-public-key",
686-
secret_key="test-secret-key",
687-
host="https://mock-host.com",
688-
)
682+
langfuse_instance = Langfuse()
689683
langfuse_instance.api = Mock()
690684

691685
return langfuse_instance
692686

693-
@pytest.fixture
694-
def langfuse_with_override_default_cache():
695-
langfuse_instance = Langfuse(
696-
public_key="test-public-key",
697-
secret_key="test-secret-key",
698-
host="https://mock-host.com",
699-
default_cache_ttl_seconds=OVERRIDE_DEFAULT_PROMPT_CACHE_TTL_SECONDS,
700-
)
701-
langfuse_instance.api = Mock()
702-
return langfuse_instance
703687

704688
# Fetching a new prompt when nothing in cache
705689
def test_get_fresh_prompt(langfuse):
@@ -1426,218 +1410,3 @@ def test_update_prompt():
14261410
expected_labels = sorted(["latest", "doe", "production", "john"])
14271411
assert sorted(fetched_prompt.labels) == expected_labels
14281412
assert sorted(updated_prompt.labels) == expected_labels
1429-
1430-
1431-
def test_environment_variable_override_prompt_cache_ttl():
1432-
"""Test that LANGFUSE_PROMPT_CACHE_DEFAULT_TTL_SECONDS environment variable overrides default TTL."""
1433-
import os
1434-
from unittest.mock import patch
1435-
1436-
# Set environment variable to override default TTL
1437-
os.environ[LANGFUSE_PROMPT_CACHE_DEFAULT_TTL_SECONDS] = "120"
1438-
1439-
# Create a new Langfuse instance to pick up the environment variable
1440-
langfuse = Langfuse(
1441-
public_key="test-public-key",
1442-
secret_key="test-secret-key",
1443-
host="https://mock-host.com",
1444-
)
1445-
langfuse.api = Mock()
1446-
1447-
prompt_name = "test_env_override_ttl"
1448-
prompt = Prompt_Text(
1449-
name=prompt_name,
1450-
version=1,
1451-
prompt="Test prompt with env override",
1452-
type="text",
1453-
labels=[],
1454-
config={},
1455-
tags=[],
1456-
)
1457-
prompt_client = TextPromptClient(prompt)
1458-
1459-
mock_server_call = langfuse.api.prompts.get
1460-
mock_server_call.return_value = prompt
1461-
1462-
# Mock time to control cache expiration
1463-
with patch.object(PromptCacheItem, "get_epoch_seconds") as mock_time:
1464-
mock_time.return_value = 0
1465-
1466-
# First call - should cache the prompt
1467-
result1 = langfuse.get_prompt(prompt_name)
1468-
assert mock_server_call.call_count == 1
1469-
assert result1 == prompt_client
1470-
1471-
# Check that prompt is cached
1472-
cached_item = langfuse._resources.prompt_cache.get(
1473-
langfuse._resources.prompt_cache.generate_cache_key(prompt_name, version=None, label=None)
1474-
)
1475-
assert cached_item is not None
1476-
assert cached_item.value == prompt_client
1477-
1478-
# Debug: check the cache item's expiry time
1479-
print(f"DEBUG: Cache item expiry: {cached_item._expiry}")
1480-
print(f"DEBUG: Current mock time: {mock_time.return_value}")
1481-
print(f"DEBUG: Is expired? {cached_item.is_expired()}")
1482-
1483-
# Set time to 60 seconds (before new TTL of 120 seconds)
1484-
mock_time.return_value = 60
1485-
1486-
# Second call - should still use cache
1487-
result2 = langfuse.get_prompt(prompt_name)
1488-
assert mock_server_call.call_count == 1 # No new server call
1489-
assert result2 == prompt_client
1490-
1491-
# Set time to 120 seconds (at TTL expiration)
1492-
mock_time.return_value = 120
1493-
1494-
# Third call - should still use cache (stale cache behavior)
1495-
result3 = langfuse.get_prompt(prompt_name)
1496-
assert result3 == prompt_client
1497-
1498-
# Wait for background refresh to complete
1499-
while True:
1500-
if langfuse._resources.prompt_cache._task_manager.active_tasks() == 0:
1501-
break
1502-
sleep(0.1)
1503-
1504-
# Should have made a new server call for refresh
1505-
assert mock_server_call.call_count == 2
1506-
1507-
# Set time to 121 seconds (after TTL expiration)
1508-
mock_time.return_value = 121
1509-
1510-
# Fourth call - should use refreshed cache
1511-
result4 = langfuse.get_prompt(prompt_name)
1512-
assert result4 == prompt_client
1513-
1514-
# Clean up environment variable
1515-
if LANGFUSE_PROMPT_CACHE_DEFAULT_TTL_SECONDS in os.environ:
1516-
del os.environ[LANGFUSE_PROMPT_CACHE_DEFAULT_TTL_SECONDS]
1517-
1518-
1519-
@patch.object(PromptCacheItem, "get_epoch_seconds")
1520-
def test_default_ttl_when_environment_variable_not_set(mock_time):
1521-
"""Test that default 60-second TTL is used when LANGFUSE_PROMPT_CACHE_DEFAULT_TTL_SECONDS is not set."""
1522-
from unittest.mock import patch
1523-
1524-
# Ensure environment variable is not set
1525-
if LANGFUSE_PROMPT_CACHE_DEFAULT_TTL_SECONDS in os.environ:
1526-
del os.environ[LANGFUSE_PROMPT_CACHE_DEFAULT_TTL_SECONDS]
1527-
1528-
# Set initial time to 0
1529-
mock_time.return_value = 0
1530-
1531-
# Create a new Langfuse instance to pick up the default TTL
1532-
langfuse = Langfuse(
1533-
public_key="test-public-key",
1534-
secret_key="test-secret-key",
1535-
host="https://mock-host.com",
1536-
)
1537-
langfuse.api = Mock()
1538-
1539-
prompt_name = "test_default_ttl"
1540-
prompt = Prompt_Text(
1541-
name=prompt_name,
1542-
version=1,
1543-
prompt="Test prompt with default TTL",
1544-
type="text",
1545-
labels=[],
1546-
config={},
1547-
tags=[],
1548-
)
1549-
prompt_client = TextPromptClient(prompt)
1550-
1551-
mock_server_call = langfuse.api.prompts.get
1552-
mock_server_call.return_value = prompt
1553-
1554-
# First call - should cache the prompt
1555-
result1 = langfuse.get_prompt(prompt_name)
1556-
assert mock_server_call.call_count == 1
1557-
assert result1 == prompt_client
1558-
1559-
# Check that prompt is cached
1560-
cached_item = langfuse._resources.prompt_cache.get(
1561-
langfuse._resources.prompt_cache.generate_cache_key(prompt_name, version=None, label=None)
1562-
)
1563-
assert cached_item is not None
1564-
assert cached_item.value == prompt_client
1565-
1566-
# Set time to just before default TTL expiration
1567-
mock_time.return_value = DEFAULT_PROMPT_CACHE_TTL_SECONDS - 1
1568-
1569-
# Second call - should still use cache
1570-
result2 = langfuse.get_prompt(prompt_name)
1571-
assert mock_server_call.call_count == 1 # No new server call
1572-
assert result2 == prompt_client
1573-
1574-
# Set time to just after default TTL expiration to trigger cache expiry
1575-
# Use the actual DEFAULT_PROMPT_CACHE_TTL_SECONDS value that was imported
1576-
mock_time.return_value = DEFAULT_PROMPT_CACHE_TTL_SECONDS + 1
1577-
1578-
# Third call - should still use cache (stale cache behavior)
1579-
result3 = langfuse.get_prompt(prompt_name)
1580-
assert result3 == prompt_client
1581-
1582-
# Wait for background refresh to complete
1583-
while True:
1584-
if langfuse._resources.prompt_cache._task_manager.active_tasks() == 0:
1585-
break
1586-
sleep(0.1)
1587-
1588-
# Should have made a new server call for refresh
1589-
assert mock_server_call.call_count == 2
1590-
1591-
# Set time to just after default TTL expiration
1592-
mock_time.return_value = DEFAULT_PROMPT_CACHE_TTL_SECONDS + 1
1593-
1594-
# Fourth call - should use refreshed cache
1595-
result4 = langfuse.get_prompt(prompt_name)
1596-
assert result4 == prompt_client
1597-
1598-
1599-
def test_clear_prompt_cache(langfuse):
1600-
"""Test clearing the entire prompt cache."""
1601-
prompt_name = create_uuid()
1602-
1603-
# Mock the API calls
1604-
mock_prompt = Prompt_Text(
1605-
name=prompt_name,
1606-
version=1,
1607-
prompt="test prompt",
1608-
type="text",
1609-
labels=["production"],
1610-
config={},
1611-
tags=[],
1612-
)
1613-
1614-
# Mock the create_prompt API call
1615-
langfuse.api.prompts.create.return_value = mock_prompt
1616-
1617-
# Mock the get_prompt API call
1618-
langfuse.api.prompts.get.return_value = mock_prompt
1619-
1620-
# Create a prompt and cache it
1621-
prompt_client = langfuse.create_prompt(
1622-
name=prompt_name,
1623-
prompt="test prompt",
1624-
labels=["production"],
1625-
)
1626-
1627-
# Get the prompt to cache it
1628-
cached_prompt = langfuse.get_prompt(prompt_name)
1629-
assert cached_prompt.name == prompt_name
1630-
1631-
# Verify that the prompt is in the cache
1632-
cache_key = f"{prompt_name}-label:production"
1633-
assert langfuse._resources.prompt_cache.get(cache_key) is not None, "Prompt should be in cache before clearing"
1634-
1635-
# Clear the entire prompt cache
1636-
langfuse.clear_prompt_cache()
1637-
1638-
# Verify cache is completely cleared
1639-
assert langfuse._resources.prompt_cache.get(cache_key) is None, "Prompt should be removed from cache after clearing"
1640-
1641-
# Verify data integrity
1642-
assert prompt_client.name == prompt_name
1643-
assert cached_prompt.name == prompt_name

0 commit comments

Comments
 (0)