libs/core/langchain_core/messages/ai.py PYTHON 854 lines View on github.com → Search inside
1"""AI message."""23import itertools4import json5import logging6import operator7from collections.abc import Sequence8from typing import Any, Literal, cast, overload910from pydantic import Field, model_validator11from typing_extensions import NotRequired, Self, TypedDict, override1213from langchain_core.messages import content as types14from langchain_core.messages.base import (15    BaseMessage,16    BaseMessageChunk,17    _extract_reasoning_from_additional_kwargs,18    merge_content,19)20from langchain_core.messages.content import InvalidToolCall21from langchain_core.messages.tool import (22    ToolCall,23    ToolCallChunk,24    default_tool_chunk_parser,25    default_tool_parser,26)27from langchain_core.messages.tool import invalid_tool_call as create_invalid_tool_call28from langchain_core.messages.tool import tool_call as create_tool_call29from langchain_core.messages.tool import tool_call_chunk as create_tool_call_chunk30from langchain_core.utils._merge import merge_dicts, merge_lists31from langchain_core.utils.json import parse_partial_json32from langchain_core.utils.usage import _dict_int_op33from langchain_core.utils.utils import LC_AUTO_PREFIX, LC_ID_PREFIX3435logger = logging.getLogger(__name__)363738class InputTokenDetails(TypedDict, total=False):39    """Breakdown of input token counts.4041    Does *not* need to sum to full input token count. Does *not* need to have all keys.4243    Example:44        ```python45        {46            "audio": 10,47            "cache_creation": 200,48            "cache_read": 100,49        }50        ```5152    May also hold extra provider-specific keys.5354    !!! version-added "Added in `langchain-core` 0.3.9"55    """5657    audio: int58    """Audio input tokens."""5960    cache_creation: int61    """Input tokens that were cached and there was a cache miss.6263    Since there was a cache miss, the cache was created from these tokens.64    """6566    cache_read: int67    """Input tokens that were cached and there was a cache hit.6869    Since there was a cache hit, the tokens were read from the cache. More precisely,70    the model state given these tokens was read from the cache.71    """727374class OutputTokenDetails(TypedDict, total=False):75    """Breakdown of output token counts.7677    Does *not* need to sum to full output token count. Does *not* need to have all keys.7879    Example:80        ```python81        {82            "audio": 10,83            "reasoning": 200,84        }85        ```8687    May also hold extra provider-specific keys.8889    !!! version-added "Added in `langchain-core` 0.3.9"9091    """9293    audio: int94    """Audio output tokens."""9596    reasoning: int97    """Reasoning output tokens.9899    Tokens generated by the model in a chain of thought process that are not100    returned as part of model output.101    """102103104class UsageMetadata(TypedDict):105    """Usage metadata for a message, such as token counts.106107    This is a standard representation of token usage that is consistent across models.108109    Example:110        ```python111        {112            "input_tokens": 350,113            "output_tokens": 240,114            "total_tokens": 590,115            "input_token_details": {116                "audio": 10,117                "cache_creation": 200,118                "cache_read": 100,119            },120            "output_token_details": {121                "audio": 10,122                "reasoning": 200,123            },124        }125        ```126127    !!! warning "Behavior changed in `langchain-core` 0.3.9"128129        Added `input_token_details` and `output_token_details`.130131    !!! note "LangSmith SDK"132133        The LangSmith SDK also has a `UsageMetadata` class. While the two share fields,134        LangSmith's `UsageMetadata` has additional fields to capture cost information135        used by the LangSmith platform.136    """137138    input_tokens: int139    """Count of input (or prompt) tokens. Sum of all input token types."""140141    output_tokens: int142    """Count of output (or completion) tokens. Sum of all output token types."""143144    total_tokens: int145    """Total token count. Sum of `input_tokens` + `output_tokens`."""146147    input_token_details: NotRequired[InputTokenDetails]148    """Breakdown of input token counts.149150    Does *not* need to sum to full input token count. Does *not* need to have all keys.151    """152153    output_token_details: NotRequired[OutputTokenDetails]154    """Breakdown of output token counts.155156    Does *not* need to sum to full output token count. Does *not* need to have all keys.157    """158159160class AIMessage(BaseMessage):161    """Message from an AI.162163    An `AIMessage` is returned from a chat model as a response to a prompt.164165    This message represents the output of the model and consists of both166    the raw output as returned by the model and standardized fields167    (e.g., tool calls, usage metadata) added by the LangChain framework.168    """169170    tool_calls: list[ToolCall] = Field(default_factory=list)171    """If present, tool calls associated with the message."""172173    invalid_tool_calls: list[InvalidToolCall] = Field(default_factory=list)174    """If present, tool calls with parsing errors associated with the message."""175176    usage_metadata: UsageMetadata | None = None177    """If present, usage metadata for a message, such as token counts.178179    This is a standard representation of token usage that is consistent across models.180    """181182    type: Literal["ai"] = "ai"183    """The type of the message (used for deserialization)."""184185    @overload186    def __init__(187        self,188        content: str | list[str | dict[Any, Any]],189        **kwargs: Any,190    ) -> None: ...191192    @overload193    def __init__(194        self,195        content: str | list[str | dict[Any, Any]] | None = None,196        content_blocks: list[types.ContentBlock] | None = None,197        **kwargs: Any,198    ) -> None: ...199200    def __init__(201        self,202        content: str | list[str | dict[Any, Any]] | None = None,203        content_blocks: list[types.ContentBlock] | None = None,204        **kwargs: Any,205    ) -> None:206        """Initialize an `AIMessage`.207208        Specify `content` as positional arg or `content_blocks` for typing.209210        Args:211            content: The content of the message.212            content_blocks: Typed standard content.213            **kwargs: Additional arguments to pass to the parent class.214        """215        if content_blocks is not None:216            # If there are tool calls in content_blocks, but not in tool_calls, add them217            content_tool_calls = [218                block for block in content_blocks if block.get("type") == "tool_call"219            ]220            if content_tool_calls and "tool_calls" not in kwargs:221                kwargs["tool_calls"] = content_tool_calls222223            super().__init__(224                content=cast("list[str | dict[Any, Any]]", content_blocks),225                **kwargs,226            )227        else:228            super().__init__(content=content, **kwargs)229230    @property231    def lc_attributes(self) -> dict[str, Any]:232        """Attributes to be serialized.233234        Includes all attributes, even if they are derived from other initialization235        arguments.236        """237        return {238            "tool_calls": self.tool_calls,239            "invalid_tool_calls": self.invalid_tool_calls,240        }241242    @property243    def content_blocks(self) -> list[types.ContentBlock]:244        """Return standard, typed `ContentBlock` dicts from the message.245246        If the message has a known model provider, use the provider-specific translator247        first before falling back to best-effort parsing. For details, see the property248        on `BaseMessage`.249        """250        if self.response_metadata.get("output_version") == "v1" and isinstance(251            self.content, list252        ):253            # Only short-circuit when content is a list (assumed under v1 to254            # already hold ContentBlock dicts; the cast is unchecked). See255            # AIMessageChunk.content_blocks for full rationale.256            return cast("list[types.ContentBlock]", self.content)257258        model_provider = self.response_metadata.get("model_provider")259        if model_provider:260            from langchain_core.messages.block_translators import (  # noqa: PLC0415261                get_translator,262            )263264            translator = get_translator(model_provider)265            if translator:266                try:267                    return translator["translate_content"](self)268                except NotImplementedError:269                    pass270271        # Otherwise, use best-effort parsing272        blocks = super().content_blocks273274        if self.tool_calls:275            # Add from tool_calls if missing from content276            content_tool_call_ids = {277                block.get("id")278                for block in self.content279                if isinstance(block, dict) and block.get("type") == "tool_call"280            }281            for tool_call in self.tool_calls:282                if (id_ := tool_call.get("id")) and id_ not in content_tool_call_ids:283                    tool_call_block: types.ToolCall = {284                        "type": "tool_call",285                        "id": id_,286                        "name": tool_call["name"],287                        "args": tool_call["args"],288                    }289                    if "index" in tool_call:290                        tool_call_block["index"] = tool_call["index"]  # type: ignore[typeddict-item]291                    if "extras" in tool_call:292                        tool_call_block["extras"] = tool_call["extras"]  # type: ignore[typeddict-item]293                    blocks.append(tool_call_block)294295        # Best-effort reasoning extraction from additional_kwargs296        # Only add reasoning if not already present297        # Insert before all other blocks to keep reasoning at the start298        has_reasoning = any(block.get("type") == "reasoning" for block in blocks)299        if not has_reasoning and (300            reasoning_block := _extract_reasoning_from_additional_kwargs(self)301        ):302            blocks.insert(0, reasoning_block)303304        return blocks305306    # TODO: remove this logic if possible, reducing breaking nature of changes307    @model_validator(mode="before")308    @classmethod309    def _backwards_compat_tool_calls(cls, values: dict[str, Any]) -> Any:310        check_additional_kwargs = not any(311            values.get(k)312            for k in ("tool_calls", "invalid_tool_calls", "tool_call_chunks")313        )314        if check_additional_kwargs and (315            raw_tool_calls := values.get("additional_kwargs", {}).get("tool_calls")316        ):317            try:318                if issubclass(cls, AIMessageChunk):319                    values["tool_call_chunks"] = default_tool_chunk_parser(320                        raw_tool_calls321                    )322                else:323                    parsed_tool_calls, parsed_invalid_tool_calls = default_tool_parser(324                        raw_tool_calls325                    )326                    values["tool_calls"] = parsed_tool_calls327                    values["invalid_tool_calls"] = parsed_invalid_tool_calls328            except Exception:329                logger.debug("Failed to parse tool calls", exc_info=True)330331        # Ensure "type" is properly set on all tool call-like dicts.332        if tool_calls := values.get("tool_calls"):333            values["tool_calls"] = [334                create_tool_call(335                    **{k: v for k, v in tc.items() if k not in {"type", "extras"}}336                )337                for tc in tool_calls338            ]339        if invalid_tool_calls := values.get("invalid_tool_calls"):340            values["invalid_tool_calls"] = [341                create_invalid_tool_call(**{k: v for k, v in tc.items() if k != "type"})342                for tc in invalid_tool_calls343            ]344345        if tool_call_chunks := values.get("tool_call_chunks"):346            values["tool_call_chunks"] = [347                create_tool_call_chunk(**{k: v for k, v in tc.items() if k != "type"})348                for tc in tool_call_chunks349            ]350351        return values352353    @override354    def pretty_repr(self, html: bool = False) -> str:355        """Return a pretty representation of the message for display.356357        Args:358            html: Whether to return an HTML-formatted string.359360        Returns:361            A pretty representation of the message.362363        Example:364            ```python365            from langchain_core.messages import AIMessage366367            msg = AIMessage(368                content="Let me check the weather.",369                tool_calls=[370                    {"name": "get_weather", "args": {"city": "Paris"}, "id": "1"}371                ],372            )373            ```374375            Results in:376            ```python377            >>> print(msg.pretty_repr())378            ================================== Ai Message ==================================379380            Let me check the weather.381            Tool Calls:382              get_weather (1)383             Call ID: 1384              Args:385                city: Paris386            ```387        """  # noqa: E501388        base = super().pretty_repr(html=html)389        lines = []390391        def _format_tool_args(tc: ToolCall | InvalidToolCall) -> list[str]:392            lines = [393                f"  {tc.get('name', 'Tool')} ({tc.get('id')})",394                f" Call ID: {tc.get('id')}",395            ]396            if tc.get("error"):397                lines.append(f"  Error: {tc.get('error')}")398            lines.append("  Args:")399            args = tc.get("args")400            if isinstance(args, str):401                lines.append(f"    {args}")402            elif isinstance(args, dict):403                for arg, value in args.items():404                    lines.append(f"    {arg}: {value}")405            return lines406407        if self.tool_calls:408            lines.append("Tool Calls:")409            for tc in self.tool_calls:410                lines.extend(_format_tool_args(tc))411        if self.invalid_tool_calls:412            lines.append("Invalid Tool Calls:")413            for itc in self.invalid_tool_calls:414                lines.extend(_format_tool_args(itc))415        return (base.strip() + "\n" + "\n".join(lines)).strip()416417418class AIMessageChunk(AIMessage, BaseMessageChunk):419    """Message chunk from an AI (yielded when streaming)."""420421    # Ignoring mypy re-assignment here since we're overriding the value422    # to make sure that the chunk variant can be discriminated from the423    # non-chunk variant.424    type: Literal["AIMessageChunk"] = "AIMessageChunk"  # type: ignore[assignment]425    """The type of the message (used for deserialization)."""426427    tool_call_chunks: list[ToolCallChunk] = Field(default_factory=list)428    """If provided, tool call chunks associated with the message."""429430    chunk_position: Literal["last"] | None = None431    """Optional span represented by an aggregated `AIMessageChunk`.432433    If a chunk with `chunk_position="last"` is aggregated into a stream,434    `tool_call_chunks` in message content will be parsed into `tool_calls`.435    """436437    @property438    @override439    def lc_attributes(self) -> dict[str, Any]:440        return {441            "tool_calls": self.tool_calls,442            "invalid_tool_calls": self.invalid_tool_calls,443        }444445    @property446    def content_blocks(self) -> list[types.ContentBlock]:447        """Return standard, typed `ContentBlock` dicts from the message."""448        if self.response_metadata.get("output_version") == "v1" and isinstance(449            self.content, list450        ):451            # Only short-circuit when content is already a list of ContentBlock452            # dicts. Some streaming implementations keep content as a string453            # even when output_version="v1" is set (e.g., OpenAI Chat454            # Completions), so it must fall through to the model_provider455            # translator which builds ContentBlock dicts from tool_calls /456            # tool_call_chunks. Without this guard, string content would be457            # returned directly, silently dropping tool calls.458            return cast("list[types.ContentBlock]", self.content)459460        model_provider = self.response_metadata.get("model_provider")461        if model_provider:462            from langchain_core.messages.block_translators import (  # noqa: PLC0415463                get_translator,464            )465466            translator = get_translator(model_provider)467            if translator:468                try:469                    return translator["translate_content_chunk"](self)470                except NotImplementedError:471                    pass472473        # Otherwise, use best-effort parsing474        blocks = super().content_blocks475476        if (477            self.tool_call_chunks478            and not self.content479            and self.chunk_position != "last"  # keep tool_calls if aggregated480        ):481            blocks = [482                block483                for block in blocks484                if block["type"] not in {"tool_call", "invalid_tool_call"}485            ]486            for tool_call_chunk in self.tool_call_chunks:487                tc: types.ToolCallChunk = {488                    "type": "tool_call_chunk",489                    "id": tool_call_chunk.get("id"),490                    "name": tool_call_chunk.get("name"),491                    "args": tool_call_chunk.get("args"),492                }493                if (idx := tool_call_chunk.get("index")) is not None:494                    tc["index"] = idx495                blocks.append(tc)496497        # Best-effort reasoning extraction from additional_kwargs498        # Only add reasoning if not already present499        # Insert before all other blocks to keep reasoning at the start500        has_reasoning = any(block.get("type") == "reasoning" for block in blocks)501        if not has_reasoning and (502            reasoning_block := _extract_reasoning_from_additional_kwargs(self)503        ):504            blocks.insert(0, reasoning_block)505506        return blocks507508    @model_validator(mode="after")509    def init_tool_calls(self) -> Self:510        """Initialize tool calls from tool call chunks.511512        Returns:513            The values with tool calls initialized.514515        Raises:516            ValueError: If the tool call chunks are malformed.517        """518        if not self.tool_call_chunks:519            if self.tool_calls:520                self.tool_call_chunks = [521                    create_tool_call_chunk(522                        name=tc["name"],523                        args=json.dumps(tc["args"]),524                        id=tc["id"],525                        index=None,526                    )527                    for tc in self.tool_calls528                ]529            if self.invalid_tool_calls:530                tool_call_chunks = self.tool_call_chunks531                tool_call_chunks.extend(532                    [533                        create_tool_call_chunk(534                            name=tc["name"], args=tc["args"], id=tc["id"], index=None535                        )536                        for tc in self.invalid_tool_calls537                    ]538                )539                self.tool_call_chunks = tool_call_chunks540541            return self542        tool_calls = []543        invalid_tool_calls = []544545        def add_chunk_to_invalid_tool_calls(chunk: ToolCallChunk) -> None:546            invalid_tool_calls.append(547                create_invalid_tool_call(548                    name=chunk["name"],549                    args=chunk["args"],550                    id=chunk["id"],551                    error=None,552                )553            )554555        for chunk in self.tool_call_chunks:556            try:557                args_ = parse_partial_json(chunk["args"]) if chunk["args"] else {}558                if isinstance(args_, dict):559                    tool_calls.append(560                        create_tool_call(561                            name=chunk["name"] or "",562                            args=args_,563                            id=chunk["id"],564                        )565                    )566                else:567                    add_chunk_to_invalid_tool_calls(chunk)568            except Exception:569                add_chunk_to_invalid_tool_calls(chunk)570        self.tool_calls = tool_calls571        self.invalid_tool_calls = invalid_tool_calls572573        if (574            self.chunk_position == "last"575            and self.tool_call_chunks576            and self.response_metadata.get("output_version") == "v1"577            and isinstance(self.content, list)578        ):579            id_to_tc: dict[str, types.ToolCall] = {580                cast("str", tc.get("id")): {581                    "type": "tool_call",582                    "name": tc["name"],583                    "args": tc["args"],584                    "id": tc.get("id"),585                }586                for tc in self.tool_calls587                if "id" in tc588            }589            for idx, block in enumerate(self.content):590                if (591                    isinstance(block, dict)592                    and block.get("type") == "tool_call_chunk"593                    and (call_id := block.get("id"))594                    and call_id in id_to_tc595                ):596                    self.content[idx] = cast("dict[str, Any]", id_to_tc[call_id])597                    if "extras" in block:598                        # mypy does not account for instance check for dict above599                        self.content[idx]["extras"] = block["extras"]  # type: ignore[index]600601        return self602603    @model_validator(mode="after")604    def init_server_tool_calls(self) -> Self:605        """Initialize server tool calls.606607        Parse `server_tool_call_chunks` from608        [`ServerToolCallChunk`][langchain.messages.ServerToolCallChunk] objects.609        """610        if (611            self.chunk_position == "last"612            and self.response_metadata.get("output_version") == "v1"613            and isinstance(self.content, list)614        ):615            for idx, block in enumerate(self.content):616                if (617                    isinstance(block, dict)618                    and block.get("type")619                    in {"server_tool_call", "server_tool_call_chunk"}620                    and isinstance(args_str := block.get("args") or "{}", str)621                ):622                    try:623                        args = json.loads(args_str)624                        if isinstance(args, dict):625                            self.content[idx]["type"] = "server_tool_call"  # type: ignore[index]626                            self.content[idx]["args"] = args  # type: ignore[index]627                    except json.JSONDecodeError:628                        pass629        return self630631    @overload  # type: ignore[override]  # summing BaseMessages gives ChatPromptTemplate632    def __add__(self, other: "AIMessageChunk") -> "AIMessageChunk": ...633634    @overload635    def __add__(self, other: Sequence["AIMessageChunk"]) -> "AIMessageChunk": ...636637    @overload638    def __add__(self, other: Any) -> BaseMessageChunk: ...639640    @override641    def __add__(self, other: Any) -> BaseMessageChunk:642        if isinstance(other, AIMessageChunk):643            return add_ai_message_chunks(self, other)644        if isinstance(other, (list, tuple)) and all(645            isinstance(o, AIMessageChunk) for o in other646        ):647            return add_ai_message_chunks(self, *other)648        return super().__add__(other)649650651def add_ai_message_chunks(652    left: AIMessageChunk, *others: AIMessageChunk653) -> AIMessageChunk:654    """Add multiple `AIMessageChunk`s together.655656    Args:657        left: The first `AIMessageChunk`.658        *others: Other `AIMessageChunk`s to add.659660    Returns:661        The resulting `AIMessageChunk`.662663    """664    content = merge_content(left.content, *(o.content for o in others))665    additional_kwargs = merge_dicts(666        left.additional_kwargs, *(o.additional_kwargs for o in others)667    )668    response_metadata = merge_dicts(669        left.response_metadata, *(o.response_metadata for o in others)670    )671672    # Merge tool call chunks673    if raw_tool_calls := merge_lists(674        left.tool_call_chunks, *(o.tool_call_chunks for o in others)675    ):676        tool_call_chunks = [677            create_tool_call_chunk(678                name=rtc.get("name"),679                args=rtc.get("args"),680                index=rtc.get("index"),681                id=rtc.get("id"),682            )683            for rtc in raw_tool_calls684        ]685    else:686        tool_call_chunks = []687688    # Token usage689    if left.usage_metadata or any(o.usage_metadata is not None for o in others):690        usage_metadata: UsageMetadata | None = left.usage_metadata691        for other in others:692            usage_metadata = add_usage(usage_metadata, other.usage_metadata)693    else:694        usage_metadata = None695696    # Ranks are defined by the order of preference. Higher is better:697    # 2. Provider-assigned IDs (non lc_* and non lc_run-*)698    # 1. lc_run-* IDs699    # 0. lc_* and other remaining IDs700    best_rank = -1701    chunk_id = None702    candidates = itertools.chain([left.id], (o.id for o in others))703704    for id_ in candidates:705        if not id_:706            continue707708        if not id_.startswith(LC_ID_PREFIX) and not id_.startswith(LC_AUTO_PREFIX):709            chunk_id = id_710            # Highest rank, return instantly711            break712713        rank = 1 if id_.startswith(LC_ID_PREFIX) else 0714715        if rank > best_rank:716            best_rank = rank717            chunk_id = id_718719    chunk_position: Literal["last"] | None = (720        "last" if any(x.chunk_position == "last" for x in [left, *others]) else None721    )722723    return left.__class__(724        content=content,725        additional_kwargs=additional_kwargs,726        tool_call_chunks=tool_call_chunks,727        response_metadata=response_metadata,728        usage_metadata=usage_metadata,729        id=chunk_id,730        chunk_position=chunk_position,731    )732733734def add_usage(left: UsageMetadata | None, right: UsageMetadata | None) -> UsageMetadata:735    """Recursively add two UsageMetadata objects.736737    Example:738        ```python739        from langchain_core.messages.ai import add_usage740741        left = UsageMetadata(742            input_tokens=5,743            output_tokens=0,744            total_tokens=5,745            input_token_details=InputTokenDetails(cache_read=3),746        )747        right = UsageMetadata(748            input_tokens=0,749            output_tokens=10,750            total_tokens=10,751            output_token_details=OutputTokenDetails(reasoning=4),752        )753754        add_usage(left, right)755        ```756757        results in758759        ```python760        UsageMetadata(761            input_tokens=5,762            output_tokens=10,763            total_tokens=15,764            input_token_details=InputTokenDetails(cache_read=3),765            output_token_details=OutputTokenDetails(reasoning=4),766        )767        ```768    Args:769        left: The first `UsageMetadata` object.770        right: The second `UsageMetadata` object.771772    Returns:773        The sum of the two `UsageMetadata` objects.774775    """776    if not (left or right):777        return UsageMetadata(input_tokens=0, output_tokens=0, total_tokens=0)778    if not (left and right):779        return cast("UsageMetadata", left or right)780781    return UsageMetadata(782        **cast(783            "UsageMetadata",784            _dict_int_op(785                cast("dict[str, Any]", left),786                cast("dict[str, Any]", right),787                operator.add,788            ),789        )790    )791792793def subtract_usage(794    left: UsageMetadata | None, right: UsageMetadata | None795) -> UsageMetadata:796    """Recursively subtract two `UsageMetadata` objects.797798    Token counts cannot be negative so the actual operation is `max(left - right, 0)`.799800    Example:801        ```python802        from langchain_core.messages.ai import subtract_usage803804        left = UsageMetadata(805            input_tokens=5,806            output_tokens=10,807            total_tokens=15,808            input_token_details=InputTokenDetails(cache_read=4),809        )810        right = UsageMetadata(811            input_tokens=3,812            output_tokens=8,813            total_tokens=11,814            output_token_details=OutputTokenDetails(reasoning=4),815        )816817        subtract_usage(left, right)818        ```819820        results in821822        ```python823        UsageMetadata(824            input_tokens=2,825            output_tokens=2,826            total_tokens=4,827            input_token_details=InputTokenDetails(cache_read=4),828            output_token_details=OutputTokenDetails(reasoning=0),829        )830        ```831    Args:832        left: The first `UsageMetadata` object.833        right: The second `UsageMetadata` object.834835    Returns:836        The resulting `UsageMetadata` after subtraction.837838    """839    if not (left or right):840        return UsageMetadata(input_tokens=0, output_tokens=0, total_tokens=0)841    if not (left and right):842        return cast("UsageMetadata", left or right)843844    return UsageMetadata(845        **cast(846            "UsageMetadata",847            _dict_int_op(848                cast("dict[str, Any]", left),849                cast("dict[str, Any]", right),850                (lambda le, ri: max(le - ri, 0)),851            ),852        )853    )

Code quality findings 4

Catch specific exceptions instead of Exception to avoid masking bugs
info correctness broad-except
except Exception:
Ensure functions have docstrings for documentation
info maintainability missing-docstring
def lc_attributes(self) -> dict[str, Any]:
Ensure functions have docstrings for documentation
info maintainability missing-docstring
def add_chunk_to_invalid_tool_calls(chunk: ToolCallChunk) -> None:
Catch specific exceptions instead of Exception to avoid masking bugs
info correctness broad-except
except Exception:

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.

Generated by searchcode.com · powered by code_get_file + code_get_findings