Avoid due to security risks; use ast.literal_eval for safer evaluation of literals
return ast.literal_eval(json_string)
1"""Ollama chat models.23**Input Flow (LangChain -> Ollama)**45`_convert_messages_to_ollama_messages()`:67- Transforms LangChain messages to `ollama.Message` format8- Extracts text content, images (base64), and tool calls910`_chat_params()`:1112- Combines messages with model parameters (temperature, top_p, etc.)13- Attaches tools if provided14- Configures reasoning/thinking mode via `think` parameter15- Sets output format (raw, JSON, or JSON schema)1617**Output Flow (Ollama -> LangChain)**18191. **Ollama Response**2021Stream dictionary chunks containing:22- `message`: Dict with `role`, `content`, `tool_calls`, `thinking`23- `done`: Boolean indicating completion24- `done_reason`: Reason for completion (`stop`, `length`, `load`)25- Token counts/timing metadata26272. **Response Processing** (`_iterate_over_stream()`)2829- Extracts content from `message.content`30- Parses tool calls into `ToolCall`s31- Separates reasoning content when `reasoning=True` (stored in `additional_kwargs`)32- Builds usage metadata from token counts33343. **LangChain Output** (`ChatGenerationChunk` -> `AIMessage`)3536- **Streaming**: Yields `ChatGenerationChunk` with `AIMessageChunk` content37- **Non-streaming**: Returns `ChatResult` with complete `AIMessage`38- Tool calls attached to `AIMessage.tool_calls`39- Reasoning content in `AIMessage.additional_kwargs['reasoning_content']`40"""4142from __future__ import annotations4344import ast45import json46import logging47import warnings48from collections.abc import AsyncIterator, Callable, Iterator, Mapping, Sequence49from operator import itemgetter50from typing import Any, Literal, cast51from uuid import uuid45253from langchain_core.callbacks import CallbackManagerForLLMRun54from langchain_core.callbacks.manager import AsyncCallbackManagerForLLMRun55from langchain_core.exceptions import OutputParserException56from langchain_core.language_models import LanguageModelInput57from langchain_core.language_models.chat_models import BaseChatModel, LangSmithParams58from langchain_core.messages import (59 AIMessage,60 AIMessageChunk,61 BaseMessage,62 ChatMessage,63 HumanMessage,64 SystemMessage,65 ToolCall,66 ToolMessage,67 is_data_content_block,68)69from langchain_core.messages import content as types70from langchain_core.messages.ai import UsageMetadata71from langchain_core.messages.tool import tool_call72from langchain_core.output_parsers import (73 JsonOutputKeyToolsParser,74 JsonOutputParser,75 PydanticOutputParser,76 PydanticToolsParser,77)78from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult79from langchain_core.runnables import Runnable, RunnableMap, RunnablePassthrough80from langchain_core.tools import BaseTool81from langchain_core.utils.function_calling import (82 convert_to_json_schema,83 convert_to_openai_tool,84)85from langchain_core.utils.pydantic import TypeBaseModel, is_basemodel_subclass86from ollama import AsyncClient, Client, Message87from pydantic import BaseModel, PrivateAttr, field_validator, model_validator88from pydantic.json_schema import JsonSchemaValue89from pydantic.v1 import BaseModel as BaseModelV190from typing_extensions import Self, is_typeddict9192from langchain_ollama._compat import _convert_from_v1_to_ollama93from langchain_ollama._utils import (94 merge_auth_headers,95 parse_url_with_auth,96 validate_model,97)98from langchain_ollama._version import __version__99100log = logging.getLogger(__name__)101102103def _get_usage_metadata_from_generation_info(104 generation_info: Mapping[str, Any] | None,105) -> UsageMetadata | None:106 """Get usage metadata from Ollama generation info mapping."""107 if generation_info is None:108 return None109 input_tokens: int | None = generation_info.get("prompt_eval_count")110 output_tokens: int | None = generation_info.get("eval_count")111 if input_tokens is not None and output_tokens is not None:112 return UsageMetadata(113 input_tokens=input_tokens,114 output_tokens=output_tokens,115 total_tokens=input_tokens + output_tokens,116 )117 return None118119120def _parse_json_string(121 json_string: str,122 *,123 raw_tool_call: dict[str, Any],124 skip: bool,125) -> Any:126 """Attempt to parse a JSON string for tool calling.127128 It first tries to use the standard `json.loads`. If that fails, it falls129 back to `ast.literal_eval` to safely parse Python literals, which is more130 robust against models using single quotes or containing apostrophes.131132 Args:133 json_string: JSON string to parse.134 raw_tool_call: Raw tool call to include in error message.135 skip: Whether to ignore parsing errors and return the value anyways.136137 Returns:138 The parsed JSON string or Python literal.139140 Raises:141 OutputParserException: If the string is invalid and `skip=False`.142 """143 try:144 return json.loads(json_string)145 except json.JSONDecodeError:146 try:147 # Use ast.literal_eval to safely parse Python-style dicts148 # (e.g. with single quotes)149 return ast.literal_eval(json_string)150 except (SyntaxError, ValueError) as e:151 # If both fail, and we're not skipping, raise an informative error.152 if skip:153 return json_string154 msg = (155 f"Function {raw_tool_call['function']['name']} arguments:\n\n"156 f"{raw_tool_call['function']['arguments']}"157 "\n\nare not valid JSON or a Python literal. "158 f"Received error: {e}"159 )160 raise OutputParserException(msg) from e161 except TypeError as e:162 if skip:163 return json_string164 msg = (165 f"Function {raw_tool_call['function']['name']} arguments:\n\n"166 f"{raw_tool_call['function']['arguments']}\n\nare not a string or a "167 f"dictionary. Received TypeError {e}"168 )169 raise OutputParserException(msg) from e170171172def _parse_arguments_from_tool_call(173 raw_tool_call: dict[str, Any],174) -> dict[str, Any] | None:175 """Parse arguments by trying to parse any shallowly nested string-encoded JSON.176177 Band-aid fix for issue in Ollama with inconsistent tool call argument structure.178 Should be removed/changed if fixed upstream.179180 See https://github.com/ollama/ollama/issues/6155181 """182 if "function" not in raw_tool_call:183 return None184 function_name = raw_tool_call["function"]["name"]185 arguments = raw_tool_call["function"]["arguments"]186 parsed_arguments: dict = {}187 if isinstance(arguments, dict):188 for key, value in arguments.items():189 # Filter out metadata fields like 'functionName' that echo function name190 if key == "functionName" and value == function_name:191 continue192 if isinstance(value, str):193 parsed_value = _parse_json_string(194 value, skip=True, raw_tool_call=raw_tool_call195 )196 if isinstance(parsed_value, (dict, list)):197 parsed_arguments[key] = parsed_value198 else:199 parsed_arguments[key] = value200 else:201 parsed_arguments[key] = value202 else:203 parsed_arguments = _parse_json_string(204 arguments, skip=False, raw_tool_call=raw_tool_call205 )206 return parsed_arguments207208209def _get_tool_calls_from_response(210 response: Mapping[str, Any],211) -> list[ToolCall]:212 """Get tool calls from Ollama response."""213 tool_calls = []214 if "message" in response and (215 raw_tool_calls := response["message"].get("tool_calls")216 ):217 tool_calls.extend(218 [219 tool_call(220 id=str(uuid4()),221 name=tc["function"]["name"],222 args=_parse_arguments_from_tool_call(tc) or {},223 )224 for tc in raw_tool_calls225 ]226 )227 return tool_calls228229230def _lc_tool_call_to_openai_tool_call(tool_call_: ToolCall) -> dict:231 """Convert a LangChain tool call to an OpenAI tool call format."""232 return {233 "type": "function",234 "id": tool_call_["id"],235 "function": {236 "name": tool_call_["name"],237 "arguments": tool_call_["args"],238 },239 }240241242def _get_image_from_data_content_block(block: dict) -> str:243 """Format standard data content block to format expected by Ollama."""244 if block["type"] == "image":245 if block.get("source_type") == "base64":246 # v0 style247 return block["data"]248 if block.get("base64"):249 # v1 content blocks250 return block["base64"]251 error_message = "Image data only supported through in-line base64 format."252 raise ValueError(error_message)253254 error_message = f"Blocks of type {block['type']} not supported."255 raise ValueError(error_message)256257258def _is_pydantic_class(obj: Any) -> bool:259 return isinstance(obj, type) and is_basemodel_subclass(obj)260261262class ChatOllama(BaseChatModel):263 r"""Ollama chat model integration.264265 ???+ note "Setup"266267 Install `langchain-ollama` and download any models you want to use from ollama.268269 ```bash270 ollama pull gpt-oss:20b271 pip install -U langchain-ollama272 ```273274 Key init args — completion params:275 model: str276 Name of Ollama model to use.277 reasoning: bool | None278 Controls the reasoning/thinking mode for279 [supported models](https://ollama.com/search?c=thinking).280281 - `True`: Enables reasoning mode. The model's reasoning process will be282 captured and returned separately in the `additional_kwargs` of the283 response message, under `reasoning_content`. The main response284 content will not include the reasoning tags.285 - `False`: Disables reasoning mode. The model will not perform any reasoning,286 and the response will not include any reasoning content.287 - `None` (Default): The model will use its default reasoning behavior. Note288 however, if the model's default behavior *is* to perform reasoning, think tags289 (`<think>` and `</think>`) will be present within the main response content290 unless you set `reasoning` to `True`.291 temperature: float292 Sampling temperature. Ranges from `0.0` to `1.0`.293 num_predict: int | None294 Max number of tokens to generate.295296 See full list of supported init args and their descriptions in the params section.297298 Instantiate:299 ```python300 from langchain_ollama import ChatOllama301302 model = ChatOllama(303 model="gpt-oss:20b",304 validate_model_on_init=True,305 temperature=0.8,306 num_predict=256,307 # other params ...308 )309 ```310311 Invoke:312 ```python313 messages = [314 ("system", "You are a helpful translator. Translate the user sentence to French."),315 ("human", "I love programming."),316 ]317 model.invoke(messages)318 ```319320 ```python321 AIMessage(content='J'adore le programmation. (Note: "programming" can also refer to the act of writing code, so if you meant that, I could translate it as "J'adore programmer". But since you didn\'t specify, I assumed you were talking about the activity itself, which is what "le programmation" usually refers to.)', response_metadata={'model': 'llama3', 'created_at': '2024-07-04T03:37:50.182604Z', 'message': {'role': 'assistant', 'content': ''}, 'done_reason': 'stop', 'done': True, 'total_duration': 3576619666, 'load_duration': 788524916, 'prompt_eval_count': 32, 'prompt_eval_duration': 128125000, 'eval_count': 71, 'eval_duration': 2656556000}, id='run-ba48f958-6402-41a5-b461-5e250a4ebd36-0')322 ```323324 Stream:325 ```python326 for chunk in model.stream("Return the words Hello World!"):327 print(chunk.text, end="")328 ```329330 ```python331 content='Hello' id='run-327ff5ad-45c8-49fe-965c-0a93982e9be1'332 content=' World' id='run-327ff5ad-45c8-49fe-965c-0a93982e9be1'333 content='!' id='run-327ff5ad-45c8-49fe-965c-0a93982e9be1'334 content='' response_metadata={'model': 'llama3', 'created_at': '2024-07-04T03:39:42.274449Z', 'message': {'role': 'assistant', 'content': ''}, 'done_reason': 'stop', 'done': True, 'total_duration': 411875125, 'load_duration': 1898166, 'prompt_eval_count': 14, 'prompt_eval_duration': 297320000, 'eval_count': 4, 'eval_duration': 111099000} id='run-327ff5ad-45c8-49fe-965c-0a93982e9be1'335336 ```337338 ```python339 stream = model.stream(messages)340 full = next(stream)341 for chunk in stream:342 full += chunk343 full344 ```345346 ```python347 AIMessageChunk(348 content='Je adore le programmation.(Note: "programmation" is the formal way to say "programming" in French, but informally, people might use the phrase "le développement logiciel" or simply "le code")',349 response_metadata={350 "model": "llama3",351 "created_at": "2024-07-04T03:38:54.933154Z",352 "message": {"role": "assistant", "content": ""},353 "done_reason": "stop",354 "done": True,355 "total_duration": 1977300042,356 "load_duration": 1345709,357 "prompt_eval_duration": 159343000,358 "eval_count": 47,359 "eval_duration": 1815123000,360 },361 id="run-3c81a3ed-3e79-4dd3-a796-04064d804890",362 )363 ```364365 Async:366 ```python367 await model.ainvoke("Hello how are you!")368 ```369370 ```python371 AIMessage(372 content="Hi there! I'm just an AI, so I don't have feelings or emotions like humans do. But I'm functioning properly and ready to help with any questions or tasks you may have! How can I assist you today?",373 response_metadata={374 "model": "llama3",375 "created_at": "2024-07-04T03:52:08.165478Z",376 "message": {"role": "assistant", "content": ""},377 "done_reason": "stop",378 "done": True,379 "total_duration": 2138492875,380 "load_duration": 1364000,381 "prompt_eval_count": 10,382 "prompt_eval_duration": 297081000,383 "eval_count": 47,384 "eval_duration": 1838524000,385 },386 id="run-29c510ae-49a4-4cdd-8f23-b972bfab1c49-0",387 )388 ```389390 ```python391 async for chunk in model.astream("Say hello world!"):392 print(chunk.content)393 ```394395 ```python396 HEL397 LO398 WORLD399 !400 ```401402 ```python403 messages = [("human", "Say hello world!"), ("human", "Say goodbye world!")]404 await model.abatch(messages)405 ```406407 ```python408 [409 AIMessage(410 content="HELLO, WORLD!",411 response_metadata={412 "model": "llama3",413 "created_at": "2024-07-04T03:55:07.315396Z",414 "message": {"role": "assistant", "content": ""},415 "done_reason": "stop",416 "done": True,417 "total_duration": 1696745458,418 "load_duration": 1505000,419 "prompt_eval_count": 8,420 "prompt_eval_duration": 111627000,421 "eval_count": 6,422 "eval_duration": 185181000,423 },424 id="run-da6c7562-e25a-4a44-987a-2c83cd8c2686-0",425 ),426 AIMessage(427 content="It's been a blast chatting with you! Say goodbye to the world for me, and don't forget to come back and visit us again soon!",428 response_metadata={429 "model": "llama3",430 "created_at": "2024-07-04T03:55:07.018076Z",431 "message": {"role": "assistant", "content": ""},432 "done_reason": "stop",433 "done": True,434 "total_duration": 1399391083,435 "load_duration": 1187417,436 "prompt_eval_count": 20,437 "prompt_eval_duration": 230349000,438 "eval_count": 31,439 "eval_duration": 1166047000,440 },441 id="run-96cad530-6f3e-4cf9-86b4-e0f8abba4cdb-0",442 ),443 ]444 ```445446 JSON mode:447 ```python448 json_model = ChatOllama(format="json")449 json_model.invoke(450 "Return a query for the weather in a random location and time of day with two keys: location and time_of_day. "451 "Respond using JSON only."452 ).content453 ```454455 ```python456 '{"location": "Pune, India", "time_of_day": "morning"}'457 ```458459 Tool Calling:460 ```python461 from langchain_ollama import ChatOllama462 from pydantic import BaseModel, Field463464465 class Multiply(BaseModel):466 a: int = Field(..., description="First integer")467 b: int = Field(..., description="Second integer")468469470 ans = await chat.invoke("What is 45*67")471 ans.tool_calls472 ```473474 ```python475 [476 {477 "name": "Multiply",478 "args": {"a": 45, "b": 67},479 "id": "420c3f3b-df10-4188-945f-eb3abdb40622",480 "type": "tool_call",481 }482 ]483 ```484485 Thinking / Reasoning:486 You can enable reasoning mode for models that support it by setting487 the `reasoning` parameter to `True` in either the constructor or488 the `invoke`/`stream` methods. This will enable the model to think489 through the problem and return the reasoning process separately in the490 `additional_kwargs` of the response message, under `reasoning_content`.491492 If `reasoning` is set to `None`, the model will use its default reasoning493 behavior, and any reasoning content will *not* be captured under the494 `reasoning_content` key, but will be present within the main response content495 as think tags (`<think>` and `</think>`).496497 !!! note498 This feature is only available for [models that support reasoning](https://ollama.com/search?c=thinking).499500 ```python501 from langchain_ollama import ChatOllama502503 model = ChatOllama(504 model="deepseek-r1:8b",505 validate_model_on_init=True,506 reasoning=True,507 )508509 model.invoke("how many r in the word strawberry?")510511 # or, on an invocation basis:512513 model.invoke("how many r in the word strawberry?", reasoning=True)514 # or model.stream("how many r in the word strawberry?", reasoning=True)515516 # If not provided, the invocation will default to the ChatOllama reasoning517 # param provided (None by default).518 ```519520 ```python521 AIMessage(content='The word "strawberry" contains **three \'r\' letters**. Here\'s a breakdown for clarity:\n\n- The spelling of "strawberry" has two parts ... be 3.\n\nTo be thorough, let\'s confirm with an online source or common knowledge.\n\nI can recall that "strawberry" has: s-t-r-a-w-b-e-r-r-y — yes, three r\'s.\n\nPerhaps it\'s misspelled by some, but standard is correct.\n\nSo I think the response should be 3.\n'}, response_metadata={'model': 'deepseek-r1:8b', 'created_at': '2025-07-08T19:33:55.891269Z', 'done': True, 'done_reason': 'stop', 'total_duration': 98232561292, 'load_duration': 28036792, 'prompt_eval_count': 10, 'prompt_eval_duration': 40171834, 'eval_count': 3615, 'eval_duration': 98163832416, 'model_name': 'deepseek-r1:8b'}, id='run--18f8269f-6a35-4a7c-826d-b89d52c753b3-0', usage_metadata={'input_tokens': 10, 'output_tokens': 3615, 'total_tokens': 3625})522523 ```524 """ # noqa: E501, pylint: disable=line-too-long525526 model: str527 """Model name to use."""528529 reasoning: bool | str | None = None530 """Controls the reasoning/thinking mode for [supported models](https://ollama.com/search?c=thinking).531532 - `True`: Enables reasoning mode. The model's reasoning process will be533 captured and returned separately in the `additional_kwargs` of the534 response message, under `reasoning_content`. The main response535 content will not include the reasoning tags.536 - `False`: Disables reasoning mode. The model will not perform any reasoning,537 and the response will not include any reasoning content.538 - `None` (Default): The model will use its default reasoning behavior. Note539 however, if the model's default behavior *is* to perform reasoning, think tags540 (`<think>` and `</think>`) will be present within the main response content541 unless you set `reasoning` to `True`.542 - `str`: e.g. `'low'`, `'medium'`, `'high'`. Enables reasoning with a custom543 intensity level. Currently, this is only supported `gpt-oss`. See the544 [Ollama docs](https://github.com/ollama/ollama-python/blob/da79e987f0ac0a4986bf396f043b36ef840370bc/ollama/_types.py#L210)545 for more information.546 """547548 validate_model_on_init: bool = False549 """Whether to validate the model exists in Ollama locally on initialization.550551 !!! version-added "Added in `langchain-ollama` 0.3.4"552 """553554 mirostat: int | None = None555 """Enable Mirostat sampling for controlling perplexity.556557 (Default: `0`, `0` = disabled, `1` = Mirostat, `2` = Mirostat 2.0)558 """559560 mirostat_eta: float | None = None561 """Influences how quickly the algorithm responds to feedback from generated text.562563 A lower learning rate will result in slower adjustments, while a higher learning564 rate will make the algorithm more responsive.565566 (Default: `0.1`)567 """568569 mirostat_tau: float | None = None570 """Controls the balance between coherence and diversity of the output.571572 A lower value will result in more focused and coherent text.573574 (Default: `5.0`)575 """576577 num_ctx: int | None = None578 """Sets the size of the context window used to generate the next token.579580 (Default: `2048`)581 """582583 num_gpu: int | None = None584 """The number of GPUs to use.585586 On macOS it defaults to `1` to enable metal support, `0` to disable.587 """588589 num_thread: int | None = None590 """Sets the number of threads to use during computation.591592 By default, Ollama will detect this for optimal performance. It is recommended to593 set this value to the number of physical CPU cores your system has (as opposed to594 the logical number of cores).595 """596597 num_predict: int | None = None598 """Maximum number of tokens to predict when generating text.599600 (Default: `128`, `-1` = infinite generation, `-2` = fill context)601 """602603 repeat_last_n: int | None = None604 """Sets how far back for the model to look back to prevent repetition.605606 (Default: `64`, `0` = disabled, `-1` = `num_ctx`)607 """608609 repeat_penalty: float | None = None610 """Sets how strongly to penalize repetitions.611612 A higher value (e.g., `1.5`) will penalize repetitions more strongly, while a613 lower value (e.g., `0.9`) will be more lenient. (Default: `1.1`)614 """615616 temperature: float | None = None617 """The temperature of the model.618619 Increasing the temperature will make the model answer more creatively.620621 (Default: `0.8`)622 """623624 seed: int | None = None625 """Sets the random number seed to use for generation.626627 Setting this to a specific number will make the model generate the same text for the628 same prompt.629 """630631 logprobs: bool | None = None632 """Whether to return logprobs.633634 !!! note635636 When streaming, per-token logprobs are available on each intermediate637 chunk (via `response_metadata["logprobs"]`) and are accumulated into the638 final aggregated response when using `invoke()`.639 """640641 top_logprobs: int | None = None642 """Number of most likely tokens to return at each token position, each with643 an associated log probability. Must be a positive integer.644645 If set without `logprobs=True`, `logprobs` will be enabled automatically.646 """647648 @field_validator("top_logprobs")649 @classmethod650 def _validate_top_logprobs(cls, v: int | None) -> int | None:651 if v is not None and v < 1:652 msg = "`top_logprobs` must be a positive integer."653 raise ValueError(msg)654 return v655656 stop: list[str] | None = None657 """Sets the stop tokens to use."""658659 tfs_z: float | None = None660 """Tail free sampling.661662 Used to reduce the impact of less probable tokens from the output.663664 A higher value (e.g., `2.0`) will reduce the impact more, while a value of `1.0`665 disables this setting.666667 (Default: `1`)668 """669670 top_k: int | None = None671 """Reduces the probability of generating nonsense.672673 A higher value (e.g. `100`) will give more diverse answers, while a lower value674 (e.g. `10`) will be more conservative.675676 (Default: `40`)677 """678679 top_p: float | None = None680 """Works together with top-k.681682 A higher value (e.g., `0.95`) will lead to more diverse text, while a lower value683 (e.g., `0.5`) will generate more focused and conservative text.684685 (Default: `0.9`)686 """687688 format: Literal["", "json"] | JsonSchemaValue | None = None689 """Specify the format of the output (options: `'json'`, JSON schema)."""690691 keep_alive: int | str | None = None692 """How long the model will stay loaded into memory."""693694 base_url: str | None = None695 """Base url the model is hosted under.696697 If none, defaults to the Ollama client default.698699 Supports `userinfo` auth in the format `http://username:password@localhost:11434`.700 Useful if your Ollama server is behind a proxy.701702 !!! warning703 `userinfo` is not secure and should only be used for local testing or704 in secure environments. Avoid using it in production or over unsecured705 networks.706707 !!! note708 If using `userinfo`, ensure that the Ollama server is configured to709 accept and validate these credentials.710711 !!! note712 `userinfo` headers are passed to both sync and async clients.713714 """715716 client_kwargs: dict | None = {}717 """Additional kwargs to pass to the httpx clients. Pass headers in here.718719 These arguments are passed to both synchronous and async clients.720721 Use `sync_client_kwargs` and `async_client_kwargs` to pass different arguments722 to synchronous and asynchronous clients.723 """724725 async_client_kwargs: dict | None = {}726 """Additional kwargs to merge with `client_kwargs` before passing to httpx client.727728 These are clients unique to the async client; for shared args use `client_kwargs`.729730 For a full list of the params, see the [httpx documentation](https://www.python-httpx.org/api/#asyncclient).731 """732733 sync_client_kwargs: dict | None = {}734 """Additional kwargs to merge with `client_kwargs` before passing to httpx client.735736 These are clients unique to the sync client; for shared args use `client_kwargs`.737738 For a full list of the params, see the [httpx documentation](https://www.python-httpx.org/api/#client).739 """740741 _client: Client = PrivateAttr()742 """The client to use for making requests."""743744 _async_client: AsyncClient = PrivateAttr()745 """The async client to use for making requests."""746747 def _chat_params(748 self,749 messages: list[BaseMessage],750 stop: list[str] | None = None,751 **kwargs: Any,752 ) -> dict[str, Any]:753 """Assemble the parameters for a chat completion request.754755 Args:756 messages: List of LangChain messages to send to the model.757 stop: Optional list of stop tokens to use for this invocation.758 **kwargs: Additional keyword arguments to include in the request.759760 Returns:761 A dictionary of parameters to pass to the Ollama client.762 """763 ollama_messages = self._convert_messages_to_ollama_messages(messages)764765 if self.stop is not None and stop is not None:766 msg = "`stop` found in both the input and default params."767 raise ValueError(msg)768 if self.stop is not None:769 stop = self.stop770771 options_dict = kwargs.pop("options", None)772 if options_dict is None:773 # Only include parameters that are explicitly set (not None)774 options_dict = {775 k: v776 for k, v in {777 "mirostat": self.mirostat,778 "mirostat_eta": self.mirostat_eta,779 "mirostat_tau": self.mirostat_tau,780 "num_ctx": self.num_ctx,781 "num_gpu": self.num_gpu,782 "num_thread": self.num_thread,783 "num_predict": self.num_predict,784 "repeat_last_n": self.repeat_last_n,785 "repeat_penalty": self.repeat_penalty,786 "temperature": self.temperature,787 "seed": self.seed,788 "stop": self.stop if stop is None else stop,789 "tfs_z": self.tfs_z,790 "top_k": self.top_k,791 "top_p": self.top_p,792 }.items()793 if v is not None794 }795796 format_param = self._resolve_format_param(797 kwargs.pop("format", self.format),798 kwargs.pop("response_format", None),799 )800801 params = {802 "messages": ollama_messages,803 "stream": kwargs.pop("stream", True),804 "model": kwargs.pop("model", self.model),805 "think": kwargs.pop("reasoning", self.reasoning),806 "format": format_param,807 "logprobs": kwargs.pop("logprobs", self.logprobs),808 "top_logprobs": kwargs.pop("top_logprobs", self.top_logprobs),809 "options": options_dict,810 "keep_alive": kwargs.pop("keep_alive", self.keep_alive),811 **kwargs,812 }813814 # Filter out 'strict' argument if present, as it is not supported by Ollama815 # but may be passed by upstream libraries (e.g. LangChain ProviderStrategy)816 if "strict" in params:817 params.pop("strict")818819 if tools := kwargs.get("tools"):820 params["tools"] = tools821822 return params823824 def _resolve_format_param(825 self,826 format_param: str | dict[str, Any] | None,827 response_format: Any | None,828 ) -> str | dict[str, Any] | None:829 """Resolve the format parameter.830831 Converts an OpenAI-style `response_format` dict to the `format`832 parameter expected by Ollama.833834 Args:835 format_param: The explicit `format` value (takes priority).836 response_format: An OpenAI-style `response_format` dict.837838 Returns:839 The resolved format value to pass to the Ollama client.840 """841 if format_param is not None:842 if response_format is not None:843 warnings.warn(844 "Both 'format' and 'response_format' were provided. "845 "'response_format' will be ignored in favor of 'format'.",846 UserWarning,847 stacklevel=2,848 )849 return format_param850851 if response_format is None:852 return None853854 return self._convert_response_format(response_format)855856 def _convert_response_format(857 self,858 response_format: Any,859 ) -> str | dict[str, Any] | None:860 """Convert an OpenAI-style `response_format` to an Ollama `format` value.861862 Args:863 response_format: The `response_format` value to convert.864865 Returns:866 The Ollama-compatible `format` value, or `None` if conversion fails.867 """868 if not isinstance(response_format, dict):869 warnings.warn(870 f"Ignored invalid 'response_format' type: {type(response_format)}. "871 "Expected a dictionary.",872 UserWarning,873 stacklevel=2,874 )875 return None876877 fmt_type = response_format.get("type")878 if fmt_type == "json_object":879 return "json"880 if fmt_type == "json_schema":881 return self._extract_json_schema(response_format)882883 warnings.warn(884 f"Ignored unrecognized 'response_format' type: {fmt_type}. "885 "Expected 'json_object' or 'json_schema'.",886 UserWarning,887 stacklevel=2,888 )889 return None890891 def _extract_json_schema(892 self,893 response_format: dict[str, Any],894 ) -> dict[str, Any] | None:895 """Extract the raw JSON schema from an OpenAI `json_schema` envelope.896897 Args:898 response_format: A dict with `type: "json_schema"`.899900 Returns:901 The raw JSON schema dict, or `None` if extraction fails.902 """903 json_schema_block = response_format.get("json_schema")904 if not isinstance(json_schema_block, dict):905 warnings.warn(906 "response_format has type 'json_schema' but 'json_schema' "907 f"value is {type(json_schema_block)}, expected a dict "908 "containing a 'schema' key. "909 "The format parameter will not be set.",910 UserWarning,911 stacklevel=2,912 )913 return None914 schema = json_schema_block.get("schema")915 if schema is None:916 warnings.warn(917 "response_format has type 'json_schema' but no 'schema' "918 "key was found in 'json_schema'. "919 "The format parameter will not be set.",920 UserWarning,921 stacklevel=2,922 )923 return schema924925 @model_validator(mode="after")926 def _set_ollama_version(self) -> Self:927 """Set package version in metadata."""928 self._add_version("langchain-ollama", __version__)929 return self930931 @model_validator(mode="after")932 def _set_clients(self) -> Self:933 """Set clients to use for ollama."""934 if self.top_logprobs is not None and self.logprobs is not True:935 if self.logprobs is False:936 msg = (937 "`top_logprobs` is set but `logprobs` is explicitly `False`. "938 "Either set `logprobs=True` to use `top_logprobs`, or remove "939 "`top_logprobs`."940 )941 raise ValueError(msg)942 # logprobs is None (unset) — auto-enable as convenience943 self.logprobs = True944 warnings.warn(945 "`top_logprobs` is set but `logprobs` was not explicitly enabled. "946 "Setting `logprobs=True` automatically.",947 UserWarning,948 stacklevel=2,949 )950951 client_kwargs = self.client_kwargs or {}952953 cleaned_url, auth_headers = parse_url_with_auth(self.base_url)954 merge_auth_headers(client_kwargs, auth_headers)955956 sync_client_kwargs = client_kwargs957 if self.sync_client_kwargs:958 sync_client_kwargs = {**sync_client_kwargs, **self.sync_client_kwargs}959960 async_client_kwargs = client_kwargs961 if self.async_client_kwargs:962 async_client_kwargs = {**async_client_kwargs, **self.async_client_kwargs}963964 self._client = Client(host=cleaned_url, **sync_client_kwargs)965 self._async_client = AsyncClient(host=cleaned_url, **async_client_kwargs)966 if self.validate_model_on_init:967 validate_model(self._client, self.model)968 return self969970 def _convert_messages_to_ollama_messages(971 self, messages: list[BaseMessage]972 ) -> Sequence[Message]:973 """Convert a BaseMessage list to list of messages for Ollama to consume.974975 Args:976 messages: List of BaseMessage to convert.977978 Returns:979 List of messages in Ollama format.980 """981 messages = list(messages) # shallow copy to avoid mutating caller's list982 for idx, message in enumerate(messages):983 # Handle message content written in v1 format984 if (985 isinstance(message, AIMessage)986 and message.response_metadata.get("output_version") == "v1"987 ):988 # Unpack known v1 content to Ollama format for the request989 # Most types are passed through unchanged990 messages[idx] = message.model_copy(991 update={992 "content": _convert_from_v1_to_ollama(993 cast("list[types.ContentBlock]", message.content),994 message.response_metadata.get("model_provider"),995 )996 }997 )998999 ollama_messages: list = []1000 for message in messages:1001 role: str1002 tool_call_id: str | None = None1003 tool_calls: list[dict[str, Any]] | None = None1004 if isinstance(message, HumanMessage):1005 role = "user"1006 elif isinstance(message, AIMessage):1007 role = "assistant"1008 tool_calls = (1009 [1010 _lc_tool_call_to_openai_tool_call(tool_call)1011 for tool_call in message.tool_calls1012 ]1013 if message.tool_calls1014 else None1015 )1016 elif isinstance(message, SystemMessage):1017 role = "system"1018 elif isinstance(message, ChatMessage):1019 role = message.role1020 elif isinstance(message, ToolMessage):1021 role = "tool"1022 tool_call_id = message.tool_call_id1023 else:1024 msg = "Received unsupported message type for Ollama."1025 raise TypeError(msg)10261027 content = ""1028 images = []1029 if isinstance(message.content, str):1030 content = message.content1031 else: # List1032 for content_part in message.content:1033 if isinstance(content_part, str):1034 if content:1035 content += "\n"1036 content += content_part1037 elif content_part.get("type") == "text":1038 if content:1039 content += "\n"1040 content += content_part["text"]1041 elif content_part.get("type") == "tool_use":1042 continue1043 elif content_part.get("type") == "image_url":1044 image_url = None1045 temp_image_url = content_part.get("image_url")1046 if isinstance(temp_image_url, str):1047 image_url = temp_image_url1048 elif (1049 isinstance(temp_image_url, dict)1050 and "url" in temp_image_url1051 and isinstance(temp_image_url["url"], str)1052 ):1053 image_url = temp_image_url["url"]1054 else:1055 msg = (1056 "Only string image_url or dict with string 'url' "1057 "inside content parts are supported."1058 )1059 raise ValueError(msg)10601061 image_url_components = image_url.split(",")1062 # Support data:image/jpeg;base64,<image> format1063 # and base64 strings1064 if len(image_url_components) > 1:1065 images.append(image_url_components[1])1066 else:1067 images.append(image_url_components[0])1068 elif is_data_content_block(content_part):1069 # Handles v1 "image" type1070 image = _get_image_from_data_content_block(content_part)1071 images.append(image)1072 else:1073 msg = (1074 "Unsupported message content type. "1075 "Must either have type 'text' or type 'image_url' "1076 "with a string 'image_url' field."1077 )1078 raise ValueError(msg)1079 # Should convert to ollama.Message once role includes tool, and tool_call_id1080 # is in Message1081 msg_: dict = {1082 "role": role,1083 "content": content,1084 "images": images,1085 }1086 if tool_calls:1087 msg_["tool_calls"] = tool_calls1088 if tool_call_id:1089 msg_["tool_call_id"] = tool_call_id1090 if isinstance(message, AIMessage):1091 thinking = message.additional_kwargs.get("reasoning_content")1092 if thinking is not None:1093 msg_["thinking"] = thinking1094 ollama_messages.append(msg_)10951096 return ollama_messages10971098 async def _acreate_chat_stream(1099 self,1100 messages: list[BaseMessage],1101 stop: list[str] | None = None,1102 **kwargs: Any,1103 ) -> AsyncIterator[Mapping[str, Any] | str]:1104 if not self._async_client:1105 msg = (1106 "Ollama async client is not initialized. "1107 "Make sure the model was properly constructed."1108 )1109 raise RuntimeError(msg)1110 chat_params = self._chat_params(messages, stop, **kwargs)11111112 if chat_params["stream"]:1113 async for part in await self._async_client.chat(**chat_params):1114 yield part1115 else:1116 yield await self._async_client.chat(**chat_params)11171118 def _create_chat_stream(1119 self,1120 messages: list[BaseMessage],1121 stop: list[str] | None = None,1122 **kwargs: Any,1123 ) -> Iterator[Mapping[str, Any] | str]:1124 if not self._client:1125 msg = (1126 "Ollama sync client is not initialized. "1127 "Make sure the model was properly constructed."1128 )1129 raise RuntimeError(msg)1130 chat_params = self._chat_params(messages, stop, **kwargs)11311132 if chat_params["stream"]:1133 yield from self._client.chat(**chat_params)1134 else:1135 yield self._client.chat(**chat_params)11361137 def _chat_stream_with_aggregation(1138 self,1139 messages: list[BaseMessage],1140 stop: list[str] | None = None,1141 run_manager: CallbackManagerForLLMRun | None = None,1142 verbose: bool = False, # noqa: FBT0021143 **kwargs: Any,1144 ) -> ChatGenerationChunk:1145 final_chunk = None1146 for chunk in self._iterate_over_stream(messages, stop, **kwargs):1147 if final_chunk is None:1148 final_chunk = chunk1149 else:1150 final_chunk += chunk1151 if run_manager:1152 run_manager.on_llm_new_token(1153 chunk.text,1154 chunk=chunk,1155 verbose=verbose,1156 )1157 if final_chunk is None:1158 msg = "No data received from Ollama stream."1159 raise ValueError(msg)11601161 return final_chunk11621163 async def _achat_stream_with_aggregation(1164 self,1165 messages: list[BaseMessage],1166 stop: list[str] | None = None,1167 run_manager: AsyncCallbackManagerForLLMRun | None = None,1168 verbose: bool = False, # noqa: FBT0021169 **kwargs: Any,1170 ) -> ChatGenerationChunk:1171 final_chunk = None1172 async for chunk in self._aiterate_over_stream(messages, stop, **kwargs):1173 if final_chunk is None:1174 final_chunk = chunk1175 else:1176 final_chunk += chunk1177 if run_manager:1178 await run_manager.on_llm_new_token(1179 chunk.text,1180 chunk=chunk,1181 verbose=verbose,1182 )1183 if final_chunk is None:1184 msg = "No data received from Ollama stream."1185 raise ValueError(msg)11861187 return final_chunk11881189 def _get_ls_params(1190 self, stop: list[str] | None = None, **kwargs: Any1191 ) -> LangSmithParams:1192 """Get standard params for tracing."""1193 params = self._get_invocation_params(stop=stop, **kwargs)1194 ls_params = LangSmithParams(1195 ls_provider="ollama",1196 ls_model_name=params.get("model", self.model),1197 ls_model_type="chat",1198 ls_temperature=params.get("temperature", self.temperature),1199 )1200 if ls_stop := stop or params.get("stop", None) or self.stop:1201 ls_params["ls_stop"] = ls_stop1202 return ls_params12031204 def _generate(1205 self,1206 messages: list[BaseMessage],1207 stop: list[str] | None = None,1208 run_manager: CallbackManagerForLLMRun | None = None,1209 **kwargs: Any,1210 ) -> ChatResult:1211 final_chunk = self._chat_stream_with_aggregation(1212 messages, stop, run_manager, verbose=self.verbose, **kwargs1213 )1214 generation_info = final_chunk.generation_info1215 chat_generation = ChatGeneration(1216 message=AIMessage(1217 content=final_chunk.text,1218 usage_metadata=cast(1219 "AIMessageChunk", final_chunk.message1220 ).usage_metadata,1221 tool_calls=cast("AIMessageChunk", final_chunk.message).tool_calls,1222 additional_kwargs=final_chunk.message.additional_kwargs,1223 ),1224 generation_info=generation_info,1225 )1226 return ChatResult(generations=[chat_generation])12271228 def _iterate_over_stream(1229 self,1230 messages: list[BaseMessage],1231 stop: list[str] | None = None,1232 **kwargs: Any,1233 ) -> Iterator[ChatGenerationChunk]:1234 reasoning = kwargs.get("reasoning", self.reasoning)1235 for stream_resp in self._create_chat_stream(messages, stop, **kwargs):1236 if not isinstance(stream_resp, str):1237 content = (1238 stream_resp["message"]["content"]1239 if "message" in stream_resp and "content" in stream_resp["message"]1240 else ""1241 )12421243 # Warn and skip responses with done_reason: 'load' and empty content1244 # These indicate the model was loaded but no actual generation occurred1245 is_load_response_with_empty_content = (1246 stream_resp.get("done") is True1247 and stream_resp.get("done_reason") == "load"1248 and not content.strip()1249 )12501251 if is_load_response_with_empty_content:1252 log.warning(1253 "Ollama returned empty response with done_reason='load'."1254 "This typically indicates the model was loaded but no content "1255 "was generated. Skipping this response."1256 )1257 continue12581259 if stream_resp.get("done") is True:1260 generation_info = dict(stream_resp)1261 if "model" in generation_info:1262 generation_info["model_name"] = generation_info["model"]1263 generation_info["model_provider"] = "ollama"1264 _ = generation_info.pop("message", None)1265 else:1266 chunk_logprobs = stream_resp.get("logprobs")1267 generation_info = (1268 {"logprobs": chunk_logprobs}1269 if chunk_logprobs is not None1270 else None1271 )12721273 additional_kwargs = {}1274 if (1275 reasoning1276 and "message" in stream_resp1277 and (thinking_content := stream_resp["message"].get("thinking"))1278 ):1279 additional_kwargs["reasoning_content"] = thinking_content12801281 chunk = ChatGenerationChunk(1282 message=AIMessageChunk(1283 content=content,1284 additional_kwargs=additional_kwargs,1285 usage_metadata=_get_usage_metadata_from_generation_info(1286 stream_resp1287 ),1288 tool_calls=_get_tool_calls_from_response(stream_resp),1289 ),1290 generation_info=generation_info,1291 )12921293 yield chunk12941295 def _stream(1296 self,1297 messages: list[BaseMessage],1298 stop: list[str] | None = None,1299 run_manager: CallbackManagerForLLMRun | None = None,1300 **kwargs: Any,1301 ) -> Iterator[ChatGenerationChunk]:1302 for chunk in self._iterate_over_stream(messages, stop, **kwargs):1303 if run_manager:1304 run_manager.on_llm_new_token(1305 chunk.text,1306 verbose=self.verbose,1307 )1308 yield chunk13091310 async def _aiterate_over_stream(1311 self,1312 messages: list[BaseMessage],1313 stop: list[str] | None = None,1314 **kwargs: Any,1315 ) -> AsyncIterator[ChatGenerationChunk]:1316 reasoning = kwargs.get("reasoning", self.reasoning)1317 async for stream_resp in self._acreate_chat_stream(messages, stop, **kwargs):1318 if not isinstance(stream_resp, str):1319 content = (1320 stream_resp["message"]["content"]1321 if "message" in stream_resp and "content" in stream_resp["message"]1322 else ""1323 )13241325 # Warn and skip responses with done_reason: 'load' and empty content1326 # These indicate the model was loaded but no actual generation occurred1327 is_load_response_with_empty_content = (1328 stream_resp.get("done") is True1329 and stream_resp.get("done_reason") == "load"1330 and not content.strip()1331 )13321333 if is_load_response_with_empty_content:1334 log.warning(1335 "Ollama returned empty response with done_reason='load'. "1336 "This typically indicates the model was loaded but no content "1337 "was generated. Skipping this response."1338 )1339 continue13401341 if stream_resp.get("done") is True:1342 generation_info = dict(stream_resp)1343 if "model" in generation_info:1344 generation_info["model_name"] = generation_info["model"]1345 generation_info["model_provider"] = "ollama"1346 _ = generation_info.pop("message", None)1347 else:1348 chunk_logprobs = stream_resp.get("logprobs")1349 generation_info = (1350 {"logprobs": chunk_logprobs}1351 if chunk_logprobs is not None1352 else None1353 )13541355 additional_kwargs = {}1356 if (1357 reasoning1358 and "message" in stream_resp1359 and (thinking_content := stream_resp["message"].get("thinking"))1360 ):1361 additional_kwargs["reasoning_content"] = thinking_content13621363 chunk = ChatGenerationChunk(1364 message=AIMessageChunk(1365 content=content,1366 additional_kwargs=additional_kwargs,1367 usage_metadata=_get_usage_metadata_from_generation_info(1368 stream_resp1369 ),1370 tool_calls=_get_tool_calls_from_response(stream_resp),1371 ),1372 generation_info=generation_info,1373 )13741375 yield chunk13761377 async def _astream(1378 self,1379 messages: list[BaseMessage],1380 stop: list[str] | None = None,1381 run_manager: AsyncCallbackManagerForLLMRun | None = None,1382 **kwargs: Any,1383 ) -> AsyncIterator[ChatGenerationChunk]:1384 async for chunk in self._aiterate_over_stream(messages, stop, **kwargs):1385 if run_manager:1386 await run_manager.on_llm_new_token(1387 chunk.text,1388 verbose=self.verbose,1389 )1390 yield chunk13911392 async def _agenerate(1393 self,1394 messages: list[BaseMessage],1395 stop: list[str] | None = None,1396 run_manager: AsyncCallbackManagerForLLMRun | None = None,1397 **kwargs: Any,1398 ) -> ChatResult:1399 final_chunk = await self._achat_stream_with_aggregation(1400 messages, stop, run_manager, verbose=self.verbose, **kwargs1401 )1402 generation_info = final_chunk.generation_info1403 chat_generation = ChatGeneration(1404 message=AIMessage(1405 content=final_chunk.text,1406 usage_metadata=cast(1407 "AIMessageChunk", final_chunk.message1408 ).usage_metadata,1409 tool_calls=cast("AIMessageChunk", final_chunk.message).tool_calls,1410 additional_kwargs=final_chunk.message.additional_kwargs,1411 ),1412 generation_info=generation_info,1413 )1414 return ChatResult(generations=[chat_generation])14151416 @property1417 def _llm_type(self) -> str:1418 """Return type of chat model."""1419 return "chat-ollama"14201421 def bind_tools(1422 self,1423 tools: Sequence[dict[str, Any] | type | Callable | BaseTool],1424 *,1425 tool_choice: dict | str | Literal["auto", "any"] | bool | None = None, # noqa: PYI051, ARG0021426 **kwargs: Any,1427 ) -> Runnable[LanguageModelInput, AIMessage]:1428 """Bind tool-like objects to this chat model.14291430 Assumes model is compatible with OpenAI tool-calling API.14311432 Args:1433 tools: A list of tool definitions to bind to this chat model.14341435 Supports any tool definition handled by [`convert_to_openai_tool`][langchain_core.utils.function_calling.convert_to_openai_tool].1436 tool_choice: If provided, which tool for model to call. **This parameter1437 is currently ignored as it is not supported by Ollama.**1438 kwargs: Any additional parameters are passed directly to1439 `self.bind(**kwargs)`.1440 """ # noqa: E5011441 formatted_tools = [convert_to_openai_tool(tool) for tool in tools]1442 return super().bind(tools=formatted_tools, **kwargs)14431444 def with_structured_output(1445 self,1446 schema: dict | type,1447 *,1448 method: Literal["function_calling", "json_mode", "json_schema"] = "json_schema",1449 include_raw: bool = False,1450 **kwargs: Any,1451 ) -> Runnable[LanguageModelInput, dict | BaseModel]:1452 r"""Model wrapper that returns outputs formatted to match the given schema.14531454 Args:1455 schema: The output schema. Can be passed in as:14561457 - An OpenAI function/tool schema.1458 - A JSON Schema,1459 - A `TypedDict` class,1460 - Or a Pydantic class.14611462 If `schema` is a Pydantic class then the model output will be a1463 Pydantic instance of that class, and the model-generated fields will be1464 validated by the Pydantic class. Otherwise the model output will be a1465 dict and will not be validated.14661467 See `langchain_core.utils.function_calling.convert_to_openai_tool` for1468 more on how to properly specify types and descriptions of schema fields1469 when specifying a Pydantic or `TypedDict` class.14701471 method: The method for steering model generation, one of:14721473 - `'json_schema'`:1474 Uses Ollama's [structured output API](https://ollama.com/blog/structured-outputs)1475 - `'function_calling'`:1476 Uses Ollama's tool-calling API1477 - `'json_mode'`:1478 Specifies `format='json'`. Note that if using JSON mode then you1479 must include instructions for formatting the output into the1480 desired schema into the model call.14811482 include_raw:1483 If `False` then only the parsed structured output is returned.14841485 If an error occurs during model output parsing it will be raised.14861487 If `True` then both the raw model response (a `BaseMessage`) and the1488 parsed model response will be returned.14891490 If an error occurs during output parsing it will be caught and returned1491 as well.14921493 The final output is always a `dict` with keys `'raw'`, `'parsed'`, and1494 `'parsing_error'`.14951496 kwargs: Additional keyword args aren't supported.14971498 Returns:1499 A `Runnable` that takes same inputs as a1500 `langchain_core.language_models.chat.BaseChatModel`. If `include_raw` is1501 `False` and `schema` is a Pydantic class, `Runnable` outputs an instance1502 of `schema` (i.e., a Pydantic object). Otherwise, if `include_raw` is1503 `False` then `Runnable` outputs a `dict`.15041505 If `include_raw` is `True`, then `Runnable` outputs a `dict` with keys:15061507 - `'raw'`: `BaseMessage`1508 - `'parsed'`: `None` if there was a parsing error, otherwise the type1509 depends on the `schema` as described above.1510 - `'parsing_error'`: `BaseException | None`15111512 !!! warning "Behavior changed in `langchain-ollama` 0.2.2"15131514 Added support for structured output API via `format` parameter.15151516 !!! warning "Behavior changed in `langchain-ollama` 0.3.0"15171518 Updated default `method` to `'json_schema'`.15191520 ??? note "Example: `schema=Pydantic` class, `method='json_schema'`, `include_raw=False`"15211522 ```python1523 from typing import Optional15241525 from langchain_ollama import ChatOllama1526 from pydantic import BaseModel, Field152715281529 class AnswerWithJustification(BaseModel):1530 '''An answer to the user question along with justification for the answer.'''15311532 answer: str1533 justification: str | None = Field(1534 default=...,1535 description="A justification for the answer.",1536 )153715381539 model = ChatOllama(model="llama3.1", temperature=0)1540 structured_model = model.with_structured_output(AnswerWithJustification)15411542 structured_model.invoke("What weighs more a pound of bricks or a pound of feathers")15431544 # -> AnswerWithJustification(1545 # answer='They weigh the same',1546 # 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.'1547 # )1548 ```15491550 ??? note "Example: `schema=Pydantic` class, `method='json_schema'`, `include_raw=True`"15511552 ```python1553 from langchain_ollama import ChatOllama1554 from pydantic import BaseModel155515561557 class AnswerWithJustification(BaseModel):1558 '''An answer to the user question along with justification for the answer.'''15591560 answer: str1561 justification: str156215631564 model = ChatOllama(model="llama3.1", temperature=0)1565 structured_model = model.with_structured_output(1566 AnswerWithJustification,1567 include_raw=True,1568 )15691570 structured_model.invoke("What weighs more a pound of bricks or a pound of feathers")1571 # -> {1572 # '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'}]}),1573 # '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.'),1574 # 'parsing_error': None1575 # }1576 ```15771578 ??? note "Example: `schema=Pydantic` class, `method='function_calling'`, `include_raw=False`"15791580 ```python1581 from typing import Optional15821583 from langchain_ollama import ChatOllama1584 from pydantic import BaseModel, Field158515861587 class AnswerWithJustification(BaseModel):1588 '''An answer to the user question along with justification for the answer.'''15891590 answer: str1591 justification: str | None = Field(1592 default=...,1593 description="A justification for the answer.",1594 )159515961597 model = ChatOllama(model="llama3.1", temperature=0)1598 structured_model = model.with_structured_output(1599 AnswerWithJustification,1600 method="function_calling",1601 )16021603 structured_model.invoke("What weighs more a pound of bricks or a pound of feathers")16041605 # -> AnswerWithJustification(1606 # answer='They weigh the same',1607 # 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.'1608 # )1609 ```16101611 ??? note "Example: `schema=TypedDict` class, `method='function_calling'`, `include_raw=False`"16121613 ```python1614 from typing_extensions import Annotated, TypedDict16151616 from langchain_ollama import ChatOllama161716181619 class AnswerWithJustification(TypedDict):1620 '''An answer to the user question along with justification for the answer.'''16211622 answer: str1623 justification: Annotated[str | None, None, "A justification for the answer."]162416251626 model = ChatOllama(model="llama3.1", temperature=0)1627 structured_model = model.with_structured_output(AnswerWithJustification)16281629 structured_model.invoke("What weighs more a pound of bricks or a pound of feathers")1630 # -> {1631 # 'answer': 'They weigh the same',1632 # '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.'1633 # }1634 ```16351636 ??? note "Example: `schema=OpenAI` function schema, `method='function_calling'`, `include_raw=False`"16371638 ```python1639 from langchain_ollama import ChatOllama16401641 oai_schema = {1642 'name': 'AnswerWithJustification',1643 'description': 'An answer to the user question along with justification for the answer.',1644 'parameters': {1645 'type': 'object',1646 'properties': {1647 'answer': {'type': 'string'},1648 'justification': {'description': 'A justification for the answer.', 'type': 'string'}1649 },1650 'required': ['answer']1651 }16521653 model = ChatOllama(model="llama3.1", temperature=0)1654 structured_model = model.with_structured_output(oai_schema)16551656 structured_model.invoke(1657 "What weighs more a pound of bricks or a pound of feathers"1658 )1659 # -> {1660 # 'answer': 'They weigh the same',1661 # '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.'1662 # }1663 ```16641665 ??? note "Example: `schema=Pydantic` class, `method='json_mode'`, `include_raw=True`"16661667 ```python1668 from langchain_ollama import ChatOllama1669 from pydantic import BaseModel167016711672 class AnswerWithJustification(BaseModel):1673 answer: str1674 justification: str167516761677 model = ChatOllama(model="llama3.1", temperature=0)1678 structured_model = model.with_structured_output(1679 AnswerWithJustification, method="json_mode", include_raw=True1680 )16811682 structured_model.invoke(1683 "Answer the following question. "1684 "Make sure to return a JSON blob with keys 'answer' and 'justification'.\\n\\n"1685 "What's heavier a pound of bricks or a pound of feathers?"1686 )1687 # -> {1688 # '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}'),1689 # '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.'),1690 # 'parsing_error': None1691 # }1692 ```16931694 """ # noqa: E5011695 _ = kwargs.pop("strict", None)1696 if kwargs:1697 msg = f"Received unsupported arguments {kwargs}"1698 raise ValueError(msg)1699 is_pydantic_schema = _is_pydantic_class(schema)1700 if method == "function_calling":1701 if schema is None:1702 msg = (1703 "schema must be specified when method is not 'json_mode'. "1704 "Received None."1705 )1706 raise ValueError(msg)1707 formatted_tool = convert_to_openai_tool(schema)1708 tool_name = formatted_tool["function"]["name"]1709 llm = self.bind_tools(1710 [schema],1711 tool_choice=tool_name,1712 ls_structured_output_format={1713 "kwargs": {"method": method},1714 "schema": formatted_tool,1715 },1716 )1717 if is_pydantic_schema:1718 output_parser: Runnable = PydanticToolsParser(1719 tools=[schema], # ty: ignore[invalid-argument-type]1720 first_tool_only=True,1721 )1722 else:1723 output_parser = JsonOutputKeyToolsParser(1724 key_name=tool_name, first_tool_only=True1725 )1726 elif method == "json_mode":1727 llm = self.bind(1728 format="json",1729 ls_structured_output_format={1730 "kwargs": {"method": method},1731 "schema": schema,1732 },1733 )1734 output_parser = (1735 PydanticOutputParser(pydantic_object=schema) # ty: ignore[invalid-argument-type]1736 if is_pydantic_schema1737 else JsonOutputParser()1738 )1739 elif method == "json_schema":1740 if schema is None:1741 msg = (1742 "schema must be specified when method is not 'json_mode'. "1743 "Received None."1744 )1745 raise ValueError(msg)1746 if is_pydantic_schema:1747 schema = cast("TypeBaseModel", schema)1748 if issubclass(schema, BaseModelV1):1749 response_format = schema.schema()1750 else:1751 response_format = schema.model_json_schema()1752 llm = self.bind(1753 format=response_format,1754 ls_structured_output_format={1755 "kwargs": {"method": method},1756 "schema": schema,1757 },1758 )1759 output_parser = PydanticOutputParser(pydantic_object=schema)1760 else:1761 if is_typeddict(schema):1762 response_format = convert_to_json_schema(schema)1763 if "required" not in response_format:1764 response_format["required"] = list(1765 response_format["properties"].keys()1766 )1767 else:1768 # is JSON schema1769 response_format = cast("dict", schema)1770 llm = self.bind(1771 format=response_format,1772 ls_structured_output_format={1773 "kwargs": {"method": method},1774 "schema": response_format,1775 },1776 )1777 output_parser = JsonOutputParser()1778 else:1779 msg = (1780 f"Unrecognized method argument. Expected one of 'function_calling', "1781 f"'json_schema', or 'json_mode'. Received: '{method}'"1782 )1783 raise ValueError(msg)17841785 if include_raw:1786 parser_assign = RunnablePassthrough.assign(1787 parsed=itemgetter("raw") | output_parser, parsing_error=lambda _: None1788 )1789 parser_none = RunnablePassthrough.assign(parsed=lambda _: None)1790 parser_with_fallback = parser_assign.with_fallbacks(1791 [parser_none], exception_key="parsing_error"1792 )1793 return RunnableMap(raw=llm) | parser_with_fallback1794 return llm | output_parser
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.