Compare commits

..

9 Commits

Author SHA1 Message Date
Eugene Yurtsev
03241dc80c x 2026-04-16 13:51:45 -04:00
Eugene Yurtsev
9c4de86bca Merge branch 'master' into jacob/traceablemetadata 2026-04-16 13:40:26 -04:00
ccurme
92a6e57d60 release(langchain-classic): 1.0.4 (#36827) 2026-04-16 12:55:30 -04:00
ccurme
1749eb8601 chore(langchain-classic): add deprecations (#36826) 2026-04-16 12:38:18 -04:00
ccurme
58c4e5bbdd release(text-splitters): 1.1.2 (#36822) 2026-04-16 10:18:05 -04:00
ccurme
c289bf10e9 fix(text-splitters): deprecate and use SSRF-safe transport in split_text_from_url (#36821) 2026-04-16 10:13:31 -04:00
jacoblee93
bc0e99d045 Remove test 2026-04-15 15:39:31 -07:00
jacoblee93
91b1ef049c Feedback 2026-04-15 15:10:16 -07:00
jacoblee93
c993ba06bb Add chat model and LLM invocation params to traceable metadata 2026-04-15 13:43:41 -07:00
11 changed files with 292 additions and 107 deletions

View File

@@ -2,6 +2,7 @@ import re
from collections.abc import Sequence
from typing import (
TYPE_CHECKING,
Any,
Literal,
TypedDict,
TypeVar,
@@ -14,6 +15,21 @@ from langchain_core.messages.content import (
)
def _filter_invocation_params_for_tracing(params: dict[str, Any]) -> dict[str, Any]:
"""Filter out large/inappropriate fields from invocation params for tracing.
Removes fields like tools, functions, messages, response_format that can be large.
Args:
params: The invocation parameters to filter.
Returns:
The filtered parameters with large fields removed.
"""
excluded_keys = {"tools", "functions", "messages", "response_format"}
return {k: v for k, v in params.items() if k not in excluded_keys}
def is_openai_data_block(
block: dict, filter_: Literal["image", "audio", "file"] | None = None
) -> bool:

View File

@@ -25,6 +25,7 @@ from langchain_core.callbacks import (
)
from langchain_core.globals import get_llm_cache
from langchain_core.language_models._utils import (
_filter_invocation_params_for_tracing,
_normalize_messages,
_update_message_content_to_blocks,
)
@@ -567,6 +568,9 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
self.tags,
inheritable_metadata,
self.metadata,
langsmith_inheritable_metadata=_filter_invocation_params_for_tracing(
params
),
)
(run_manager,) = callback_manager.on_chat_model_start(
self._serialized,
@@ -695,6 +699,9 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
self.tags,
inheritable_metadata,
self.metadata,
langsmith_inheritable_metadata=_filter_invocation_params_for_tracing(
params
),
)
(run_manager,) = await callback_manager.on_chat_model_start(
self._serialized,
@@ -972,6 +979,9 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
self.tags,
inheritable_metadata,
self.metadata,
langsmith_inheritable_metadata=_filter_invocation_params_for_tracing(
params
),
)
messages_to_trace = [
_format_for_tracing(message_list) for message_list in messages
@@ -1095,6 +1105,9 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
self.tags,
inheritable_metadata,
self.metadata,
langsmith_inheritable_metadata=_filter_invocation_params_for_tracing(
params
),
)
messages_to_trace = [

View File

@@ -42,6 +42,7 @@ from langchain_core.callbacks import (
Callbacks,
)
from langchain_core.globals import get_llm_cache
from langchain_core.language_models._utils import _filter_invocation_params_for_tracing
from langchain_core.language_models.base import (
BaseLanguageModel,
LangSmithParams,
@@ -537,6 +538,9 @@ class BaseLLM(BaseLanguageModel[str], ABC):
self.tags,
inheritable_metadata,
self.metadata,
langsmith_inheritable_metadata=_filter_invocation_params_for_tracing(
params
),
)
(run_manager,) = callback_manager.on_llm_start(
self._serialized,
@@ -607,6 +611,9 @@ class BaseLLM(BaseLanguageModel[str], ABC):
self.tags,
inheritable_metadata,
self.metadata,
langsmith_inheritable_metadata=_filter_invocation_params_for_tracing(
params
),
)
(run_manager,) = await callback_manager.on_llm_start(
self._serialized,
@@ -950,6 +957,8 @@ class BaseLLM(BaseLanguageModel[str], ABC):
run_name_list = run_name or cast(
"list[str | None]", ([None] * len(prompts))
)
params = self.dict()
params["stop"] = stop
callback_managers = [
CallbackManager.configure(
callback,
@@ -959,6 +968,9 @@ class BaseLLM(BaseLanguageModel[str], ABC):
self.tags,
meta,
self.metadata,
langsmith_inheritable_metadata=_filter_invocation_params_for_tracing(
params
),
)
for callback, tag, meta in zip(
callbacks, tags_list, metadata_list, strict=False
@@ -966,6 +978,8 @@ class BaseLLM(BaseLanguageModel[str], ABC):
]
else:
# We've received a single callbacks arg to apply to all inputs
params = self.dict()
params["stop"] = stop
callback_managers = [
CallbackManager.configure(
cast("Callbacks", callbacks),
@@ -975,12 +989,13 @@ class BaseLLM(BaseLanguageModel[str], ABC):
self.tags,
cast("dict[str, Any]", metadata),
self.metadata,
langsmith_inheritable_metadata=_filter_invocation_params_for_tracing(
params
),
)
] * len(prompts)
run_name_list = [cast("str | None", run_name)] * len(prompts)
run_ids_list = self._get_run_ids_list(run_id, prompts)
params = self.dict()
params["stop"] = stop
options = {"stop": stop}
(
existing_prompts,
@@ -1214,6 +1229,8 @@ class BaseLLM(BaseLanguageModel[str], ABC):
run_name_list = run_name or cast(
"list[str | None]", ([None] * len(prompts))
)
params = self.dict()
params["stop"] = stop
callback_managers = [
AsyncCallbackManager.configure(
callback,
@@ -1223,6 +1240,9 @@ class BaseLLM(BaseLanguageModel[str], ABC):
self.tags,
meta,
self.metadata,
langsmith_inheritable_metadata=_filter_invocation_params_for_tracing(
params
),
)
for callback, tag, meta in zip(
callbacks, tags_list, metadata_list, strict=False
@@ -1230,6 +1250,8 @@ class BaseLLM(BaseLanguageModel[str], ABC):
]
else:
# We've received a single callbacks arg to apply to all inputs
params = self.dict()
params["stop"] = stop
callback_managers = [
AsyncCallbackManager.configure(
cast("Callbacks", callbacks),
@@ -1239,12 +1261,13 @@ class BaseLLM(BaseLanguageModel[str], ABC):
self.tags,
cast("dict[str, Any]", metadata),
self.metadata,
langsmith_inheritable_metadata=_filter_invocation_params_for_tracing(
params
),
)
] * len(prompts)
run_name_list = [cast("str | None", run_name)] * len(prompts)
run_ids_list = self._get_run_ids_list(run_id, prompts)
params = self.dict()
params["stop"] = stop
options = {"stop": stop}
(
existing_prompts,

View File

@@ -17,8 +17,14 @@ from langchain_core.language_models import (
FakeListChatModel,
ParrotFakeChatModel,
)
from langchain_core.language_models._utils import _normalize_messages
from langchain_core.language_models.chat_models import _generate_response_from_error
from langchain_core.language_models._utils import (
_filter_invocation_params_for_tracing,
_normalize_messages,
)
from langchain_core.language_models.chat_models import (
SimpleChatModel,
_generate_response_from_error,
)
from langchain_core.language_models.fake_chat_models import (
FakeListChatModelError,
GenericFakeChatModel,
@@ -1390,3 +1396,86 @@ def test_generate_response_from_error_handles_streaming_response_failure() -> No
assert metadata["body"] is None
assert metadata["headers"] == {"content-type": "application/json"}
assert metadata["status_code"] == 400
def test_filter_invocation_params_for_tracing() -> None:
"""Test that large fields are filtered from invocation params for tracing."""
params = {
"temperature": 0.7,
"tools": [{"name": "test_tool"}],
"functions": [{"name": "test_function"}],
"messages": [{"role": "system", "content": "test"}],
"response_format": {"type": "json_object"},
}
filtered = _filter_invocation_params_for_tracing(params)
# Should include temperature
assert "temperature" in filtered
assert filtered["temperature"] == 0.7
# Should exclude these large fields
assert "tools" not in filtered
assert "functions" not in filtered
assert "messages" not in filtered
assert "response_format" not in filtered
class FakeChatModelWithInvocationParams(SimpleChatModel):
"""Fake chat model with invocation params for testing tracing."""
temperature: float = 0.7
@property
@override
def _llm_type(self) -> str:
return "fake-chat-model-with-invocation-params"
@property
@override
def _identifying_params(self) -> dict[str, Any]:
return {
"temperature": self.temperature,
"tools": [{"name": "test_tool"}],
"functions": [{"name": "test_function"}],
"messages": [{"role": "system", "content": "test"}],
"response_format": {"type": "json_object"},
}
@override
def _call(
self,
messages: list[BaseMessage],
stop: list[str] | None = None,
run_manager: CallbackManagerForLLMRun | None = None,
**kwargs: Any,
) -> str:
return "test response"
def test_invocation_params_passed_to_tracer_metadata() -> None:
"""Test that invocation params are passed to tracer metadata."""
llm = FakeChatModelWithInvocationParams()
with collect_runs() as cb:
llm.invoke([HumanMessage(content="Hello")], config={"callbacks": [cb]})
assert len(cb.traced_runs) == 1
run = cb.traced_runs[0]
# The invocation params should be in the run's extra
assert run.extra == {
"batch_size": 1,
"invocation_params": {
"_type": "fake-chat-model-with-invocation-params",
"functions": [{"name": "test_function"}],
"messages": [{"content": "test", "role": "system"}],
"response_format": {"type": "json_object"},
"stop": None,
"temperature": 0.7,
"tools": [{"name": "test_tool"}],
},
"metadata": {
"ls_integration": "langchain_chat_model",
"ls_model_type": "chat",
"ls_provider": "fakechatmodelwithinvocationparams",
"ls_temperature": 0.7,
},
"options": {"stop": None},
}

View File

@@ -13,6 +13,7 @@ from langchain_core.language_models import (
BaseLLM,
FakeListLLM,
)
from langchain_core.language_models._utils import _filter_invocation_params_for_tracing
from langchain_core.outputs import Generation, GenerationChunk, LLMResult
from langchain_core.tracers.context import collect_runs
from tests.unit_tests.fake.callbacks import (
@@ -284,3 +285,94 @@ def test_get_ls_params() -> None:
ls_params = llm._get_ls_params(stop=["stop"])
assert ls_params["ls_stop"] == ["stop"]
def test_filter_invocation_params_for_tracing() -> None:
"""Test that large fields are filtered from invocation params for tracing."""
params = {
"temperature": 0.7,
"tools": [{"name": "test_tool"}],
"functions": [{"name": "test_function"}],
"messages": [{"role": "system", "content": "test"}],
"response_format": {"type": "json_object"},
}
filtered = _filter_invocation_params_for_tracing(params)
# Should include temperature
assert "temperature" in filtered
assert filtered["temperature"] == 0.7
# Should exclude these large fields
assert "tools" not in filtered
assert "functions" not in filtered
assert "messages" not in filtered
assert "response_format" not in filtered
class FakeLLMWithInvocationParams(BaseLLM):
"""Fake LLM with invocation params for testing tracing."""
temperature: float = 0.7
@property
@override
def _llm_type(self) -> str:
return "fake-llm-with-invocation-params"
@property
@override
def _identifying_params(self) -> dict[str, Any]:
return {
"temperature": self.temperature,
"tools": [{"name": "test_tool"}],
"functions": [{"name": "test_function"}],
"messages": [{"role": "system", "content": "test"}],
"response_format": {"type": "json_object"},
}
@override
def _generate(
self,
prompts: list[str],
stop: list[str] | None = None,
run_manager: CallbackManagerForLLMRun | None = None,
**kwargs: Any,
) -> LLMResult:
generations = [[Generation(text="test response")]]
return LLMResult(generations=generations)
@override
async def _agenerate(
self,
prompts: list[str],
stop: list[str] | None = None,
run_manager: AsyncCallbackManagerForLLMRun | None = None,
**kwargs: Any,
) -> LLMResult:
generations = [[Generation(text="test response")]]
return LLMResult(generations=generations)
async def test_llm_invocation_params_filtered_in_stream() -> None:
"""Test that invocation params are filtered when streaming."""
# Create a custom LLM that supports streaming
class FakeStreamingLLM(FakeLLMWithInvocationParams):
@override
def _stream(
self,
prompt: str,
stop: list[str] | None = None,
run_manager: CallbackManagerForLLMRun | None = None,
**kwargs: Any,
) -> Iterator[GenerationChunk]:
yield GenerationChunk(text="test ")
streaming_llm = FakeStreamingLLM()
with collect_runs() as cb:
list(streaming_llm.stream("Hello", config={"callbacks": [cb]}))
assert len(cb.traced_runs) == 1
run = cb.traced_runs[0]
# Verify the run was traced
assert run.extra is not None

View File

@@ -83,6 +83,14 @@ def _openapi_params_to_json_schema(params: list[Parameter], spec: OpenAPISpec) -
return {"type": "object", "properties": properties, "required": required}
@deprecated(
since="1.0.4",
message=(
"This function is deprecated and will be removed in a future version. "
"Use LLM tool calling features directly with an HTTP client instead."
),
removal="2.0",
)
def openapi_spec_to_openai_fn(
spec: OpenAPISpec,
) -> tuple[list[dict[str, Any]], Callable]:
@@ -198,6 +206,14 @@ def openapi_spec_to_openai_fn(
return functions, default_call_api
@deprecated(
since="1.0.4",
message=(
"This class is deprecated and will be removed in a future version. "
"Use LLM tool calling features directly with an HTTP client instead."
),
removal="2.0",
)
class SimpleRequestChain(Chain):
"""Chain for making a simple request to an API endpoint."""
@@ -252,11 +268,10 @@ class SimpleRequestChain(Chain):
@deprecated(
since="0.2.13",
message=(
"This function is deprecated and will be removed in langchain 1.0. "
"See API reference for replacement: "
"https://api.python.langchain.com/en/latest/chains/langchain.chains.openai_functions.openapi.get_openapi_chain.html"
"This function is deprecated and will be removed in a future version. "
"Use LLM tool calling features directly with an HTTP client instead."
),
removal="1.0",
removal="2.0",
)
def get_openapi_chain(
spec: OpenAPISpec | str,
@@ -271,82 +286,9 @@ def get_openapi_chain(
) -> SequentialChain:
r"""Create a chain for querying an API from a OpenAPI spec.
Note: this class is deprecated. See below for a replacement implementation.
The benefits of this implementation are:
- Uses LLM tool calling features to encourage properly-formatted API requests;
- Includes async support.
```python
from typing import Any
from langchain_classic.chains.openai_functions.openapi import openapi_spec_to_openai_fn
from langchain_community.utilities.openapi import OpenAPISpec
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
# Define API spec. Can be JSON or YAML
api_spec = \"\"\"
{
"openapi": "3.1.0",
"info": {
"title": "JSONPlaceholder API",
"version": "1.0.0"
},
"servers": [
{
"url": "https://jsonplaceholder.typicode.com"
}
],
"paths": {
"/posts": {
"get": {
"summary": "Get posts",
"parameters": [
{
"name": "_limit",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"example": 2
},
"description": "Limit the number of results"
}
]
}
}
}
}
\"\"\"
parsed_spec = OpenAPISpec.from_text(api_spec)
openai_fns, call_api_fn = openapi_spec_to_openai_fn(parsed_spec)
tools = [
{"type": "function", "function": fn}
for fn in openai_fns
]
prompt = ChatPromptTemplate.from_template(
"Use the provided APIs to respond to this user query:\\n\\n{query}"
)
model = ChatOpenAI(model="gpt-4o-mini", temperature=0).bind_tools(tools)
def _execute_tool(message) -> Any:
if tool_calls := message.tool_calls:
tool_call = message.tool_calls[0]
response = call_api_fn(name=tool_call["name"], fn_args=tool_call["args"])
response.raise_for_status()
return response.json()
else:
return message.content
chain = prompt | model | _execute_tool
```
```python
response = chain.invoke({"query": "Get me top two posts."})
```
!!! warning "Deprecated"
This function and all related utilities in this module are deprecated.
Use LLM tool calling features directly with an HTTP client instead.
Args:
spec: OpenAPISpec or url/file/text string corresponding to one.
@@ -360,7 +302,7 @@ def get_openapi_chain(
llm_chain_kwargs: LLM chain additional keyword arguments.
**kwargs: Additional keyword arguments to pass to the chain.
""" # noqa: E501
"""
try:
from langchain_community.utilities.openapi import OpenAPISpec
except ImportError as e:

View File

@@ -20,11 +20,11 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules",
]
version = "1.0.3"
version = "1.0.4"
requires-python = ">=3.10.0,<4.0.0"
dependencies = [
"langchain-core>=1.2.19,<2.0.0",
"langchain-text-splitters>=1.1.1,<2.0.0",
"langchain-core>=1.2.31,<2.0.0",
"langchain-text-splitters>=1.1.2,<2.0.0",
"langsmith>=0.1.17,<1.0.0",
"pydantic>=2.7.4,<3.0.0",
"SQLAlchemy>=1.4.0,<3.0.0",

View File

@@ -1,5 +1,5 @@
version = 1
revision = 3
revision = 2
requires-python = ">=3.10.0, <4.0.0"
resolution-markers = [
"python_full_version >= '3.14' and platform_python_implementation == 'PyPy'",
@@ -2391,7 +2391,7 @@ wheels = [
[[package]]
name = "langchain-classic"
version = "1.0.3"
version = "1.0.4"
source = { editable = "." }
dependencies = [
{ name = "async-timeout", marker = "python_full_version < '3.11'" },
@@ -2784,7 +2784,7 @@ wheels = [
[[package]]
name = "langchain-openai"
version = "1.1.13"
version = "1.1.14"
source = { editable = "../partners/openai" }
dependencies = [
{ name = "langchain-core" },
@@ -2891,7 +2891,7 @@ typing = [
[[package]]
name = "langchain-text-splitters"
version = "1.1.1"
version = "1.1.2"
source = { editable = "../text-splitters" }
dependencies = [
{ name = "langchain-core" },

View File

@@ -15,8 +15,7 @@ from typing import (
cast,
)
import requests
from langchain_core._api import beta
from langchain_core._api import beta, deprecated
from langchain_core.documents import BaseDocumentTransformer, Document
from typing_extensions import override
@@ -186,8 +185,19 @@ class HTMLHeaderTextSplitter:
"""
return self.split_text_from_file(StringIO(text))
@deprecated(
since="1.1.2",
removal="2.0.0",
message=(
"Please fetch the HTML content from the URL yourself and pass it "
"to split_text."
),
)
def split_text_from_url(
self, url: str, timeout: int = 10, **kwargs: Any
self,
url: str,
timeout: int = 10,
**kwargs: Any, # noqa: ARG002
) -> list[Document]:
"""Fetch text content from a URL and split it into documents.
@@ -205,14 +215,14 @@ class HTMLHeaderTextSplitter:
Raises:
requests.RequestException: If the HTTP request fails.
"""
from langchain_core._security._ssrf_protection import ( # noqa: PLC0415
validate_safe_url,
from langchain_core._security._transport import ( # noqa: PLC0415
ssrf_safe_client,
)
validate_safe_url(url, allow_private=False, allow_http=True)
response = requests.get(url, timeout=timeout, **kwargs)
response.raise_for_status()
return self.split_text(response.text)
with ssrf_safe_client() as client:
response = client.get(url, timeout=timeout)
response.raise_for_status()
return self.split_text(response.text)
def split_text_from_file(self, file: str | IO[str]) -> list[Document]:
"""Split HTML content from a file into a list of `Document` objects.

View File

@@ -22,10 +22,10 @@ classifiers = [
"Topic :: Text Processing",
]
version = "1.1.1"
version = "1.1.2"
requires-python = ">=3.10.0,<4.0.0"
dependencies = [
"langchain-core>=1.2.13,<2.0.0",
"langchain-core>=1.2.31,<2.0.0",
]
[project.urls]

View File

@@ -1,5 +1,5 @@
version = 1
revision = 3
revision = 2
requires-python = ">=3.10.0, <4.0.0"
resolution-markers = [
"python_full_version >= '3.14'",
@@ -1246,7 +1246,7 @@ typing = [
[[package]]
name = "langchain-text-splitters"
version = "1.1.1"
version = "1.1.2"
source = { editable = "." }
dependencies = [
{ name = "langchain-core" },