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)9899log = logging.getLogger(__name__)100101102def _get_usage_metadata_from_generation_info(103 generation_info: Mapping[str, Any] | None,104) -> UsageMetadata | None:105 """Get usage metadata from Ollama generation info mapping."""106 if generation_info is None:107 return None108 input_tokens: int | None = generation_info.get("prompt_eval_count")109 output_tokens: int | None = generation_info.get("eval_count")110 if input_tokens is not None and output_tokens is not None:111 return UsageMetadata(112 input_tokens=input_tokens,113 output_tokens=output_tokens,114 total_tokens=input_tokens + output_tokens,115 )116 return None117118119def _parse_json_string(120 json_string: str,121 *,122 raw_tool_call: dict[str, Any],123 skip: bool,124) -> Any:125 """Attempt to parse a JSON string for tool calling.126127 It first tries to use the standard `json.loads`. If that fails, it falls128 back to `ast.literal_eval` to safely parse Python literals, which is more129 robust against models using single quotes or containing apostrophes.130131 Args:132 json_string: JSON string to parse.133 raw_tool_call: Raw tool call to include in error message.134 skip: Whether to ignore parsing errors and return the value anyways.135136 Returns:137 The parsed JSON string or Python literal.138139 Raises:140 OutputParserException: If the string is invalid and `skip=False`.141 """142 try:143 return json.loads(json_string)144 except json.JSONDecodeError:145 try:146 # Use ast.literal_eval to safely parse Python-style dicts147 # (e.g. with single quotes)148 return ast.literal_eval(json_string)149 except (SyntaxError, ValueError) as e:150 # If both fail, and we're not skipping, raise an informative error.151 if skip:152 return json_string153 msg = (154 f"Function {raw_tool_call['function']['name']} arguments:\n\n"155 f"{raw_tool_call['function']['arguments']}"156 "\n\nare not valid JSON or a Python literal. "157 f"Received error: {e}"158 )159 raise OutputParserException(msg) from e160 except TypeError as e:161 if skip:162 return json_string163 msg = (164 f"Function {raw_tool_call['function']['name']} arguments:\n\n"165 f"{raw_tool_call['function']['arguments']}\n\nare not a string or a "166 f"dictionary. Received TypeError {e}"167 )168 raise OutputParserException(msg) from e169170171def _parse_arguments_from_tool_call(172 raw_tool_call: dict[str, Any],173) -> dict[str, Any] | None:174 """Parse arguments by trying to parse any shallowly nested string-encoded JSON.175176 Band-aid fix for issue in Ollama with inconsistent tool call argument structure.177 Should be removed/changed if fixed upstream.178179 See https://github.com/ollama/ollama/issues/6155180 """181 if "function" not in raw_tool_call:182 return None183 function_name = raw_tool_call["function"]["name"]184 arguments = raw_tool_call["function"]["arguments"]185 parsed_arguments: dict = {}186 if isinstance(arguments, dict):187 for key, value in arguments.items():188 # Filter out metadata fields like 'functionName' that echo function name189 if key == "functionName" and value == function_name:190 continue191 if isinstance(value, str):192 parsed_value = _parse_json_string(193 value, skip=True, raw_tool_call=raw_tool_call194 )195 if isinstance(parsed_value, (dict, list)):196 parsed_arguments[key] = parsed_value197 else:198 parsed_arguments[key] = value199 else:200 parsed_arguments[key] = value201 else:202 parsed_arguments = _parse_json_string(203 arguments, skip=False, raw_tool_call=raw_tool_call204 )205 return parsed_arguments206207208def _get_tool_calls_from_response(209 response: Mapping[str, Any],210) -> list[ToolCall]:211 """Get tool calls from Ollama response."""212 tool_calls = []213 if "message" in response and (214 raw_tool_calls := response["message"].get("tool_calls")215 ):216 tool_calls.extend(217 [218 tool_call(219 id=str(uuid4()),220 name=tc["function"]["name"],221 args=_parse_arguments_from_tool_call(tc) or {},222 )223 for tc in raw_tool_calls224 ]225 )226 return tool_calls227228229def _lc_tool_call_to_openai_tool_call(tool_call_: ToolCall) -> dict:230 """Convert a LangChain tool call to an OpenAI tool call format."""231 return {232 "type": "function",233 "id": tool_call_["id"],234 "function": {235 "name": tool_call_["name"],236 "arguments": tool_call_["args"],237 },238 }239240241def _get_image_from_data_content_block(block: dict) -> str:242 """Format standard data content block to format expected by Ollama."""243 if block["type"] == "image":244 if block.get("source_type") == "base64":245 # v0 style246 return block["data"]247 if block.get("base64"):248 # v1 content blocks249 return block["base64"]250 error_message = "Image data only supported through in-line base64 format."251 raise ValueError(error_message)252253 error_message = f"Blocks of type {block['type']} not supported."254 raise ValueError(error_message)255256257def _is_pydantic_class(obj: Any) -> bool:258 return isinstance(obj, type) and is_basemodel_subclass(obj)259260261class ChatOllama(BaseChatModel):262 r"""Ollama chat model integration.263264 ???+ note "Setup"265266 Install `langchain-ollama` and download any models you want to use from ollama.267268 ```bash269 ollama pull gpt-oss:20b270 pip install -U langchain-ollama271 ```272273 Key init args — completion params:274 model: str275 Name of Ollama model to use.276 reasoning: bool | None277 Controls the reasoning/thinking mode for278 [supported models](https://ollama.com/search?c=thinking).279280 - `True`: Enables reasoning mode. The model's reasoning process will be281 captured and returned separately in the `additional_kwargs` of the282 response message, under `reasoning_content`. The main response283 content will not include the reasoning tags.284 - `False`: Disables reasoning mode. The model will not perform any reasoning,285 and the response will not include any reasoning content.286 - `None` (Default): The model will use its default reasoning behavior. Note287 however, if the model's default behavior *is* to perform reasoning, think tags288 (`<think>` and `</think>`) will be present within the main response content289 unless you set `reasoning` to `True`.290 temperature: float291 Sampling temperature. Ranges from `0.0` to `1.0`.292 num_predict: int | None293 Max number of tokens to generate.294295 See full list of supported init args and their descriptions in the params section.296297 Instantiate:298 ```python299 from langchain_ollama import ChatOllama300301 model = ChatOllama(302 model="gpt-oss:20b",303 validate_model_on_init=True,304 temperature=0.8,305 num_predict=256,306 # other params ...307 )308 ```309310 Invoke:311 ```python312 messages = [313 ("system", "You are a helpful translator. Translate the user sentence to French."),314 ("human", "I love programming."),315 ]316 model.invoke(messages)317 ```318319 ```python320 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')321 ```322323 Stream:324 ```python325 for chunk in model.stream("Return the words Hello World!"):326 print(chunk.text, end="")327 ```328329 ```python330 content='Hello' id='run-327ff5ad-45c8-49fe-965c-0a93982e9be1'331 content=' World' id='run-327ff5ad-45c8-49fe-965c-0a93982e9be1'332 content='!' id='run-327ff5ad-45c8-49fe-965c-0a93982e9be1'333 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'334335 ```336337 ```python338 stream = model.stream(messages)339 full = next(stream)340 for chunk in stream:341 full += chunk342 full343 ```344345 ```python346 AIMessageChunk(347 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")',348 response_metadata={349 "model": "llama3",350 "created_at": "2024-07-04T03:38:54.933154Z",351 "message": {"role": "assistant", "content": ""},352 "done_reason": "stop",353 "done": True,354 "total_duration": 1977300042,355 "load_duration": 1345709,356 "prompt_eval_duration": 159343000,357 "eval_count": 47,358 "eval_duration": 1815123000,359 },360 id="run-3c81a3ed-3e79-4dd3-a796-04064d804890",361 )362 ```363364 Async:365 ```python366 await model.ainvoke("Hello how are you!")367 ```368369 ```python370 AIMessage(371 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?",372 response_metadata={373 "model": "llama3",374 "created_at": "2024-07-04T03:52:08.165478Z",375 "message": {"role": "assistant", "content": ""},376 "done_reason": "stop",377 "done": True,378 "total_duration": 2138492875,379 "load_duration": 1364000,380 "prompt_eval_count": 10,381 "prompt_eval_duration": 297081000,382 "eval_count": 47,383 "eval_duration": 1838524000,384 },385 id="run-29c510ae-49a4-4cdd-8f23-b972bfab1c49-0",386 )387 ```388389 ```python390 async for chunk in model.astream("Say hello world!"):391 print(chunk.content)392 ```393394 ```python395 HEL396 LO397 WORLD398 !399 ```400401 ```python402 messages = [("human", "Say hello world!"), ("human", "Say goodbye world!")]403 await model.abatch(messages)404 ```405406 ```python407 [408 AIMessage(409 content="HELLO, WORLD!",410 response_metadata={411 "model": "llama3",412 "created_at": "2024-07-04T03:55:07.315396Z",413 "message": {"role": "assistant", "content": ""},414 "done_reason": "stop",415 "done": True,416 "total_duration": 1696745458,417 "load_duration": 1505000,418 "prompt_eval_count": 8,419 "prompt_eval_duration": 111627000,420 "eval_count": 6,421 "eval_duration": 185181000,422 },423 id="run-da6c7562-e25a-4a44-987a-2c83cd8c2686-0",424 ),425 AIMessage(426 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!",427 response_metadata={428 "model": "llama3",429 "created_at": "2024-07-04T03:55:07.018076Z",430 "message": {"role": "assistant", "content": ""},431 "done_reason": "stop",432 "done": True,433 "total_duration": 1399391083,434 "load_duration": 1187417,435 "prompt_eval_count": 20,436 "prompt_eval_duration": 230349000,437 "eval_count": 31,438 "eval_duration": 1166047000,439 },440 id="run-96cad530-6f3e-4cf9-86b4-e0f8abba4cdb-0",441 ),442 ]443 ```444445 JSON mode:446 ```python447 json_model = ChatOllama(format="json")448 json_model.invoke(449 "Return a query for the weather in a random location and time of day with two keys: location and time_of_day. "450 "Respond using JSON only."451 ).content452 ```453454 ```python455 '{"location": "Pune, India", "time_of_day": "morning"}'456 ```457458 Tool Calling:459 ```python460 from langchain_ollama import ChatOllama461 from pydantic import BaseModel, Field462463464 class Multiply(BaseModel):465 a: int = Field(..., description="First integer")466 b: int = Field(..., description="Second integer")467468469 ans = await chat.invoke("What is 45*67")470 ans.tool_calls471 ```472473 ```python474 [475 {476 "name": "Multiply",477 "args": {"a": 45, "b": 67},478 "id": "420c3f3b-df10-4188-945f-eb3abdb40622",479 "type": "tool_call",480 }481 ]482 ```483484 Thinking / Reasoning:485 You can enable reasoning mode for models that support it by setting486 the `reasoning` parameter to `True` in either the constructor or487 the `invoke`/`stream` methods. This will enable the model to think488 through the problem and return the reasoning process separately in the489 `additional_kwargs` of the response message, under `reasoning_content`.490491 If `reasoning` is set to `None`, the model will use its default reasoning492 behavior, and any reasoning content will *not* be captured under the493 `reasoning_content` key, but will be present within the main response content494 as think tags (`<think>` and `</think>`).495496 !!! note497 This feature is only available for [models that support reasoning](https://ollama.com/search?c=thinking).498499 ```python500 from langchain_ollama import ChatOllama501502 model = ChatOllama(503 model="deepseek-r1:8b",504 validate_model_on_init=True,505 reasoning=True,506 )507508 model.invoke("how many r in the word strawberry?")509510 # or, on an invocation basis:511512 model.invoke("how many r in the word strawberry?", reasoning=True)513 # or model.stream("how many r in the word strawberry?", reasoning=True)514515 # If not provided, the invocation will default to the ChatOllama reasoning516 # param provided (None by default).517 ```518519 ```python520 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})521522 ```523 """ # noqa: E501, pylint: disable=line-too-long524525 model: str526 """Model name to use."""527528 reasoning: bool | str | None = None529 """Controls the reasoning/thinking mode for [supported models](https://ollama.com/search?c=thinking).530531 - `True`: Enables reasoning mode. The model's reasoning process will be532 captured and returned separately in the `additional_kwargs` of the533 response message, under `reasoning_content`. The main response534 content will not include the reasoning tags.535 - `False`: Disables reasoning mode. The model will not perform any reasoning,536 and the response will not include any reasoning content.537 - `None` (Default): The model will use its default reasoning behavior. Note538 however, if the model's default behavior *is* to perform reasoning, think tags539 (`<think>` and `</think>`) will be present within the main response content540 unless you set `reasoning` to `True`.541 - `str`: e.g. `'low'`, `'medium'`, `'high'`. Enables reasoning with a custom542 intensity level. Currently, this is only supported `gpt-oss`. See the543 [Ollama docs](https://github.com/ollama/ollama-python/blob/da79e987f0ac0a4986bf396f043b36ef840370bc/ollama/_types.py#L210)544 for more information.545 """546547 validate_model_on_init: bool = False548 """Whether to validate the model exists in Ollama locally on initialization.549550 !!! version-added "Added in `langchain-ollama` 0.3.4"551 """552553 mirostat: int | None = None554 """Enable Mirostat sampling for controlling perplexity.555556 (Default: `0`, `0` = disabled, `1` = Mirostat, `2` = Mirostat 2.0)557 """558559 mirostat_eta: float | None = None560 """Influences how quickly the algorithm responds to feedback from generated text.561562 A lower learning rate will result in slower adjustments, while a higher learning563 rate will make the algorithm more responsive.564565 (Default: `0.1`)566 """567568 mirostat_tau: float | None = None569 """Controls the balance between coherence and diversity of the output.570571 A lower value will result in more focused and coherent text.572573 (Default: `5.0`)574 """575576 num_ctx: int | None = None577 """Sets the size of the context window used to generate the next token.578579 (Default: `2048`)580 """581582 num_gpu: int | None = None583 """The number of GPUs to use.584585 On macOS it defaults to `1` to enable metal support, `0` to disable.586 """587588 num_thread: int | None = None589 """Sets the number of threads to use during computation.590591 By default, Ollama will detect this for optimal performance. It is recommended to592 set this value to the number of physical CPU cores your system has (as opposed to593 the logical number of cores).594 """595596 num_predict: int | None = None597 """Maximum number of tokens to predict when generating text.598599 (Default: `128`, `-1` = infinite generation, `-2` = fill context)600 """601602 repeat_last_n: int | None = None603 """Sets how far back for the model to look back to prevent repetition.604605 (Default: `64`, `0` = disabled, `-1` = `num_ctx`)606 """607608 repeat_penalty: float | None = None609 """Sets how strongly to penalize repetitions.610611 A higher value (e.g., `1.5`) will penalize repetitions more strongly, while a612 lower value (e.g., `0.9`) will be more lenient. (Default: `1.1`)613 """614615 temperature: float | None = None616 """The temperature of the model.617618 Increasing the temperature will make the model answer more creatively.619620 (Default: `0.8`)621 """622623 seed: int | None = None624 """Sets the random number seed to use for generation.625626 Setting this to a specific number will make the model generate the same text for the627 same prompt.628 """629630 logprobs: bool | None = None631 """Whether to return logprobs.632633 !!! note634635 When streaming, per-token logprobs are available on each intermediate636 chunk (via `response_metadata["logprobs"]`) and are accumulated into the637 final aggregated response when using `invoke()`.638 """639640 top_logprobs: int | None = None641 """Number of most likely tokens to return at each token position, each with642 an associated log probability. Must be a positive integer.643644 If set without `logprobs=True`, `logprobs` will be enabled automatically.645 """646647 @field_validator("top_logprobs")648 @classmethod649 def _validate_top_logprobs(cls, v: int | None) -> int | None:650 if v is not None and v < 1:651 msg = "`top_logprobs` must be a positive integer."652 raise ValueError(msg)653 return v654655 stop: list[str] | None = None656 """Sets the stop tokens to use."""657658 tfs_z: float | None = None659 """Tail free sampling.660661 Used to reduce the impact of less probable tokens from the output.662663 A higher value (e.g., `2.0`) will reduce the impact more, while a value of `1.0`664 disables this setting.665666 (Default: `1`)667 """668669 top_k: int | None = None670 """Reduces the probability of generating nonsense.671672 A higher value (e.g. `100`) will give more diverse answers, while a lower value673 (e.g. `10`) will be more conservative.674675 (Default: `40`)676 """677678 top_p: float | None = None679 """Works together with top-k.680681 A higher value (e.g., `0.95`) will lead to more diverse text, while a lower value682 (e.g., `0.5`) will generate more focused and conservative text.683684 (Default: `0.9`)685 """686687 format: Literal["", "json"] | JsonSchemaValue | None = None688 """Specify the format of the output (options: `'json'`, JSON schema)."""689690 keep_alive: int | str | None = None691 """How long the model will stay loaded into memory."""692693 base_url: str | None = None694 """Base url the model is hosted under.695696 If none, defaults to the Ollama client default.697698 Supports `userinfo` auth in the format `http://username:password@localhost:11434`.699 Useful if your Ollama server is behind a proxy.700701 !!! warning702 `userinfo` is not secure and should only be used for local testing or703 in secure environments. Avoid using it in production or over unsecured704 networks.705706 !!! note707 If using `userinfo`, ensure that the Ollama server is configured to708 accept and validate these credentials.709710 !!! note711 `userinfo` headers are passed to both sync and async clients.712713 """714715 client_kwargs: dict | None = {}716 """Additional kwargs to pass to the httpx clients. Pass headers in here.717718 These arguments are passed to both synchronous and async clients.719720 Use `sync_client_kwargs` and `async_client_kwargs` to pass different arguments721 to synchronous and asynchronous clients.722 """723724 async_client_kwargs: dict | None = {}725 """Additional kwargs to merge with `client_kwargs` before passing to httpx client.726727 These are clients unique to the async client; for shared args use `client_kwargs`.728729 For a full list of the params, see the [httpx documentation](https://www.python-httpx.org/api/#asyncclient).730 """731732 sync_client_kwargs: dict | None = {}733 """Additional kwargs to merge with `client_kwargs` before passing to httpx client.734735 These are clients unique to the sync client; for shared args use `client_kwargs`.736737 For a full list of the params, see the [httpx documentation](https://www.python-httpx.org/api/#client).738 """739740 _client: Client = PrivateAttr()741 """The client to use for making requests."""742743 _async_client: AsyncClient = PrivateAttr()744 """The async client to use for making requests."""745746 def _chat_params(747 self,748 messages: list[BaseMessage],749 stop: list[str] | None = None,750 **kwargs: Any,751 ) -> dict[str, Any]:752 """Assemble the parameters for a chat completion request.753754 Args:755 messages: List of LangChain messages to send to the model.756 stop: Optional list of stop tokens to use for this invocation.757 **kwargs: Additional keyword arguments to include in the request.758759 Returns:760 A dictionary of parameters to pass to the Ollama client.761 """762 ollama_messages = self._convert_messages_to_ollama_messages(messages)763764 if self.stop is not None and stop is not None:765 msg = "`stop` found in both the input and default params."766 raise ValueError(msg)767 if self.stop is not None:768 stop = self.stop769770 options_dict = kwargs.pop("options", None)771 if options_dict is None:772 # Only include parameters that are explicitly set (not None)773 options_dict = {774 k: v775 for k, v in {776 "mirostat": self.mirostat,777 "mirostat_eta": self.mirostat_eta,778 "mirostat_tau": self.mirostat_tau,779 "num_ctx": self.num_ctx,780 "num_gpu": self.num_gpu,781 "num_thread": self.num_thread,782 "num_predict": self.num_predict,783 "repeat_last_n": self.repeat_last_n,784 "repeat_penalty": self.repeat_penalty,785 "temperature": self.temperature,786 "seed": self.seed,787 "stop": self.stop if stop is None else stop,788 "tfs_z": self.tfs_z,789 "top_k": self.top_k,790 "top_p": self.top_p,791 }.items()792 if v is not None793 }794795 format_param = self._resolve_format_param(796 kwargs.pop("format", self.format),797 kwargs.pop("response_format", None),798 )799800 params = {801 "messages": ollama_messages,802 "stream": kwargs.pop("stream", True),803 "model": kwargs.pop("model", self.model),804 "think": kwargs.pop("reasoning", self.reasoning),805 "format": format_param,806 "logprobs": kwargs.pop("logprobs", self.logprobs),807 "top_logprobs": kwargs.pop("top_logprobs", self.top_logprobs),808 "options": options_dict,809 "keep_alive": kwargs.pop("keep_alive", self.keep_alive),810 **kwargs,811 }812813 # Filter out 'strict' argument if present, as it is not supported by Ollama814 # but may be passed by upstream libraries (e.g. LangChain ProviderStrategy)815 if "strict" in params:816 params.pop("strict")817818 if tools := kwargs.get("tools"):819 params["tools"] = tools820821 return params822823 def _resolve_format_param(824 self,825 format_param: str | dict[str, Any] | None,826 response_format: Any | None,827 ) -> str | dict[str, Any] | None:828 """Resolve the format parameter.829830 Converts an OpenAI-style `response_format` dict to the `format`831 parameter expected by Ollama.832833 Args:834 format_param: The explicit `format` value (takes priority).835 response_format: An OpenAI-style `response_format` dict.836837 Returns:838 The resolved format value to pass to the Ollama client.839 """840 if format_param is not None:841 if response_format is not None:842 warnings.warn(843 "Both 'format' and 'response_format' were provided. "844 "'response_format' will be ignored in favor of 'format'.",845 UserWarning,846 stacklevel=2,847 )848 return format_param849850 if response_format is None:851 return None852853 return self._convert_response_format(response_format)854855 def _convert_response_format(856 self,857 response_format: Any,858 ) -> str | dict[str, Any] | None:859 """Convert an OpenAI-style `response_format` to an Ollama `format` value.860861 Args:862 response_format: The `response_format` value to convert.863864 Returns:865 The Ollama-compatible `format` value, or `None` if conversion fails.866 """867 if not isinstance(response_format, dict):868 warnings.warn(869 f"Ignored invalid 'response_format' type: {type(response_format)}. "870 "Expected a dictionary.",871 UserWarning,872 stacklevel=2,873 )874 return None875876 fmt_type = response_format.get("type")877 if fmt_type == "json_object":878 return "json"879 if fmt_type == "json_schema":880 return self._extract_json_schema(response_format)881882 warnings.warn(883 f"Ignored unrecognized 'response_format' type: {fmt_type}. "884 "Expected 'json_object' or 'json_schema'.",885 UserWarning,886 stacklevel=2,887 )888 return None889890 def _extract_json_schema(891 self,892 response_format: dict[str, Any],893 ) -> dict[str, Any] | None:894 """Extract the raw JSON schema from an OpenAI ``json_schema`` envelope.895896 Args:897 response_format: A dict with ``type: "json_schema"``.898899 Returns:900 The raw JSON schema dict, or ``None`` if extraction fails.901 """902 json_schema_block = response_format.get("json_schema")903 if not isinstance(json_schema_block, dict):904 warnings.warn(905 "response_format has type 'json_schema' but 'json_schema' "906 f"value is {type(json_schema_block)}, expected a dict "907 "containing a 'schema' key. "908 "The format parameter will not be set.",909 UserWarning,910 stacklevel=2,911 )912 return None913 schema = json_schema_block.get("schema")914 if schema is None:915 warnings.warn(916 "response_format has type 'json_schema' but no 'schema' "917 "key was found in 'json_schema'. "918 "The format parameter will not be set.",919 UserWarning,920 stacklevel=2,921 )922 return schema923924 @model_validator(mode="after")925 def _set_clients(self) -> Self:926 """Set clients to use for ollama."""927 if self.top_logprobs is not None and self.logprobs is not True:928 if self.logprobs is False:929 msg = (930 "`top_logprobs` is set but `logprobs` is explicitly `False`. "931 "Either set `logprobs=True` to use `top_logprobs`, or remove "932 "`top_logprobs`."933 )934 raise ValueError(msg)935 # logprobs is None (unset) — auto-enable as convenience936 self.logprobs = True937 warnings.warn(938 "`top_logprobs` is set but `logprobs` was not explicitly enabled. "939 "Setting `logprobs=True` automatically.",940 UserWarning,941 stacklevel=2,942 )943944 client_kwargs = self.client_kwargs or {}945946 cleaned_url, auth_headers = parse_url_with_auth(self.base_url)947 merge_auth_headers(client_kwargs, auth_headers)948949 sync_client_kwargs = client_kwargs950 if self.sync_client_kwargs:951 sync_client_kwargs = {**sync_client_kwargs, **self.sync_client_kwargs}952953 async_client_kwargs = client_kwargs954 if self.async_client_kwargs:955 async_client_kwargs = {**async_client_kwargs, **self.async_client_kwargs}956957 self._client = Client(host=cleaned_url, **sync_client_kwargs)958 self._async_client = AsyncClient(host=cleaned_url, **async_client_kwargs)959 if self.validate_model_on_init:960 validate_model(self._client, self.model)961 return self962963 def _convert_messages_to_ollama_messages(964 self, messages: list[BaseMessage]965 ) -> Sequence[Message]:966 """Convert a BaseMessage list to list of messages for Ollama to consume.967968 Args:969 messages: List of BaseMessage to convert.970971 Returns:972 List of messages in Ollama format.973 """974 messages = list(messages) # shallow copy to avoid mutating caller's list975 for idx, message in enumerate(messages):976 # Handle message content written in v1 format977 if (978 isinstance(message, AIMessage)979 and message.response_metadata.get("output_version") == "v1"980 ):981 # Unpack known v1 content to Ollama format for the request982 # Most types are passed through unchanged983 messages[idx] = message.model_copy(984 update={985 "content": _convert_from_v1_to_ollama(986 cast("list[types.ContentBlock]", message.content),987 message.response_metadata.get("model_provider"),988 )989 }990 )991992 ollama_messages: list = []993 for message in messages:994 role: str995 tool_call_id: str | None = None996 tool_calls: list[dict[str, Any]] | None = None997 if isinstance(message, HumanMessage):998 role = "user"999 elif isinstance(message, AIMessage):1000 role = "assistant"1001 tool_calls = (1002 [1003 _lc_tool_call_to_openai_tool_call(tool_call)1004 for tool_call in message.tool_calls1005 ]1006 if message.tool_calls1007 else None1008 )1009 elif isinstance(message, SystemMessage):1010 role = "system"1011 elif isinstance(message, ChatMessage):1012 role = message.role1013 elif isinstance(message, ToolMessage):1014 role = "tool"1015 tool_call_id = message.tool_call_id1016 else:1017 msg = "Received unsupported message type for Ollama."1018 raise TypeError(msg)10191020 content = ""1021 images = []1022 if isinstance(message.content, str):1023 content = message.content1024 else: # List1025 for content_part in message.content:1026 if isinstance(content_part, str):1027 if content:1028 content += "\n"1029 content += content_part1030 elif content_part.get("type") == "text":1031 if content:1032 content += "\n"1033 content += content_part["text"]1034 elif content_part.get("type") == "tool_use":1035 continue1036 elif content_part.get("type") == "image_url":1037 image_url = None1038 temp_image_url = content_part.get("image_url")1039 if isinstance(temp_image_url, str):1040 image_url = temp_image_url1041 elif (1042 isinstance(temp_image_url, dict)1043 and "url" in temp_image_url1044 and isinstance(temp_image_url["url"], str)1045 ):1046 image_url = temp_image_url["url"]1047 else:1048 msg = (1049 "Only string image_url or dict with string 'url' "1050 "inside content parts are supported."1051 )1052 raise ValueError(msg)10531054 image_url_components = image_url.split(",")1055 # Support data:image/jpeg;base64,<image> format1056 # and base64 strings1057 if len(image_url_components) > 1:1058 images.append(image_url_components[1])1059 else:1060 images.append(image_url_components[0])1061 elif is_data_content_block(content_part):1062 # Handles v1 "image" type1063 image = _get_image_from_data_content_block(content_part)1064 images.append(image)1065 else:1066 msg = (1067 "Unsupported message content type. "1068 "Must either have type 'text' or type 'image_url' "1069 "with a string 'image_url' field."1070 )1071 raise ValueError(msg)1072 # Should convert to ollama.Message once role includes tool, and tool_call_id1073 # is in Message1074 msg_: dict = {1075 "role": role,1076 "content": content,1077 "images": images,1078 }1079 if tool_calls:1080 msg_["tool_calls"] = tool_calls1081 if tool_call_id:1082 msg_["tool_call_id"] = tool_call_id1083 if isinstance(message, AIMessage):1084 thinking = message.additional_kwargs.get("reasoning_content")1085 if thinking is not None:1086 msg_["thinking"] = thinking1087 ollama_messages.append(msg_)10881089 return ollama_messages10901091 async def _acreate_chat_stream(1092 self,1093 messages: list[BaseMessage],1094 stop: list[str] | None = None,1095 **kwargs: Any,1096 ) -> AsyncIterator[Mapping[str, Any] | str]:1097 if not self._async_client:1098 msg = (1099 "Ollama async client is not initialized. "1100 "Make sure the model was properly constructed."1101 )1102 raise RuntimeError(msg)1103 chat_params = self._chat_params(messages, stop, **kwargs)11041105 if chat_params["stream"]:1106 async for part in await self._async_client.chat(**chat_params):1107 yield part1108 else:1109 yield await self._async_client.chat(**chat_params)11101111 def _create_chat_stream(1112 self,1113 messages: list[BaseMessage],1114 stop: list[str] | None = None,1115 **kwargs: Any,1116 ) -> Iterator[Mapping[str, Any] | str]:1117 if not self._client:1118 msg = (1119 "Ollama sync client is not initialized. "1120 "Make sure the model was properly constructed."1121 )1122 raise RuntimeError(msg)1123 chat_params = self._chat_params(messages, stop, **kwargs)11241125 if chat_params["stream"]:1126 yield from self._client.chat(**chat_params)1127 else:1128 yield self._client.chat(**chat_params)11291130 def _chat_stream_with_aggregation(1131 self,1132 messages: list[BaseMessage],1133 stop: list[str] | None = None,1134 run_manager: CallbackManagerForLLMRun | None = None,1135 verbose: bool = False, # noqa: FBT0021136 **kwargs: Any,1137 ) -> ChatGenerationChunk:1138 final_chunk = None1139 for chunk in self._iterate_over_stream(messages, stop, **kwargs):1140 if final_chunk is None:1141 final_chunk = chunk1142 else:1143 final_chunk += chunk1144 if run_manager:1145 run_manager.on_llm_new_token(1146 chunk.text,1147 chunk=chunk,1148 verbose=verbose,1149 )1150 if final_chunk is None:1151 msg = "No data received from Ollama stream."1152 raise ValueError(msg)11531154 return final_chunk11551156 async def _achat_stream_with_aggregation(1157 self,1158 messages: list[BaseMessage],1159 stop: list[str] | None = None,1160 run_manager: AsyncCallbackManagerForLLMRun | None = None,1161 verbose: bool = False, # noqa: FBT0021162 **kwargs: Any,1163 ) -> ChatGenerationChunk:1164 final_chunk = None1165 async for chunk in self._aiterate_over_stream(messages, stop, **kwargs):1166 if final_chunk is None:1167 final_chunk = chunk1168 else:1169 final_chunk += chunk1170 if run_manager:1171 await run_manager.on_llm_new_token(1172 chunk.text,1173 chunk=chunk,1174 verbose=verbose,1175 )1176 if final_chunk is None:1177 msg = "No data received from Ollama stream."1178 raise ValueError(msg)11791180 return final_chunk11811182 def _get_ls_params(1183 self, stop: list[str] | None = None, **kwargs: Any1184 ) -> LangSmithParams:1185 """Get standard params for tracing."""1186 params = self._get_invocation_params(stop=stop, **kwargs)1187 ls_params = LangSmithParams(1188 ls_provider="ollama",1189 ls_model_name=params.get("model", self.model),1190 ls_model_type="chat",1191 ls_temperature=params.get("temperature", self.temperature),1192 )1193 if ls_stop := stop or params.get("stop", None) or self.stop:1194 ls_params["ls_stop"] = ls_stop1195 return ls_params11961197 def _generate(1198 self,1199 messages: list[BaseMessage],1200 stop: list[str] | None = None,1201 run_manager: CallbackManagerForLLMRun | None = None,1202 **kwargs: Any,1203 ) -> ChatResult:1204 final_chunk = self._chat_stream_with_aggregation(1205 messages, stop, run_manager, verbose=self.verbose, **kwargs1206 )1207 generation_info = final_chunk.generation_info1208 chat_generation = ChatGeneration(1209 message=AIMessage(1210 content=final_chunk.text,1211 usage_metadata=cast(1212 "AIMessageChunk", final_chunk.message1213 ).usage_metadata,1214 tool_calls=cast("AIMessageChunk", final_chunk.message).tool_calls,1215 additional_kwargs=final_chunk.message.additional_kwargs,1216 ),1217 generation_info=generation_info,1218 )1219 return ChatResult(generations=[chat_generation])12201221 def _iterate_over_stream(1222 self,1223 messages: list[BaseMessage],1224 stop: list[str] | None = None,1225 **kwargs: Any,1226 ) -> Iterator[ChatGenerationChunk]:1227 reasoning = kwargs.get("reasoning", self.reasoning)1228 for stream_resp in self._create_chat_stream(messages, stop, **kwargs):1229 if not isinstance(stream_resp, str):1230 content = (1231 stream_resp["message"]["content"]1232 if "message" in stream_resp and "content" in stream_resp["message"]1233 else ""1234 )12351236 # Warn and skip responses with done_reason: 'load' and empty content1237 # These indicate the model was loaded but no actual generation occurred1238 is_load_response_with_empty_content = (1239 stream_resp.get("done") is True1240 and stream_resp.get("done_reason") == "load"1241 and not content.strip()1242 )12431244 if is_load_response_with_empty_content:1245 log.warning(1246 "Ollama returned empty response with done_reason='load'."1247 "This typically indicates the model was loaded but no content "1248 "was generated. Skipping this response."1249 )1250 continue12511252 if stream_resp.get("done") is True:1253 generation_info = dict(stream_resp)1254 if "model" in generation_info:1255 generation_info["model_name"] = generation_info["model"]1256 generation_info["model_provider"] = "ollama"1257 _ = generation_info.pop("message", None)1258 else:1259 chunk_logprobs = stream_resp.get("logprobs")1260 generation_info = (1261 {"logprobs": chunk_logprobs}1262 if chunk_logprobs is not None1263 else None1264 )12651266 additional_kwargs = {}1267 if (1268 reasoning1269 and "message" in stream_resp1270 and (thinking_content := stream_resp["message"].get("thinking"))1271 ):1272 additional_kwargs["reasoning_content"] = thinking_content12731274 chunk = ChatGenerationChunk(1275 message=AIMessageChunk(1276 content=content,1277 additional_kwargs=additional_kwargs,1278 usage_metadata=_get_usage_metadata_from_generation_info(1279 stream_resp1280 ),1281 tool_calls=_get_tool_calls_from_response(stream_resp),1282 ),1283 generation_info=generation_info,1284 )12851286 yield chunk12871288 def _stream(1289 self,1290 messages: list[BaseMessage],1291 stop: list[str] | None = None,1292 run_manager: CallbackManagerForLLMRun | None = None,1293 **kwargs: Any,1294 ) -> Iterator[ChatGenerationChunk]:1295 for chunk in self._iterate_over_stream(messages, stop, **kwargs):1296 if run_manager:1297 run_manager.on_llm_new_token(1298 chunk.text,1299 verbose=self.verbose,1300 )1301 yield chunk13021303 async def _aiterate_over_stream(1304 self,1305 messages: list[BaseMessage],1306 stop: list[str] | None = None,1307 **kwargs: Any,1308 ) -> AsyncIterator[ChatGenerationChunk]:1309 reasoning = kwargs.get("reasoning", self.reasoning)1310 async for stream_resp in self._acreate_chat_stream(messages, stop, **kwargs):1311 if not isinstance(stream_resp, str):1312 content = (1313 stream_resp["message"]["content"]1314 if "message" in stream_resp and "content" in stream_resp["message"]1315 else ""1316 )13171318 # Warn and skip responses with done_reason: 'load' and empty content1319 # These indicate the model was loaded but no actual generation occurred1320 is_load_response_with_empty_content = (1321 stream_resp.get("done") is True1322 and stream_resp.get("done_reason") == "load"1323 and not content.strip()1324 )13251326 if is_load_response_with_empty_content:1327 log.warning(1328 "Ollama returned empty response with done_reason='load'. "1329 "This typically indicates the model was loaded but no content "1330 "was generated. Skipping this response."1331 )1332 continue13331334 if stream_resp.get("done") is True:1335 generation_info = dict(stream_resp)1336 if "model" in generation_info:1337 generation_info["model_name"] = generation_info["model"]1338 generation_info["model_provider"] = "ollama"1339 _ = generation_info.pop("message", None)1340 else:1341 chunk_logprobs = stream_resp.get("logprobs")1342 generation_info = (1343 {"logprobs": chunk_logprobs}1344 if chunk_logprobs is not None1345 else None1346 )13471348 additional_kwargs = {}1349 if (1350 reasoning1351 and "message" in stream_resp1352 and (thinking_content := stream_resp["message"].get("thinking"))1353 ):1354 additional_kwargs["reasoning_content"] = thinking_content13551356 chunk = ChatGenerationChunk(1357 message=AIMessageChunk(1358 content=content,1359 additional_kwargs=additional_kwargs,1360 usage_metadata=_get_usage_metadata_from_generation_info(1361 stream_resp1362 ),1363 tool_calls=_get_tool_calls_from_response(stream_resp),1364 ),1365 generation_info=generation_info,1366 )13671368 yield chunk13691370 async def _astream(1371 self,1372 messages: list[BaseMessage],1373 stop: list[str] | None = None,1374 run_manager: AsyncCallbackManagerForLLMRun | None = None,1375 **kwargs: Any,1376 ) -> AsyncIterator[ChatGenerationChunk]:1377 async for chunk in self._aiterate_over_stream(messages, stop, **kwargs):1378 if run_manager:1379 await run_manager.on_llm_new_token(1380 chunk.text,1381 verbose=self.verbose,1382 )1383 yield chunk13841385 async def _agenerate(1386 self,1387 messages: list[BaseMessage],1388 stop: list[str] | None = None,1389 run_manager: AsyncCallbackManagerForLLMRun | None = None,1390 **kwargs: Any,1391 ) -> ChatResult:1392 final_chunk = await self._achat_stream_with_aggregation(1393 messages, stop, run_manager, verbose=self.verbose, **kwargs1394 )1395 generation_info = final_chunk.generation_info1396 chat_generation = ChatGeneration(1397 message=AIMessage(1398 content=final_chunk.text,1399 usage_metadata=cast(1400 "AIMessageChunk", final_chunk.message1401 ).usage_metadata,1402 tool_calls=cast("AIMessageChunk", final_chunk.message).tool_calls,1403 additional_kwargs=final_chunk.message.additional_kwargs,1404 ),1405 generation_info=generation_info,1406 )1407 return ChatResult(generations=[chat_generation])14081409 @property1410 def _llm_type(self) -> str:1411 """Return type of chat model."""1412 return "chat-ollama"14131414 def bind_tools(1415 self,1416 tools: Sequence[dict[str, Any] | type | Callable | BaseTool],1417 *,1418 tool_choice: dict | str | Literal["auto", "any"] | bool | None = None, # noqa: PYI051, ARG0021419 **kwargs: Any,1420 ) -> Runnable[LanguageModelInput, AIMessage]:1421 """Bind tool-like objects to this chat model.14221423 Assumes model is compatible with OpenAI tool-calling API.14241425 Args:1426 tools: A list of tool definitions to bind to this chat model.14271428 Supports any tool definition handled by [`convert_to_openai_tool`][langchain_core.utils.function_calling.convert_to_openai_tool].1429 tool_choice: If provided, which tool for model to call. **This parameter1430 is currently ignored as it is not supported by Ollama.**1431 kwargs: Any additional parameters are passed directly to1432 `self.bind(**kwargs)`.1433 """ # noqa: E5011434 formatted_tools = [convert_to_openai_tool(tool) for tool in tools]1435 return super().bind(tools=formatted_tools, **kwargs)14361437 def with_structured_output(1438 self,1439 schema: dict | type,1440 *,1441 method: Literal["function_calling", "json_mode", "json_schema"] = "json_schema",1442 include_raw: bool = False,1443 **kwargs: Any,1444 ) -> Runnable[LanguageModelInput, dict | BaseModel]:1445 r"""Model wrapper that returns outputs formatted to match the given schema.14461447 Args:1448 schema: The output schema. Can be passed in as:14491450 - An OpenAI function/tool schema.1451 - A JSON Schema,1452 - A `TypedDict` class,1453 - Or a Pydantic class.14541455 If `schema` is a Pydantic class then the model output will be a1456 Pydantic instance of that class, and the model-generated fields will be1457 validated by the Pydantic class. Otherwise the model output will be a1458 dict and will not be validated.14591460 See `langchain_core.utils.function_calling.convert_to_openai_tool` for1461 more on how to properly specify types and descriptions of schema fields1462 when specifying a Pydantic or `TypedDict` class.14631464 method: The method for steering model generation, one of:14651466 - `'json_schema'`:1467 Uses Ollama's [structured output API](https://ollama.com/blog/structured-outputs)1468 - `'function_calling'`:1469 Uses Ollama's tool-calling API1470 - `'json_mode'`:1471 Specifies `format='json'`. Note that if using JSON mode then you1472 must include instructions for formatting the output into the1473 desired schema into the model call.14741475 include_raw:1476 If `False` then only the parsed structured output is returned.14771478 If an error occurs during model output parsing it will be raised.14791480 If `True` then both the raw model response (a `BaseMessage`) and the1481 parsed model response will be returned.14821483 If an error occurs during output parsing it will be caught and returned1484 as well.14851486 The final output is always a `dict` with keys `'raw'`, `'parsed'`, and1487 `'parsing_error'`.14881489 kwargs: Additional keyword args aren't supported.14901491 Returns:1492 A `Runnable` that takes same inputs as a1493 `langchain_core.language_models.chat.BaseChatModel`. If `include_raw` is1494 `False` and `schema` is a Pydantic class, `Runnable` outputs an instance1495 of `schema` (i.e., a Pydantic object). Otherwise, if `include_raw` is1496 `False` then `Runnable` outputs a `dict`.14971498 If `include_raw` is `True`, then `Runnable` outputs a `dict` with keys:14991500 - `'raw'`: `BaseMessage`1501 - `'parsed'`: `None` if there was a parsing error, otherwise the type1502 depends on the `schema` as described above.1503 - `'parsing_error'`: `BaseException | None`15041505 !!! warning "Behavior changed in `langchain-ollama` 0.2.2"15061507 Added support for structured output API via `format` parameter.15081509 !!! warning "Behavior changed in `langchain-ollama` 0.3.0"15101511 Updated default `method` to `'json_schema'`.15121513 ??? note "Example: `schema=Pydantic` class, `method='json_schema'`, `include_raw=False`"15141515 ```python1516 from typing import Optional15171518 from langchain_ollama import ChatOllama1519 from pydantic import BaseModel, Field152015211522 class AnswerWithJustification(BaseModel):1523 '''An answer to the user question along with justification for the answer.'''15241525 answer: str1526 justification: str | None = Field(1527 default=...,1528 description="A justification for the answer.",1529 )153015311532 model = ChatOllama(model="llama3.1", temperature=0)1533 structured_model = model.with_structured_output(AnswerWithJustification)15341535 structured_model.invoke("What weighs more a pound of bricks or a pound of feathers")15361537 # -> AnswerWithJustification(1538 # answer='They weigh the same',1539 # 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.'1540 # )1541 ```15421543 ??? note "Example: `schema=Pydantic` class, `method='json_schema'`, `include_raw=True`"15441545 ```python1546 from langchain_ollama import ChatOllama1547 from pydantic import BaseModel154815491550 class AnswerWithJustification(BaseModel):1551 '''An answer to the user question along with justification for the answer.'''15521553 answer: str1554 justification: str155515561557 model = ChatOllama(model="llama3.1", temperature=0)1558 structured_model = model.with_structured_output(1559 AnswerWithJustification,1560 include_raw=True,1561 )15621563 structured_model.invoke("What weighs more a pound of bricks or a pound of feathers")1564 # -> {1565 # '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'}]}),1566 # '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.'),1567 # 'parsing_error': None1568 # }1569 ```15701571 ??? note "Example: `schema=Pydantic` class, `method='function_calling'`, `include_raw=False`"15721573 ```python1574 from typing import Optional15751576 from langchain_ollama import ChatOllama1577 from pydantic import BaseModel, Field157815791580 class AnswerWithJustification(BaseModel):1581 '''An answer to the user question along with justification for the answer.'''15821583 answer: str1584 justification: str | None = Field(1585 default=...,1586 description="A justification for the answer.",1587 )158815891590 model = ChatOllama(model="llama3.1", temperature=0)1591 structured_model = model.with_structured_output(1592 AnswerWithJustification,1593 method="function_calling",1594 )15951596 structured_model.invoke("What weighs more a pound of bricks or a pound of feathers")15971598 # -> AnswerWithJustification(1599 # answer='They weigh the same',1600 # 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.'1601 # )1602 ```16031604 ??? note "Example: `schema=TypedDict` class, `method='function_calling'`, `include_raw=False`"16051606 ```python1607 from typing_extensions import Annotated, TypedDict16081609 from langchain_ollama import ChatOllama161016111612 class AnswerWithJustification(TypedDict):1613 '''An answer to the user question along with justification for the answer.'''16141615 answer: str1616 justification: Annotated[str | None, None, "A justification for the answer."]161716181619 model = ChatOllama(model="llama3.1", temperature=0)1620 structured_model = model.with_structured_output(AnswerWithJustification)16211622 structured_model.invoke("What weighs more a pound of bricks or a pound of feathers")1623 # -> {1624 # 'answer': 'They weigh the same',1625 # '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.'1626 # }1627 ```16281629 ??? note "Example: `schema=OpenAI` function schema, `method='function_calling'`, `include_raw=False`"16301631 ```python1632 from langchain_ollama import ChatOllama16331634 oai_schema = {1635 'name': 'AnswerWithJustification',1636 'description': 'An answer to the user question along with justification for the answer.',1637 'parameters': {1638 'type': 'object',1639 'properties': {1640 'answer': {'type': 'string'},1641 'justification': {'description': 'A justification for the answer.', 'type': 'string'}1642 },1643 'required': ['answer']1644 }16451646 model = ChatOllama(model="llama3.1", temperature=0)1647 structured_model = model.with_structured_output(oai_schema)16481649 structured_model.invoke(1650 "What weighs more a pound of bricks or a pound of feathers"1651 )1652 # -> {1653 # 'answer': 'They weigh the same',1654 # '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.'1655 # }1656 ```16571658 ??? note "Example: `schema=Pydantic` class, `method='json_mode'`, `include_raw=True`"16591660 ```python1661 from langchain_ollama import ChatOllama1662 from pydantic import BaseModel166316641665 class AnswerWithJustification(BaseModel):1666 answer: str1667 justification: str166816691670 model = ChatOllama(model="llama3.1", temperature=0)1671 structured_model = model.with_structured_output(1672 AnswerWithJustification, method="json_mode", include_raw=True1673 )16741675 structured_model.invoke(1676 "Answer the following question. "1677 "Make sure to return a JSON blob with keys 'answer' and 'justification'.\\n\\n"1678 "What's heavier a pound of bricks or a pound of feathers?"1679 )1680 # -> {1681 # '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}'),1682 # '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.'),1683 # 'parsing_error': None1684 # }1685 ```16861687 """ # noqa: E5011688 _ = kwargs.pop("strict", None)1689 if kwargs:1690 msg = f"Received unsupported arguments {kwargs}"1691 raise ValueError(msg)1692 is_pydantic_schema = _is_pydantic_class(schema)1693 if method == "function_calling":1694 if schema is None:1695 msg = (1696 "schema must be specified when method is not 'json_mode'. "1697 "Received None."1698 )1699 raise ValueError(msg)1700 formatted_tool = convert_to_openai_tool(schema)1701 tool_name = formatted_tool["function"]["name"]1702 llm = self.bind_tools(1703 [schema],1704 tool_choice=tool_name,1705 ls_structured_output_format={1706 "kwargs": {"method": method},1707 "schema": formatted_tool,1708 },1709 )1710 if is_pydantic_schema:1711 output_parser: Runnable = PydanticToolsParser(1712 tools=[schema], # ty: ignore[invalid-argument-type]1713 first_tool_only=True,1714 )1715 else:1716 output_parser = JsonOutputKeyToolsParser(1717 key_name=tool_name, first_tool_only=True1718 )1719 elif method == "json_mode":1720 llm = self.bind(1721 format="json",1722 ls_structured_output_format={1723 "kwargs": {"method": method},1724 "schema": schema,1725 },1726 )1727 output_parser = (1728 PydanticOutputParser(pydantic_object=schema) # ty: ignore[invalid-argument-type]1729 if is_pydantic_schema1730 else JsonOutputParser()1731 )1732 elif method == "json_schema":1733 if schema is None:1734 msg = (1735 "schema must be specified when method is not 'json_mode'. "1736 "Received None."1737 )1738 raise ValueError(msg)1739 if is_pydantic_schema:1740 schema = cast("TypeBaseModel", schema)1741 if issubclass(schema, BaseModelV1):1742 response_format = schema.schema()1743 else:1744 response_format = schema.model_json_schema()1745 llm = self.bind(1746 format=response_format,1747 ls_structured_output_format={1748 "kwargs": {"method": method},1749 "schema": schema,1750 },1751 )1752 output_parser = PydanticOutputParser(pydantic_object=schema)1753 else:1754 if is_typeddict(schema):1755 response_format = convert_to_json_schema(schema)1756 if "required" not in response_format:1757 response_format["required"] = list(1758 response_format["properties"].keys()1759 )1760 else:1761 # is JSON schema1762 response_format = cast("dict", schema)1763 llm = self.bind(1764 format=response_format,1765 ls_structured_output_format={1766 "kwargs": {"method": method},1767 "schema": response_format,1768 },1769 )1770 output_parser = JsonOutputParser()1771 else:1772 msg = (1773 f"Unrecognized method argument. Expected one of 'function_calling', "1774 f"'json_schema', or 'json_mode'. Received: '{method}'"1775 )1776 raise ValueError(msg)17771778 if include_raw:1779 parser_assign = RunnablePassthrough.assign(1780 parsed=itemgetter("raw") | output_parser, parsing_error=lambda _: None1781 )1782 parser_none = RunnablePassthrough.assign(parsed=lambda _: None)1783 parser_with_fallback = parser_assign.with_fallbacks(1784 [parser_none], exception_key="parsing_error"1785 )1786 return RunnableMap(raw=llm) | parser_with_fallback1787 return llm | output_parser
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.