libs/partners/mistralai/langchain_mistralai/chat_models.py PYTHON 1,329 lines View on github.com → Search inside
1from __future__ import annotations23import hashlib4import json5import logging6import os7import re8import ssl9import uuid10from collections.abc import Callable, Sequence  # noqa: TC00311from operator import itemgetter12from typing import (13    TYPE_CHECKING,14    Any,15    Literal,16    cast,17)1819import certifi20import httpx21from httpx_sse import EventSource, aconnect_sse, connect_sse22from langchain_core.callbacks import (23    AsyncCallbackManagerForLLMRun,24    CallbackManagerForLLMRun,25)26from langchain_core.language_models import (27    LanguageModelInput,28    ModelProfile,29    ModelProfileRegistry,30)31from langchain_core.language_models.chat_models import BaseChatModel, LangSmithParams32from langchain_core.language_models.llms import create_base_retry_decorator33from langchain_core.messages import (34    AIMessage,35    AIMessageChunk,36    BaseMessage,37    BaseMessageChunk,38    ChatMessage,39    ChatMessageChunk,40    HumanMessage,41    HumanMessageChunk,42    InvalidToolCall,43    SystemMessage,44    SystemMessageChunk,45    ToolCall,46    ToolMessage,47    is_data_content_block,48)49from langchain_core.messages.block_translators.openai import (50    convert_to_openai_data_block,51)52from langchain_core.messages.tool import tool_call_chunk53from langchain_core.output_parsers import (54    JsonOutputParser,55    PydanticOutputParser,56)57from langchain_core.output_parsers.base import OutputParserLike58from langchain_core.output_parsers.openai_tools import (59    JsonOutputKeyToolsParser,60    PydanticToolsParser,61    make_invalid_tool_call,62    parse_tool_call,63)64from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult65from langchain_core.runnables import Runnable, RunnableMap, RunnablePassthrough66from langchain_core.tools import BaseTool67from langchain_core.utils import get_pydantic_field_names, secret_from_env68from langchain_core.utils.function_calling import convert_to_openai_tool69from langchain_core.utils.pydantic import is_basemodel_subclass70from langchain_core.utils.utils import _build_model_kwargs71from pydantic import (72    BaseModel,73    ConfigDict,74    Field,75    SecretStr,76    model_validator,77)78from typing_extensions import Self7980from langchain_mistralai._compat import _convert_from_v1_to_mistral81from langchain_mistralai._version import __version__82from langchain_mistralai.data._profiles import _PROFILES8384if TYPE_CHECKING:85    from collections.abc import AsyncIterator, Iterator86    from contextlib import AbstractAsyncContextManager8788logger = logging.getLogger(__name__)8990# Mistral enforces a specific pattern for tool call IDs91TOOL_CALL_ID_PATTERN = re.compile(r"^[a-zA-Z0-9]{9}$")929394# This SSL context is equivalent to the default `verify=True`.95# https://www.python-httpx.org/advanced/ssl/#configuring-client-instances96global_ssl_context = ssl.create_default_context(cafile=certifi.where())979899_MODEL_PROFILES = cast("ModelProfileRegistry", _PROFILES)100101102def _get_default_model_profile(model_name: str) -> ModelProfile:103    default = _MODEL_PROFILES.get(model_name) or {}104    return default.copy()105106107def _create_retry_decorator(108    llm: ChatMistralAI,109    run_manager: AsyncCallbackManagerForLLMRun | CallbackManagerForLLMRun | None = None,110) -> Callable[[Any], Any]:111    """Return a tenacity retry decorator, preconfigured to handle exceptions."""112    errors = [httpx.RequestError, httpx.StreamError]113    return create_base_retry_decorator(114        error_types=errors, max_retries=llm.max_retries, run_manager=run_manager115    )116117118def _is_valid_mistral_tool_call_id(tool_call_id: str) -> bool:119    """Check if tool call ID is nine character string consisting of a-z, A-Z, 0-9."""120    return bool(TOOL_CALL_ID_PATTERN.match(tool_call_id))121122123def _base62_encode(num: int) -> str:124    """Encode a number in base62 and ensures result is of a specified length."""125    base62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"126    if num == 0:127        return base62[0]128    arr = []129    base = len(base62)130    while num:131        num, rem = divmod(num, base)132        arr.append(base62[rem])133    arr.reverse()134    return "".join(arr)135136137def _convert_tool_call_id_to_mistral_compatible(tool_call_id: str) -> str:138    """Convert a tool call ID to a Mistral-compatible format."""139    if _is_valid_mistral_tool_call_id(tool_call_id):140        return tool_call_id141    hash_bytes = hashlib.sha256(tool_call_id.encode()).digest()142    hash_int = int.from_bytes(hash_bytes, byteorder="big")143    base62_str = _base62_encode(hash_int)144    if len(base62_str) >= 9:145        return base62_str[:9]146    return base62_str.rjust(9, "0")147148149def _normalize_mistral_content(content: Any) -> str | list[str | dict]:150    """Normalize Mistral content so reference blocks are visible to .text.151152    Mistral citation responses return content as a list of typed chunks where153    `reference` blocks carry visible answer text alongside citation metadata.154    The core `.text` accessor only concatenates blocks whose type is155    `"text"`, so preserving `reference` as-is would drop cited answer spans156    from `message.text` and `ChatGeneration.text`.157158    To keep the answer text visible while preserving citation metadata, rewrite159    each `reference` block to `type: "text"` and move the original block160    (including `reference_ids`) under a `"reference"` key. The `_compat.py`161    translator reads that key to produce standard `Citation` annotations.162    """163    if not isinstance(content, list):164        return content or ""165    has_reference = False166    new_blocks: list[str | dict] = []167    for block in content:168        if isinstance(block, dict) and block.get("type") == "reference":169            has_reference = True170            new_block = {171                "type": "text",172                "text": block.get("text", ""),173                "reference": {174                    k: v for k, v in block.items() if k not in ("type", "text")175                },176            }177            if "index" in block:178                new_block["index"] = block["index"]179            new_blocks.append(new_block)180        else:181            new_blocks.append(block)182    return new_blocks if has_reference else content183184185def _convert_mistral_chat_message_to_message(186    _message: dict,187) -> BaseMessage:188    role = _message["role"]189    if role != "assistant":190        msg = f"Expected role to be 'assistant', got {role}"191        raise ValueError(msg)192    # Mistral returns None for tool invocations. When citations are enabled,193    # content is a list of typed chunks (text and reference). Normalize194    # reference blocks so their answer text is visible via .text while195    # citation metadata is preserved for _compat.py to translate.196    content = _normalize_mistral_content(_message.get("content", ""))197198    additional_kwargs: dict = {}199    tool_calls = []200    invalid_tool_calls = []201    if raw_tool_calls := _message.get("tool_calls"):202        additional_kwargs["tool_calls"] = raw_tool_calls203        for raw_tool_call in raw_tool_calls:204            try:205                parsed: dict = cast(206                    "dict", parse_tool_call(raw_tool_call, return_id=True)207                )208                if not parsed["id"]:209                    parsed["id"] = uuid.uuid4().hex[:]210                tool_calls.append(parsed)211            except Exception as e:212                invalid_tool_calls.append(make_invalid_tool_call(raw_tool_call, str(e)))213    return AIMessage(214        content=content,215        additional_kwargs=additional_kwargs,216        tool_calls=tool_calls,217        invalid_tool_calls=invalid_tool_calls,218        response_metadata={"model_provider": "mistralai"},219    )220221222def _raise_on_error(response: httpx.Response) -> None:223    """Raise an error if the response is an error."""224    if httpx.codes.is_error(response.status_code):225        error_message = response.read().decode("utf-8")226        msg = (227            f"Error response {response.status_code} "228            f"while fetching {response.url}: {error_message}"229        )230        raise httpx.HTTPStatusError(231            msg,232            request=response.request,233            response=response,234        )235236237async def _araise_on_error(response: httpx.Response) -> None:238    """Raise an error if the response is an error."""239    if httpx.codes.is_error(response.status_code):240        error_message = (await response.aread()).decode("utf-8")241        msg = (242            f"Error response {response.status_code} "243            f"while fetching {response.url}: {error_message}"244        )245        raise httpx.HTTPStatusError(246            msg,247            request=response.request,248            response=response,249        )250251252async def _aiter_sse(253    event_source_mgr: AbstractAsyncContextManager[EventSource],254) -> AsyncIterator[dict]:255    """Iterate over the server-sent events."""256    async with event_source_mgr as event_source:257        await _araise_on_error(event_source.response)258        async for event in event_source.aiter_sse():259            if event.data == "[DONE]":260                return261            yield event.json()262263264async def acompletion_with_retry(265    llm: ChatMistralAI,266    run_manager: AsyncCallbackManagerForLLMRun | None = None,267    **kwargs: Any,268) -> Any:269    """Use tenacity to retry the async completion call."""270    retry_decorator = _create_retry_decorator(llm, run_manager=run_manager)271272    @retry_decorator273    async def _completion_with_retry(**kwargs: Any) -> Any:274        if "stream" not in kwargs:275            kwargs["stream"] = False276        stream = kwargs["stream"]277        if stream:278            event_source = aconnect_sse(279                llm.async_client, "POST", "/chat/completions", json=kwargs280            )281            return _aiter_sse(event_source)282        response = await llm.async_client.post(url="/chat/completions", json=kwargs)283        await _araise_on_error(response)284        return response.json()285286    return await _completion_with_retry(**kwargs)287288289def _convert_chunk_to_message_chunk(290    chunk: dict,291    default_class: type[BaseMessageChunk],292    index: int,293    index_type: str,294    output_version: str | None,295) -> tuple[BaseMessageChunk, int, str]:296    _choice = chunk["choices"][0]297    _delta = _choice["delta"]298    role = _delta.get("role")299    content = _delta.get("content") or ""300    if output_version == "v1" and isinstance(content, str):301        content = [{"type": "text", "text": content}]302    content = _normalize_mistral_content(content)303    if isinstance(content, list):304        for block in content:305            if isinstance(block, dict):306                block_type = "reference" if "reference" in block else block.get("type")307                if block_type is not None and block_type != index_type:308                    index_type = block_type309                    index = index + 1310                if "index" not in block:311                    block["index"] = index312                if block.get("type") == "thinking" and isinstance(313                    block.get("thinking"), list314                ):315                    for sub_block in block["thinking"]:316                        if isinstance(sub_block, dict) and "index" not in sub_block:317                            sub_block["index"] = 0318    if role == "user" or default_class == HumanMessageChunk:319        return HumanMessageChunk(content=content), index, index_type320    if role == "assistant" or default_class == AIMessageChunk:321        additional_kwargs: dict = {}322        response_metadata: dict[str, Any] = {}323        if raw_tool_calls := _delta.get("tool_calls"):324            additional_kwargs["tool_calls"] = raw_tool_calls325            try:326                tool_call_chunks = []327                for raw_tool_call in raw_tool_calls:328                    if not raw_tool_call.get("index") and not raw_tool_call.get("id"):329                        tool_call_id = uuid.uuid4().hex[:]330                    else:331                        tool_call_id = raw_tool_call.get("id")332                    tool_call_chunks.append(333                        tool_call_chunk(334                            name=raw_tool_call["function"].get("name"),335                            args=raw_tool_call["function"].get("arguments"),336                            id=tool_call_id,337                            index=raw_tool_call.get("index"),338                        )339                    )340            except KeyError:341                pass342        else:343            tool_call_chunks = []344        if token_usage := chunk.get("usage"):345            usage_metadata = {346                "input_tokens": token_usage.get("prompt_tokens", 0),347                "output_tokens": token_usage.get("completion_tokens", 0),348                "total_tokens": token_usage.get("total_tokens", 0),349            }350        else:351            usage_metadata = None352        if _choice.get("finish_reason") is not None and isinstance(353            chunk.get("model"), str354        ):355            response_metadata["model_name"] = chunk["model"]356            response_metadata["finish_reason"] = _choice["finish_reason"]357        return (358            AIMessageChunk(359                content=content,360                additional_kwargs=additional_kwargs,361                tool_call_chunks=tool_call_chunks,  # type: ignore[arg-type]362                usage_metadata=usage_metadata,  # type: ignore[arg-type]363                response_metadata={"model_provider": "mistralai", **response_metadata},364            ),365            index,366            index_type,367        )368    if role == "system" or default_class == SystemMessageChunk:369        return SystemMessageChunk(content=content), index, index_type370    if role or default_class == ChatMessageChunk:371        return ChatMessageChunk(content=content, role=role), index, index_type372    return default_class(content=content), index, index_type  # type: ignore[call-arg]373374375def _format_tool_call_for_mistral(tool_call: ToolCall) -> dict:376    """Format LangChain ToolCall to dict expected by Mistral."""377    result: dict[str, Any] = {378        "function": {379            "name": tool_call["name"],380            "arguments": json.dumps(tool_call["args"], ensure_ascii=False),381        }382    }383    if _id := tool_call.get("id"):384        result["id"] = _convert_tool_call_id_to_mistral_compatible(_id)385386    return result387388389def _format_invalid_tool_call_for_mistral(invalid_tool_call: InvalidToolCall) -> dict:390    """Format LangChain InvalidToolCall to dict expected by Mistral."""391    result: dict[str, Any] = {392        "function": {393            "name": invalid_tool_call["name"],394            "arguments": invalid_tool_call["args"],395        }396    }397    if _id := invalid_tool_call.get("id"):398        result["id"] = _convert_tool_call_id_to_mistral_compatible(_id)399400    return result401402403def _clean_block(block: dict) -> dict:404    # Remove internal keys added by LangChain or by provider response normalization.405    if block.get("type") == "text" and "text" in block:406        return {"type": "text", "text": block["text"]}407408    new_block = {k: v for k, v in block.items() if k != "index"}409    if block.get("type") == "thinking" and isinstance(block.get("thinking"), list):410        new_block["thinking"] = [411            (412                {k: v for k, v in sb.items() if k != "index"}413                if isinstance(sb, dict) and "index" in sb414                else sb415            )416            for sb in block["thinking"]417        ]418    return new_block419420421def _sanitize_chat_completions_content(content: Any) -> Any:422    """Strip non-wire keys from text content blocks.423424    Mistral's chat completions endpoint rejects unknown fields on tool425    message content blocks (e.g. the `id` that LangChain auto-generates on426    `TextContentBlock`). For list content, keep only `type` and `text` on427    text blocks; pass other blocks and non-list content through unchanged.428    """429    if not isinstance(content, list):430        return content431    sanitized: list[Any] = []432    for block in content:433        if isinstance(block, dict) and block.get("type") == "text" and "text" in block:434            sanitized.append({"type": "text", "text": block["text"]})435        else:436            sanitized.append(block)437    return sanitized438439440def _format_message_content(content: Any) -> Any:441    """Format message content for the Mistral chat completions wire format.442443    Walks list content and translates LangChain canonical v0/v1 multimodal444    data blocks (e.g. `ImageContentBlock` with `url`, `base64`, or445    `file_id`) into the OpenAI-compatible shape that Mistral accepts:446    `{"type": "image_url", "image_url": {"url": "..."}}`. Strings and any447    other dict blocks are returned unchanged so that already-translated wire448    blocks (e.g. `text`, `image_url`) and Mistral-specific blocks449    (`document_url`, `input_audio`) pass through; the API surfaces an error450    for anything it doesn't understand.451452    Args:453        content: The message content. Strings and non-list values pass454            through unchanged; lists are walked block by block.455456    Returns:457        The formatted content. List inputs return a new list with canonical458        data-block translations applied; other inputs are returned as-is.459    """460    if not isinstance(content, list):461        return content462    formatted: list[Any] = []463    for block in content:464        if isinstance(block, dict) and is_data_content_block(block):465            formatted.append(466                convert_to_openai_data_block(block, api="chat/completions")467            )468            continue469        formatted.append(block)470    return formatted471472473def _convert_message_to_mistral_chat_message(474    message: BaseMessage,475) -> dict:476    if isinstance(message, ChatMessage):477        return {"role": message.role, "content": message.content}478    if isinstance(message, HumanMessage):479        return {"role": "user", "content": _format_message_content(message.content)}480    if isinstance(message, AIMessage):481        message_dict: dict[str, Any] = {"role": "assistant"}482        tool_calls: list = []483        if message.tool_calls or message.invalid_tool_calls:484            if message.tool_calls:485                tool_calls.extend(486                    _format_tool_call_for_mistral(tool_call)487                    for tool_call in message.tool_calls488                )489            if message.invalid_tool_calls:490                tool_calls.extend(491                    _format_invalid_tool_call_for_mistral(invalid_tool_call)492                    for invalid_tool_call in message.invalid_tool_calls493                )494        elif "tool_calls" in message.additional_kwargs:495            for tc in message.additional_kwargs["tool_calls"]:496                chunk = {497                    "function": {498                        "name": tc["function"]["name"],499                        "arguments": tc["function"]["arguments"],500                    }501                }502                if _id := tc.get("id"):503                    chunk["id"] = _id504                tool_calls.append(chunk)505        else:506            pass507        if tool_calls:  # do not populate empty list tool_calls508            message_dict["tool_calls"] = tool_calls509510        # Message content511        # Translate v1 content512        if message.response_metadata.get("output_version") == "v1":513            content = _convert_from_v1_to_mistral(514                message.content_blocks, message.response_metadata.get("model_provider")515            )516        else:517            content = message.content518519        if tool_calls and content:520            # Assistant message must have either content or tool_calls, but not both.521            # Some providers may not support tool_calls in the same message as content.522            # This is done to ensure compatibility with messages from other providers.523            content = ""524525        elif isinstance(content, list):526            content = [527                _clean_block(block) if isinstance(block, dict) else block528                for block in content529            ]530        else:531            content = message.content532533        # if any blocks are dicts, cast strings to text blocks534        if any(isinstance(block, dict) for block in content):535            content = [536                block if isinstance(block, dict) else {"type": "text", "text": block}537                for block in content538            ]539        message_dict["content"] = content540541        if "prefix" in message.additional_kwargs:542            message_dict["prefix"] = message.additional_kwargs["prefix"]543        return message_dict544    if isinstance(message, SystemMessage):545        return {"role": "system", "content": message.content}546    if isinstance(message, ToolMessage):547        return {548            "role": "tool",549            "content": _sanitize_chat_completions_content(message.content),550            "name": message.name,551            "tool_call_id": _convert_tool_call_id_to_mistral_compatible(552                message.tool_call_id553            ),554        }555    msg = f"Got unknown type {message}"556    raise ValueError(msg)557558559class ChatMistralAI(BaseChatModel):560    """A chat model that uses the Mistral AI API."""561562    # The type for client and async_client is ignored because the type is not563    # an Optional after the model is initialized and the model_validator564    # is run.565    client: httpx.Client = Field(  # type: ignore[assignment] # : meta private:566        default=None, exclude=True567    )568569    async_client: httpx.AsyncClient = Field(  # type: ignore[assignment] # : meta private:570        default=None, exclude=True571    )572573    mistral_api_key: SecretStr | None = Field(574        alias="api_key",575        default_factory=secret_from_env("MISTRAL_API_KEY", default=None),576    )577578    endpoint: str | None = Field(default=None, alias="base_url")579580    max_retries: int = 5581582    timeout: int = 120583584    max_concurrent_requests: int = 64585586    model: str = Field(default="mistral-small", alias="model_name")587588    temperature: float = 0.7589590    max_tokens: int | None = None591592    stop: list[str] | None = None593    """Default stop sequences.594595    Generation stops when any of these strings is produced; the stop sequence itself596    is not included in the output. Can be overridden per call via the `stop` argument.597    Mistral accepts up to 4 stop sequences.598    """599600    top_p: float = 1601    """Decode using nucleus sampling: consider the smallest set of tokens whose602    probability sum is at least `top_p`. Must be in the closed interval603    `[0.0, 1.0]`."""604605    random_seed: int | None = None606607    safe_mode: bool | None = None608609    streaming: bool = False610611    model_kwargs: dict[str, Any] = Field(default_factory=dict)612    """Holds any invocation parameters not explicitly specified."""613614    model_config = ConfigDict(615        populate_by_name=True,616        arbitrary_types_allowed=True,617    )618619    @model_validator(mode="before")620    @classmethod621    def build_extra(cls, values: dict[str, Any]) -> Any:622        """Build extra kwargs from additional params that were passed in."""623        all_required_field_names = get_pydantic_field_names(cls)624        return _build_model_kwargs(values, all_required_field_names)625626    @property627    def _default_params(self) -> dict[str, Any]:628        """Get the default parameters for calling the API."""629        defaults = {630            "model": self.model,631            "temperature": self.temperature,632            "max_tokens": self.max_tokens,633            "top_p": self.top_p,634            "random_seed": self.random_seed,635            "safe_prompt": self.safe_mode,636            **self.model_kwargs,637        }638        return {k: v for k, v in defaults.items() if v is not None}639640    def _get_ls_params(641        self, stop: list[str] | None = None, **kwargs: Any642    ) -> LangSmithParams:643        """Get standard params for tracing."""644        params = self._get_invocation_params(stop=stop, **kwargs)645        ls_params = LangSmithParams(646            ls_provider="mistral",647            ls_model_name=params.get("model", self.model),648            ls_model_type="chat",649            ls_temperature=params.get("temperature", self.temperature),650        )651        if ls_max_tokens := params.get("max_tokens", self.max_tokens):652            ls_params["ls_max_tokens"] = ls_max_tokens653        if ls_stop := stop or self.stop or params.get("stop", None):654            ls_params["ls_stop"] = ls_stop655        return ls_params656657    @property658    def _client_params(self) -> dict[str, Any]:659        """Get the parameters used for the client."""660        return self._default_params661662    def completion_with_retry(663        self, run_manager: CallbackManagerForLLMRun | None = None, **kwargs: Any664    ) -> Any:665        """Use tenacity to retry the completion call."""666        retry_decorator = _create_retry_decorator(self, run_manager=run_manager)667668        @retry_decorator669        def _completion_with_retry(**kwargs: Any) -> Any:670            if "stream" not in kwargs:671                kwargs["stream"] = False672            stream = kwargs["stream"]673            if stream:674675                def iter_sse() -> Iterator[dict]:676                    with connect_sse(677                        self.client, "POST", "/chat/completions", json=kwargs678                    ) as event_source:679                        _raise_on_error(event_source.response)680                        for event in event_source.iter_sse():681                            if event.data == "[DONE]":682                                return683                            yield event.json()684685                return iter_sse()686            response = self.client.post(url="/chat/completions", json=kwargs)687            _raise_on_error(response)688            return response.json()689690        return _completion_with_retry(**kwargs)691692    def _combine_llm_outputs(self, llm_outputs: list[dict | None]) -> dict:693        overall_token_usage: dict = {}694        for output in llm_outputs:695            if output is None:696                # Happens in streaming697                continue698            token_usage = output["token_usage"]699            if token_usage is not None:700                for k, v in token_usage.items():701                    if k in overall_token_usage:702                        overall_token_usage[k] += v703                    else:704                        overall_token_usage[k] = v705        return {"token_usage": overall_token_usage, "model_name": self.model}706707    @model_validator(mode="after")708    def _set_mistralai_version(self) -> Self:709        """Set package version in metadata."""710        self._add_version("langchain-mistralai", __version__)711        return self712713    @model_validator(mode="after")714    def validate_environment(self) -> Self:715        """Validate api key, python package exists, temperature, and top_p."""716        if isinstance(self.mistral_api_key, SecretStr):717            api_key_str: str | None = self.mistral_api_key.get_secret_value()718        else:719            api_key_str = self.mistral_api_key720721        # TODO: handle retries722        base_url_str = (723            self.endpoint724            or os.environ.get("MISTRAL_BASE_URL")725            or "https://api.mistral.ai/v1"726        )727        self.endpoint = base_url_str728        if not self.client:729            self.client = httpx.Client(730                base_url=base_url_str,731                headers={732                    "Content-Type": "application/json",733                    "Accept": "application/json",734                    "Authorization": f"Bearer {api_key_str}",735                },736                timeout=self.timeout,737                verify=global_ssl_context,738            )739        # TODO: handle retries and max_concurrency740        if not self.async_client:741            self.async_client = httpx.AsyncClient(742                base_url=base_url_str,743                headers={744                    "Content-Type": "application/json",745                    "Accept": "application/json",746                    "Authorization": f"Bearer {api_key_str}",747                },748                timeout=self.timeout,749                verify=global_ssl_context,750            )751752        if self.temperature is not None and not 0 <= self.temperature <= 1:753            msg = "temperature must be in the range [0.0, 1.0]"754            raise ValueError(msg)755756        if self.top_p is not None and not 0 <= self.top_p <= 1:757            msg = "top_p must be in the range [0.0, 1.0]"758            raise ValueError(msg)759760        return self761762    def _resolve_model_profile(self) -> ModelProfile | None:763        return _get_default_model_profile(self.model) or None764765    def _generate(766        self,767        messages: list[BaseMessage],768        stop: list[str] | None = None,769        run_manager: CallbackManagerForLLMRun | None = None,770        stream: bool | None = None,  # noqa: FBT001771        **kwargs: Any,772    ) -> ChatResult:773        message_dicts, params = self._create_message_dicts(messages, stop)774        params = {**params, **kwargs}775        response = self.completion_with_retry(776            messages=message_dicts, run_manager=run_manager, **params777        )778        return self._create_chat_result(response)779780    def _create_chat_result(self, response: dict) -> ChatResult:781        generations = []782        token_usage = response.get("usage", {})783        for res in response["choices"]:784            finish_reason = res.get("finish_reason")785            message = _convert_mistral_chat_message_to_message(res["message"])786            if token_usage and isinstance(message, AIMessage):787                message.usage_metadata = {788                    "input_tokens": token_usage.get("prompt_tokens", 0),789                    "output_tokens": token_usage.get("completion_tokens", 0),790                    "total_tokens": token_usage.get("total_tokens", 0),791                }792            gen = ChatGeneration(793                message=message,794                generation_info={"finish_reason": finish_reason},795            )796            generations.append(gen)797798        llm_output = {799            "token_usage": token_usage,800            "model_name": self.model,801            "model": self.model,  # Backwards compatibility802        }803        return ChatResult(generations=generations, llm_output=llm_output)804805    def _create_message_dicts(806        self, messages: list[BaseMessage], stop: list[str] | None807    ) -> tuple[list[dict], dict[str, Any]]:808        params = self._client_params809        stop = stop if stop is not None else self.stop810        if stop:811            params["stop"] = stop812        message_dicts = [_convert_message_to_mistral_chat_message(m) for m in messages]813        return message_dicts, params814815    def _stream(816        self,817        messages: list[BaseMessage],818        stop: list[str] | None = None,819        run_manager: CallbackManagerForLLMRun | None = None,820        **kwargs: Any,821    ) -> Iterator[ChatGenerationChunk]:822        message_dicts, params = self._create_message_dicts(messages, stop)823        params = {**params, **kwargs, "stream": True}824825        default_chunk_class: type[BaseMessageChunk] = AIMessageChunk826        index = -1827        index_type = ""828        for chunk in self.completion_with_retry(829            messages=message_dicts, run_manager=run_manager, **params830        ):831            if len(chunk.get("choices", [])) == 0:832                continue833            new_chunk, index, index_type = _convert_chunk_to_message_chunk(834                chunk, default_chunk_class, index, index_type, self.output_version835            )836            # make future chunks same type as first chunk837            default_chunk_class = new_chunk.__class__838            gen_chunk = ChatGenerationChunk(message=new_chunk)839            if run_manager:840                run_manager.on_llm_new_token(841                    token=cast("str", new_chunk.content), chunk=gen_chunk842                )843            yield gen_chunk844845    async def _astream(846        self,847        messages: list[BaseMessage],848        stop: list[str] | None = None,849        run_manager: AsyncCallbackManagerForLLMRun | None = None,850        **kwargs: Any,851    ) -> AsyncIterator[ChatGenerationChunk]:852        message_dicts, params = self._create_message_dicts(messages, stop)853        params = {**params, **kwargs, "stream": True}854855        default_chunk_class: type[BaseMessageChunk] = AIMessageChunk856        index = -1857        index_type = ""858        async for chunk in await acompletion_with_retry(859            self, messages=message_dicts, run_manager=run_manager, **params860        ):861            if len(chunk.get("choices", [])) == 0:862                continue863            new_chunk, index, index_type = _convert_chunk_to_message_chunk(864                chunk, default_chunk_class, index, index_type, self.output_version865            )866            # make future chunks same type as first chunk867            default_chunk_class = new_chunk.__class__868            gen_chunk = ChatGenerationChunk(message=new_chunk)869            if run_manager:870                await run_manager.on_llm_new_token(871                    token=cast("str", new_chunk.content), chunk=gen_chunk872                )873            yield gen_chunk874875    async def _agenerate(876        self,877        messages: list[BaseMessage],878        stop: list[str] | None = None,879        run_manager: AsyncCallbackManagerForLLMRun | None = None,880        stream: bool | None = None,  # noqa: FBT001881        **kwargs: Any,882    ) -> ChatResult:883        message_dicts, params = self._create_message_dicts(messages, stop)884        params = {**params, **kwargs}885        response = await acompletion_with_retry(886            self, messages=message_dicts, run_manager=run_manager, **params887        )888        return self._create_chat_result(response)889890    def bind_tools(891        self,892        tools: Sequence[dict[str, Any] | type | Callable | BaseTool],893        tool_choice: dict | str | Literal["auto", "any"] | None = None,  # noqa: PYI051894        **kwargs: Any,895    ) -> Runnable[LanguageModelInput, AIMessage]:896        """Bind tool-like objects to this chat model.897898        Assumes model is compatible with OpenAI tool-calling API.899900        Args:901            tools: A list of tool definitions to bind to this chat model.902903                Supports any tool definition handled by [`convert_to_openai_tool`][langchain_core.utils.function_calling.convert_to_openai_tool].904            tool_choice: Which tool to require the model to call.905                Must be the name of the single provided function or906                `'auto'` to automatically determine which function to call907                (if any), or a dict of the form:908                {"type": "function", "function": {"name": <<tool_name>>}}.909            kwargs: Any additional parameters are passed directly to910                `self.bind(**kwargs)`.911        """  # noqa: E501912        formatted_tools = [convert_to_openai_tool(tool) for tool in tools]913        if tool_choice:914            tool_names = []915            for tool in formatted_tools:916                if ("function" in tool and (name := tool["function"].get("name"))) or (917                    name := tool.get("name")918                ):919                    tool_names.append(name)920                else:921                    pass922            if tool_choice in tool_names:923                kwargs["tool_choice"] = {924                    "type": "function",925                    "function": {"name": tool_choice},926                }927            else:928                kwargs["tool_choice"] = tool_choice929        return super().bind(tools=formatted_tools, **kwargs)930931    def with_structured_output(932        self,933        schema: dict | type | None = None,934        *,935        method: Literal[936            "function_calling", "json_mode", "json_schema"937        ] = "function_calling",938        include_raw: bool = False,939        **kwargs: Any,940    ) -> Runnable[LanguageModelInput, dict | BaseModel]:941        r"""Model wrapper that returns outputs formatted to match the given schema.942943        Args:944            schema: The output schema. Can be passed in as:945946                - An OpenAI function/tool schema,947                - A JSON Schema,948                - A `TypedDict` class,949                - Or a Pydantic class.950951                If `schema` is a Pydantic class then the model output will be a952                Pydantic instance of that class, and the model-generated fields will be953                validated by the Pydantic class. Otherwise the model output will be a954                dict and will not be validated.955956                See `langchain_core.utils.function_calling.convert_to_openai_tool` for957                more on how to properly specify types and descriptions of schema fields958                when specifying a Pydantic or `TypedDict` class.959960            method: The method for steering model generation, one of:961962                - `'function_calling'`:963                    Uses Mistral's964                    [function-calling feature](https://docs.mistral.ai/capabilities/function_calling/).965                - `'json_schema'`:966                    Uses Mistral's967                    [structured output feature](https://docs.mistral.ai/capabilities/structured-output/custom_structured_output/).968                - `'json_mode'`:969                    Uses Mistral's970                    [JSON mode](https://docs.mistral.ai/capabilities/structured-output/json_mode/).971                    Note that if using JSON mode then you972                    must include instructions for formatting the output into the973                    desired schema into the model call.974975                !!! warning "Behavior changed in `langchain-mistralai` 0.2.5"976977                    Added method="json_schema"978979            include_raw:980                If `False` then only the parsed structured output is returned.981982                If an error occurs during model output parsing it will be raised.983984                If `True` then both the raw model response (a `BaseMessage`) and the985                parsed model response will be returned.986987                If an error occurs during output parsing it will be caught and returned988                as well.989990                The final output is always a `dict` with keys `'raw'`, `'parsed'`, and991                `'parsing_error'`.992993            kwargs: Any additional parameters are passed directly to994                `self.bind(**kwargs)`. This is useful for passing in995                parameters such as `tool_choice` or `tools` to control996                which tool the model should call, or to pass in parameters such as997                `stop` to control when the model should stop generating output.998999        Returns:1000            A `Runnable` that takes same inputs as a1001                `langchain_core.language_models.chat.BaseChatModel`. If `include_raw` is1002                `False` and `schema` is a Pydantic class, `Runnable` outputs an instance1003                of `schema` (i.e., a Pydantic object). Otherwise, if `include_raw` is1004                `False` then `Runnable` outputs a `dict`.10051006                If `include_raw` is `True`, then `Runnable` outputs a `dict` with keys:10071008                - `'raw'`: `BaseMessage`1009                - `'parsed'`: `None` if there was a parsing error, otherwise the type1010                    depends on the `schema` as described above.1011                - `'parsing_error'`: `BaseException | None`10121013        Example: schema=Pydantic class, method="function_calling", include_raw=False:10141015        ```python1016        from typing import Optional10171018        from langchain_mistralai import ChatMistralAI1019        from pydantic import BaseModel, Field102010211022        class AnswerWithJustification(BaseModel):1023            '''An answer to the user question along with justification for the answer.'''10241025            answer: str1026            # If we provide default values and/or descriptions for fields, these will be passed1027            # to the model. This is an important part of improving a model's ability to1028            # correctly return structured outputs.1029            justification: str | None = Field(1030                default=None, description="A justification for the answer."1031            )103210331034        model = ChatMistralAI(model="mistral-large-latest", temperature=0)1035        structured_model = model.with_structured_output(AnswerWithJustification)10361037        structured_model.invoke(1038            "What weighs more a pound of bricks or a pound of feathers"1039        )10401041        # -> AnswerWithJustification(1042        #     answer='They weigh the same',1043        #     justification='Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ.'1044        # )1045        ```10461047        Example: schema=Pydantic class, method="function_calling", include_raw=True:10481049        ```python1050        from langchain_mistralai import ChatMistralAI1051        from pydantic import BaseModel105210531054        class AnswerWithJustification(BaseModel):1055            '''An answer to the user question along with justification for the answer.'''10561057            answer: str1058            justification: str105910601061        model = ChatMistralAI(model="mistral-large-latest", temperature=0)1062        structured_model = model.with_structured_output(1063            AnswerWithJustification, include_raw=True1064        )10651066        structured_model.invoke(1067            "What weighs more a pound of bricks or a pound of feathers"1068        )1069        # -> {1070        #     'raw': AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_Ao02pnFYXD6GN1yzc0uXPsvF', 'function': {'arguments': '{"answer":"They weigh the same.","justification":"Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ."}', 'name': 'AnswerWithJustification'}, 'type': 'function'}]}),1071        #     'parsed': AnswerWithJustification(answer='They weigh the same.', justification='Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ.'),1072        #     'parsing_error': None1073        # }1074        ```10751076        Example: schema=TypedDict class, method="function_calling", include_raw=False:10771078        ```python1079        from typing_extensions import Annotated, TypedDict10801081        from langchain_mistralai import ChatMistralAI108210831084        class AnswerWithJustification(TypedDict):1085            '''An answer to the user question along with justification for the answer.'''10861087            answer: str1088            justification: Annotated[1089                str | None, None, "A justification for the answer."1090            ]109110921093        model = ChatMistralAI(model="mistral-large-latest", temperature=0)1094        structured_model = model.with_structured_output(AnswerWithJustification)10951096        structured_model.invoke(1097            "What weighs more a pound of bricks or a pound of feathers"1098        )1099        # -> {1100        #     'answer': 'They weigh the same',1101        #     'justification': 'Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume and density of the two substances differ.'1102        # }1103        ```11041105        Example: schema=OpenAI function schema, method="function_calling", include_raw=False:11061107        ```python1108        from langchain_mistralai import ChatMistralAI11091110        oai_schema = {1111            'name': 'AnswerWithJustification',1112            'description': 'An answer to the user question along with justification for the answer.',1113            'parameters': {1114                'type': 'object',1115                'properties': {1116                    'answer': {'type': 'string'},1117                    'justification': {'description': 'A justification for the answer.', 'type': 'string'}1118                },1119                'required': ['answer']1120            }11211122            model = ChatMistralAI(model="mistral-large-latest", temperature=0)1123            structured_model = model.with_structured_output(oai_schema)11241125            structured_model.invoke(1126                "What weighs more a pound of bricks or a pound of feathers"1127            )1128            # -> {1129            #     'answer': 'They weigh the same',1130            #     'justification': 'Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume and density of the two substances differ.'1131            # }1132        ```11331134        Example: schema=Pydantic class, method="json_mode", include_raw=True:11351136        ```python1137        from langchain_mistralai import ChatMistralAI1138        from pydantic import BaseModel113911401141        class AnswerWithJustification(BaseModel):1142            answer: str1143            justification: str114411451146        model = ChatMistralAI(model="mistral-large-latest", temperature=0)1147        structured_model = model.with_structured_output(1148            AnswerWithJustification, method="json_mode", include_raw=True1149        )11501151        structured_model.invoke(1152            "Answer the following question. "1153            "Make sure to return a JSON blob with keys 'answer' and 'justification'.\\n\\n"1154            "What's heavier a pound of bricks or a pound of feathers?"1155        )1156        # -> {1157        #     'raw': AIMessage(content='{\\n    "answer": "They are both the same weight.",\\n    "justification": "Both a pound of bricks and a pound of feathers weigh one pound. The difference lies in the volume and density of the materials, not the weight." \\n}'),1158        #     'parsed': AnswerWithJustification(answer='They are both the same weight.', justification='Both a pound of bricks and a pound of feathers weigh one pound. The difference lies in the volume and density of the materials, not the weight.'),1159        #     'parsing_error': None1160        # }1161        ```11621163        Example: schema=None, method="json_mode", include_raw=True:11641165        ```python1166        structured_model = model.with_structured_output(1167            method="json_mode", include_raw=True1168        )11691170        structured_model.invoke(1171            "Answer the following question. "1172            "Make sure to return a JSON blob with keys 'answer' and 'justification'.\\n\\n"1173            "What's heavier a pound of bricks or a pound of feathers?"1174        )1175        # -> {1176        #     'raw': AIMessage(content='{\\n    "answer": "They are both the same weight.",\\n    "justification": "Both a pound of bricks and a pound of feathers weigh one pound. The difference lies in the volume and density of the materials, not the weight." \\n}'),1177        #     'parsed': {1178        #         'answer': 'They are both the same weight.',1179        #         'justification': 'Both a pound of bricks and a pound of feathers weigh one pound. The difference lies in the volume and density of the materials, not the weight.'1180        #     },1181        #     'parsing_error': None1182        # }1183        ```1184        """  # noqa: E5011185        _ = kwargs.pop("strict", None)1186        if kwargs:1187            msg = f"Received unsupported arguments {kwargs}"1188            raise ValueError(msg)1189        is_pydantic_schema = isinstance(schema, type) and is_basemodel_subclass(schema)1190        if method == "function_calling":1191            if schema is None:1192                msg = (1193                    "schema must be specified when method is 'function_calling'. "1194                    "Received None."1195                )1196                raise ValueError(msg)1197            # TODO: Update to pass in tool name as tool_choice if/when Mistral supports1198            # specifying a tool.1199            llm = self.bind_tools(1200                [schema],1201                tool_choice="any",1202                ls_structured_output_format={1203                    "kwargs": {"method": "function_calling"},1204                    "schema": schema,1205                },1206            )1207            if is_pydantic_schema:1208                output_parser: OutputParserLike = PydanticToolsParser(1209                    tools=[schema],  # type: ignore[list-item]1210                    first_tool_only=True,  # type: ignore[list-item]1211                )1212            else:1213                key_name = convert_to_openai_tool(schema)["function"]["name"]1214                output_parser = JsonOutputKeyToolsParser(1215                    key_name=key_name, first_tool_only=True1216                )1217        elif method == "json_mode":1218            llm = self.bind(1219                response_format={"type": "json_object"},1220                ls_structured_output_format={1221                    "kwargs": {1222                        # this is correct - name difference with mistral api1223                        "method": "json_mode"1224                    },1225                    "schema": schema,1226                },1227            )1228            output_parser = (1229                PydanticOutputParser(pydantic_object=schema)  # type: ignore[type-var, arg-type]1230                if is_pydantic_schema1231                else JsonOutputParser()1232            )1233        elif method == "json_schema":1234            if schema is None:1235                msg = (1236                    "schema must be specified when method is 'json_schema'. "1237                    "Received None."1238                )1239                raise ValueError(msg)1240            response_format = _convert_to_openai_response_format(schema, strict=True)1241            llm = self.bind(1242                response_format=response_format,1243                ls_structured_output_format={1244                    "kwargs": {"method": "json_schema"},1245                    "schema": schema,1246                },1247            )12481249            output_parser = (1250                PydanticOutputParser(pydantic_object=schema)  # type: ignore[arg-type]1251                if is_pydantic_schema1252                else JsonOutputParser()1253            )1254        if include_raw:1255            parser_assign = RunnablePassthrough.assign(1256                parsed=itemgetter("raw") | output_parser, parsing_error=lambda _: None1257            )1258            parser_none = RunnablePassthrough.assign(parsed=lambda _: None)1259            parser_with_fallback = parser_assign.with_fallbacks(1260                [parser_none], exception_key="parsing_error"1261            )1262            return RunnableMap(raw=llm) | parser_with_fallback1263        return llm | output_parser12641265    @property1266    def _identifying_params(self) -> dict[str, Any]:1267        """Get the identifying parameters."""1268        return self._default_params12691270    @property1271    def _llm_type(self) -> str:1272        """Return type of chat model."""1273        return "mistralai-chat"12741275    @property1276    def lc_secrets(self) -> dict[str, str]:1277        return {"mistral_api_key": "MISTRAL_API_KEY"}12781279    @classmethod1280    def is_lc_serializable(cls) -> bool:1281        """Return whether this model can be serialized by LangChain."""1282        return True12831284    @classmethod1285    def get_lc_namespace(cls) -> list[str]:1286        """Get the namespace of the LangChain object.12871288        Returns:1289            `["langchain", "chat_models", "mistralai"]`1290        """1291        return ["langchain", "chat_models", "mistralai"]129212931294def _convert_to_openai_response_format(1295    schema: dict[str, Any] | type, *, strict: bool | None = None1296) -> dict:1297    """Perform same op as in ChatOpenAI, but do not pass through Pydantic BaseModels."""1298    if (1299        isinstance(schema, dict)1300        and "json_schema" in schema1301        and schema.get("type") == "json_schema"1302    ):1303        response_format = schema1304    elif isinstance(schema, dict) and "name" in schema and "schema" in schema:1305        response_format = {"type": "json_schema", "json_schema": schema}1306    else:1307        if strict is None:1308            if isinstance(schema, dict) and isinstance(schema.get("strict"), bool):1309                strict = schema["strict"]1310            else:1311                strict = False1312        function = convert_to_openai_tool(schema, strict=strict)["function"]1313        function["schema"] = function.pop("parameters")1314        response_format = {"type": "json_schema", "json_schema": function}13151316    if (1317        strict is not None1318        and strict is not response_format["json_schema"].get("strict")1319        and isinstance(schema, dict)1320    ):1321        msg = (1322            f"Output schema already has 'strict' value set to "1323            f"{schema['json_schema']['strict']} but 'strict' also passed in to "1324            f"with_structured_output as {strict}. Please make sure that "1325            f"'strict' is only specified in one place."1326        )1327        raise ValueError(msg)1328    return response_format

