* Fixed a few TC
* Added a few Pydantic classes to
`flake8-type-checking.runtime-evaluated-base-classes` (not as much as I
would have imagined)
* Added a few `noqa: TC`
* Activated TC rules
Moves hex color validation regex from inside
`_render_mermaid_using_api()` to module-level constant
`_HEX_COLOR_PATTERN`. This avoids recompiling the regex on every
function call, improving performance when rendering multiple Mermaid
graphs.
**Testing:**
- `make lint` passes
- `make test` passes
---------
Co-authored-by: Mason Daugherty <github@mdrxy.com>
## Summary
Fixes#34247
When using `Annotated[type, Field(description="...")]` syntax with the
`@tool` decorator, field descriptions were being lost during schema
generation. The `_get_annotation_description()` function only checked
for string annotations but not for Pydantic `FieldInfo` objects.
## Changes
- Extended `_get_annotation_description()` to also extract descriptions
from `FieldInfo` objects within `Annotated` types
- Added import for `pydantic.fields.FieldInfo`
- Added unit test to verify `Field(description=...)` is preserved
## Why this approach
The fix is minimal and targeted - it extends the existing description
extraction logic rather than restructuring the schema generation. This
maintains backward compatibility while supporting both annotation
styles:
```python
# Both now work correctly:
topic: Annotated[str, "The research topic"] # existing
topic: Annotated[str, Field(description="...")] # now fixed
```
## Known limitation
This fix only handles `pydantic.fields.FieldInfo` (Pydantic v2). The v1
compatibility layer (`pydantic.v1.fields.FieldInfo`) is a different
class and will not have descriptions extracted. This is intentional:
- Pydantic v1 is deprecated; users should migrate to v2
- The v1 compat layer exists for legacy model migration, not new tool
definitions
- Duck-typing on `description` attribute could match unintended objects
If v1 `Field` support is needed, it can be addressed in a follow-up PR
with explicit handling.
## Testing
- Added `test_tool_field_description_preserved()` covering required and
optional params
- Verified existing `test_tool_annotated_descriptions` still passes
- Lint and type checks pass
---
> [!NOTE]
> This PR was developed with AI agent assistance (Factory/Droid).
---------
Co-authored-by: Mason Daugherty <github@mdrxy.com>
## Summary
- Fixes issue where Pydantic default values from `args_schema` were not
passed to tool functions when the caller omits optional arguments
- Modified `_parse_input()` in `libs/core/langchain_core/tools/base.py`
to include fields with non-None defaults
- Added unit tests to verify default args behavior for both sync and
async tools
## Problem
When a tool has an `args_schema` with default values:
```python
class SearchArgs(BaseModel):
query: str = Field(..., description="Search query")
page: int = Field(default=1, description="Page number")
size: int = Field(default=10, description="Results per page")
@tool("search", args_schema=SearchArgs)
def search_tool(query: str, page: int, size: int) -> str:
return f"query={query}, page={page}, size={size}"
# This threw: TypeError: search_tool() missing 2 required positional arguments
search_tool.invoke({"query": "test"})
```
The defaults from `args_schema` were being discarded because
`_parse_input()` filtered validated results to only include keys from
the original input.
## Solution
Changed the filtering logic to:
1. Include all fields that were in the original input (validated)
2. Also include fields with non-None defaults from the Pydantic schema
This applies user-defined defaults (like `Field(default=1)`) while
excluding synthetic fields from `*args`/`**kwargs` which have
`default=None`.
## Test plan
- [x] Added `test_tool_args_schema_default_values` - tests sync tool
with defaults
- [x] Added `test_tool_args_schema_default_values_async` - tests async
tool with defaults
- [x] All existing tests pass (150 passed, 4 skipped)
- [x] Lint passes
Fixes#34384
---------
Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
## Summary
Fixes#33970
`get_buffer_string` was only checking for the deprecated `function_call`
field in `additional_kwargs`, which modern LLM providers no longer
return. This fix updates the function to check for the modern
`tool_calls` field first, falling back to `function_call` for legacy
compatibility.
## Changes
- Check `AIMessage.tool_calls` first (modern standard)
- Fall back to `additional_kwargs["function_call"]` (legacy support)
- Added 3 unit tests covering tool_calls, empty content, and precedence
behavior
## Testing
```python
# Before fix: tool_calls info was lost
msg = AIMessage(content="Hi", tool_calls=[{"name": "search", ...}])
get_buffer_string([msg]) # "AI: Hi" (no tool info)
# After fix: tool_calls are included
get_buffer_string([msg]) # "AI: Hi[{\"name\": \"search\", ...}]"
```
- All existing `get_buffer_string` tests pass
- Legacy `function_call` behavior preserved
---
> [!NOTE]
> This PR was developed with AI agent assistance (Factory/Droid).
---------
Co-authored-by: Mason Daugherty <github@mdrxy.com>
Adds [PEP 702](https://peps.python.org/pep-0702/) `__deprecated__`
attribute support to the `@deprecated` decorator, enabling IDE and type
checker integration for deprecation warnings.
---
PEP 702 introduced the `__deprecated__` attribute convention, which type
checkers (Pyright, mypy) and IDEs (VS Code with Pylance, PyCharm) can
use to surface deprecations directly in the editor. This PR sets
`__deprecated__` on all objects decorated with `@deprecated`.
With this change, developers using supported IDEs will see:
- **Strikethrough text** on deprecated symbols
- **Hover messages** showing the deprecation reason and suggested
alternative
- **Diagnostic warnings** during type checking (e.g., `pyright`, `mypy`)
### References
- [PEP 702 – Marking deprecations using the type
system](https://peps.python.org/pep-0702/)
- [`typing.deprecated`
specification](https://typing.python.org/en/latest/spec/directives.html#deprecated)
Adds automatic tool call counting to tracing by means of a new
`store_tool_call_count_in_run()`, which calls on newly added
`count_tool_calls_in_run()`.
Runs on successful LLM completion. Does not run on errored runs.
### Description:
earlier we have to use like below:
```python
from langchain_core.messages import trim_messages
from langchain_core.messages.utils import count_tokens_approximately
trim_messages(..., token_counter=count_tokens_approximately)
```
Now can be used as like this also
```python
from langchain_core.messages import trim_messages
trim_messages(..., token_counter="approximate")
```
- [x] **Added tests**
- [x] **Lint and test**: Run this as I made change in langchain/core, uv
run --group test pytest tests/unit_tests/messages/test_utils.py -v
<img width="1006" height="66" alt="image"
src="https://github.com/user-attachments/assets/c6938c29-a781-4e7f-871b-8e888ee764b7"
/>
---------
Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
Adds `usage_metadata` (token counts, etc.) to the run metadata in
`LangChainTracer`.
When an LLM run ends, usage metadata is extracted from all generations
and aggregated using the existing `add_usage` helper, then stored in
`run.extra["metadata"]["usage_metadata"]`.
The original data in outputs remains unchanged.
Also, see #34415
---------
Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
ref https://github.com/langchain-ai/langchainjs/pull/9665
Fixes trace persistence for iterator/generator inputs (like
`RunnableGenerator`) where the full input isn't available at chain
start. Instead of POSTing a run with incomplete inputs on start and
PATCHing later, this defers the POST until chain end when inputs are
fully realized.
---------
Co-authored-by: Mason Daugherty <github@mdrxy.com>
Replace direct `__annotations__` access with `get_type_hints()` in
`_convert_any_typed_dicts_to_pydantic` to handle [PEP
649](https://peps.python.org/pep-0649/) deferred annotations in Python
3.14:
> [`Changed in version 3.14: Annotations are now lazily evaluated by
default`](https://docs.python.org/3/reference/compound_stmts.html#annotations)
Before:
```python
class MyTool(TypedDict):
name: str
MyTool.__annotations__ # {'name': 'str'} - string, not type
issubclass('str', ...) # TypeError: arg 1 must be a class
```
After:
```python
get_type_hints(MyTool) # {'name': <class 'str'>} - actual type
```
Fixes#34291
Added test that fails on `master`.
`ToolNode` uses `get_type_hints` which doesn't work properly w/ partial
funcs on Python 3.12+
The diff here is nice anyways when we inline the logic.
## Summary
When invoking a tool with a `ToolCall`, the `tool_call_id` is extracted
but was **not forwarded** to callback handlers in `on_tool_start`. This
made it impossible for callback handlers to correlate tool executions
with the original LLM tool calls.
This fix adds `tool_call_id=tool_call_id` to both:
- Sync `run()` method's `on_tool_start` call
- Async `arun()` method's `on_tool_start` call
## Changes
- **`libs/core/langchain_core/tools/base.py`**: Added `tool_call_id`
parameter to `on_tool_start` calls (2 lines)
- **`libs/core/tests/unit_tests/test_tools.py`**: Added 6 comprehensive
tests covering:
- Sync tool invocation via `invoke()`
- Async tool invocation via `ainvoke()`
- `tool_call_id` is `None` when invoked without a ToolCall
- Empty string `tool_call_id` edge case
- Direct `run()` method
- Direct `arun()` method
## Test plan
- [x] All 147 existing tests pass
- [x] 6 new tests added and passing
- [x] Linting passes
Fixes#34168
---
This PR was developed with AI assistance (Claude).
---------
Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
With this we get the correct types for `_runnable_support` annotated
functions.
* return list[BaseMessage] when messages is not None
* return Runnable when messages is None
* typing of function args
# PR Title: fix(core): prevent async task garbage collection (RUF006)
## Description
This PR addresses a cryptic issue (flagged by Ruff rule RUF006) where
`asyncio` tasks created via `loop.create_task` could be garbage
collected mid-execution because no strong reference was maintained.
In `libs/core/langchain_core/language_models/llms.py`, the retry
decorator's `_before_sleep` hook creates a fire-and-forget task for
logging/callbacks. If the garbage collector runs before this task
completes, the task may be destroyed, leading to silent failures.
## Changes
- Introduced a module-level set `_background_tasks` to hold strong
references to running tasks.
- Updated `_before_sleep` to add new tasks to this set.
- Added a `done_callback` to remove the task from the set upon
completion, preventing memory leaks.
## Verification
- Verified logic with a standalone script to ensure tasks are
added/removed from the set correctly.
- This is a standard pattern recommended in the Python `asyncio`
documentation.
## Checklist
- [x] I have read the contributing guidelines.
- [x] I have run tests locally (logic verification).
---------
Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>