Code quality findings 36

Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not isinstance(content, list):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(block, dict) and block.get("type") == "reference":
Ensure functions have docstrings for documentation
missing-docstring
async def acompletion_with_retry(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if output_version == "v1" and isinstance(content, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(content, list):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(block, dict):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if block.get("type") == "thinking" and isinstance(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(sub_block, dict) and "index" not in sub_block:
Ensure try blocks have corresponding except or finally blocks
try-without-except
try:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if _choice.get("finish_reason") is not None and isinstance(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if block.get("type") == "thinking" and isinstance(block.get("thinking"), list):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(sb, dict) and "index" in sb
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not isinstance(content, list):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(block, dict) and block.get("type") == "text" and "text" in block:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not isinstance(content, list):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(block, dict) and is_data_content_block(block):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(message, ChatMessage):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(message, HumanMessage):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(message, AIMessage):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif isinstance(content, list):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
_clean_block(block) if isinstance(block, dict) else block
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
block if isinstance(block, dict) else {"type": "text", "text": block}
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(message, SystemMessage):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(message, ToolMessage):
Ensure functions have docstrings for documentation
missing-docstring
def completion_with_retry(
Ensure functions have docstrings for documentation
missing-docstring
def iter_sse() -> Iterator[dict]:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(self.mistral_api_key, SecretStr):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if token_usage and isinstance(message, AIMessage):
Ensure functions have docstrings for documentation
missing-docstring
def bind_tools(
Ensure functions have docstrings for documentation
missing-docstring
def with_structured_output(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
is_pydantic_schema = isinstance(schema, type) and is_basemodel_subclass(schema)
Ensure functions have docstrings for documentation
missing-docstring
def lc_secrets(self) -> dict[str, str]:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
isinstance(schema, dict)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif isinstance(schema, dict) and "name" in schema and "schema" in schema:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(schema, dict) and isinstance(schema.get("strict"), bool):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
and isinstance(schema, dict)

Get this view in your editor

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