langchain-ollama 0.2.0.dev1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: langchain-ollama
3
- Version: 0.2.0.dev1
3
+ Version: 0.2.2
4
4
  Summary: An integration package connecting Ollama and LangChain
5
5
  Home-page: https://github.com/langchain-ai/langchain
6
6
  License: MIT
@@ -11,8 +11,9 @@ Classifier: Programming Language :: Python :: 3.9
11
11
  Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
- Requires-Dist: langchain-core (>=0.3.0.dev4,<0.4.0)
15
- Requires-Dist: ollama (>=0.3.0,<1)
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Requires-Dist: langchain-core (>=0.3.27,<0.4.0)
16
+ Requires-Dist: ollama (>=0.4.4,<1)
16
17
  Project-URL: Repository, https://github.com/langchain-ai/langchain
17
18
  Project-URL: Release Notes, https://github.com/langchain-ai/langchain/releases?q=tag%3A%22langchain-ollama%3D%3D0%22&expanded=true
18
19
  Project-URL: Source Code, https://github.com/langchain-ai/langchain/tree/master/libs/partners/ollama
@@ -1,3 +1,9 @@
1
+ """This is the langchain_ollama package.
2
+
3
+ It provides infrastructure for interacting with the Ollama service.
4
+ """
5
+
6
+
1
7
  from importlib import metadata
2
8
 
3
9
  from langchain_ollama.chat_models import ChatOllama
@@ -1,5 +1,7 @@
1
1
  """Ollama chat models."""
2
2
 
3
+ import json
4
+ from operator import itemgetter
3
5
  from typing import (
4
6
  Any,
5
7
  AsyncIterator,
@@ -21,6 +23,7 @@ from langchain_core.callbacks import (
21
23
  CallbackManagerForLLMRun,
22
24
  )
23
25
  from langchain_core.callbacks.manager import AsyncCallbackManagerForLLMRun
26
+ from langchain_core.exceptions import OutputParserException
24
27
  from langchain_core.language_models import LanguageModelInput
25
28
  from langchain_core.language_models.chat_models import BaseChatModel, LangSmithParams
26
29
  from langchain_core.messages import (
@@ -34,13 +37,24 @@ from langchain_core.messages import (
34
37
  )
35
38
  from langchain_core.messages.ai import UsageMetadata
36
39
  from langchain_core.messages.tool import tool_call
40
+ from langchain_core.output_parsers import (
41
+ JsonOutputKeyToolsParser,
42
+ JsonOutputParser,
43
+ PydanticOutputParser,
44
+ PydanticToolsParser,
45
+ )
37
46
  from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult
38
- from langchain_core.runnables import Runnable
47
+ from langchain_core.runnables import Runnable, RunnableMap, RunnablePassthrough
39
48
  from langchain_core.tools import BaseTool
49
+ from langchain_core.utils.function_calling import (
50
+ _convert_any_typed_dicts_to_pydantic as convert_any_typed_dicts_to_pydantic,
51
+ )
40
52
  from langchain_core.utils.function_calling import convert_to_openai_tool
53
+ from langchain_core.utils.pydantic import TypeBaseModel, is_basemodel_subclass
41
54
  from ollama import AsyncClient, Client, Message, Options
42
- from pydantic import PrivateAttr, model_validator
43
- from typing_extensions import Self
55
+ from pydantic import BaseModel, PrivateAttr, model_validator
56
+ from pydantic.json_schema import JsonSchemaValue
57
+ from typing_extensions import Self, is_typeddict
44
58
 
45
59
 
46
60
  def _get_usage_metadata_from_generation_info(
@@ -60,19 +74,85 @@ def _get_usage_metadata_from_generation_info(
60
74
  return None
61
75
 
62
76
 
77
+ def _parse_json_string(
78
+ json_string: str, raw_tool_call: dict[str, Any], skip: bool
79
+ ) -> Any:
80
+ """Attempt to parse a JSON string for tool calling.
81
+
82
+ Args:
83
+ json_string: JSON string to parse.
84
+ skip: Whether to ignore parsing errors and return the value anyways.
85
+ raw_tool_call: Raw tool call to include in error message.
86
+
87
+ Returns:
88
+ The parsed JSON string.
89
+
90
+ Raises:
91
+ OutputParserException: If the JSON string wrong invalid and skip=False.
92
+ """
93
+ try:
94
+ return json.loads(json_string)
95
+ except json.JSONDecodeError as e:
96
+ if skip:
97
+ return json_string
98
+ msg = (
99
+ f"Function {raw_tool_call['function']['name']} arguments:\n\n"
100
+ f"{raw_tool_call['function']['arguments']}\n\nare not valid JSON. "
101
+ f"Received JSONDecodeError {e}"
102
+ )
103
+ raise OutputParserException(msg) from e
104
+ except TypeError as e:
105
+ if skip:
106
+ return json_string
107
+ msg = (
108
+ f"Function {raw_tool_call['function']['name']} arguments:\n\n"
109
+ f"{raw_tool_call['function']['arguments']}\n\nare not a string or a "
110
+ f"dictionary. Received TypeError {e}"
111
+ )
112
+ raise OutputParserException(msg) from e
113
+
114
+
115
+ def _parse_arguments_from_tool_call(
116
+ raw_tool_call: dict[str, Any],
117
+ ) -> Optional[dict[str, Any]]:
118
+ """Parse arguments by trying to parse any shallowly nested string-encoded JSON.
119
+
120
+ Band-aid fix for issue in Ollama with inconsistent tool call argument structure.
121
+ Should be removed/changed if fixed upstream.
122
+ See https://github.com/ollama/ollama/issues/6155
123
+ """
124
+ if "function" not in raw_tool_call:
125
+ return None
126
+ arguments = raw_tool_call["function"]["arguments"]
127
+ parsed_arguments = {}
128
+ if isinstance(arguments, dict):
129
+ for key, value in arguments.items():
130
+ if isinstance(value, str):
131
+ parsed_arguments[key] = _parse_json_string(
132
+ value, skip=True, raw_tool_call=raw_tool_call
133
+ )
134
+ else:
135
+ parsed_arguments[key] = value
136
+ else:
137
+ parsed_arguments = _parse_json_string(
138
+ arguments, skip=False, raw_tool_call=raw_tool_call
139
+ )
140
+ return parsed_arguments
141
+
142
+
63
143
  def _get_tool_calls_from_response(
64
144
  response: Mapping[str, Any],
65
145
  ) -> List[ToolCall]:
66
146
  """Get tool calls from ollama response."""
67
147
  tool_calls = []
68
148
  if "message" in response:
69
- if "tool_calls" in response["message"]:
70
- for tc in response["message"]["tool_calls"]:
149
+ if raw_tool_calls := response["message"].get("tool_calls"):
150
+ for tc in raw_tool_calls:
71
151
  tool_calls.append(
72
152
  tool_call(
73
153
  id=str(uuid4()),
74
154
  name=tc["function"]["name"],
75
- args=tc["function"]["arguments"],
155
+ args=_parse_arguments_from_tool_call(tc) or {},
76
156
  )
77
157
  )
78
158
  return tool_calls
@@ -89,10 +169,16 @@ def _lc_tool_call_to_openai_tool_call(tool_call: ToolCall) -> dict:
89
169
  }
90
170
 
91
171
 
172
+ def _is_pydantic_class(obj: Any) -> bool:
173
+ return isinstance(obj, type) and is_basemodel_subclass(obj)
174
+
175
+
92
176
  class ChatOllama(BaseChatModel):
93
- """Ollama chat model integration.
177
+ r"""Ollama chat model integration.
178
+
179
+ .. dropdown:: Setup
180
+ :open:
94
181
 
95
- Setup:
96
182
  Install ``langchain-ollama`` and download any models you want to use from ollama.
97
183
 
98
184
  .. code-block:: bash
@@ -220,8 +306,6 @@ class ChatOllama(BaseChatModel):
220
306
  '{"location": "Pune, India", "time_of_day": "morning"}'
221
307
 
222
308
  Tool Calling:
223
- .. warning::
224
- Ollama currently does not support streaming for tools
225
309
 
226
310
  .. code-block:: python
227
311
 
@@ -315,8 +399,8 @@ class ChatOllama(BaseChatModel):
315
399
  to more diverse text, while a lower value (e.g., 0.5) will
316
400
  generate more focused and conservative text. (Default: 0.9)"""
317
401
 
318
- format: Literal["", "json"] = ""
319
- """Specify the format of the output (options: json)"""
402
+ format: Optional[Union[Literal["", "json"], JsonSchemaValue]] = None
403
+ """Specify the format of the output (options: "json", JSON schema)."""
320
404
 
321
405
  keep_alive: Optional[Union[int, str]] = None
322
406
  """How long the model will stay loaded into memory."""
@@ -325,27 +409,36 @@ class ChatOllama(BaseChatModel):
325
409
  """Base url the model is hosted under."""
326
410
 
327
411
  client_kwargs: Optional[dict] = {}
328
- """Additional kwargs to pass to the httpx Client.
412
+ """Additional kwargs to pass to the httpx Client.
329
413
  For a full list of the params, see [this link](https://pydoc.dev/httpx/latest/httpx.Client.html)
330
414
  """
331
415
 
332
- _client: Client = PrivateAttr(default=None)
416
+ _client: Client = PrivateAttr(default=None) # type: ignore
333
417
  """
334
418
  The client to use for making requests.
335
419
  """
336
420
 
337
- _async_client: AsyncClient = PrivateAttr(default=None)
421
+ _async_client: AsyncClient = PrivateAttr(default=None) # type: ignore
338
422
  """
339
423
  The async client to use for making requests.
340
424
  """
341
425
 
342
- @property
343
- def _default_params(self) -> Dict[str, Any]:
344
- """Get the default parameters for calling Ollama."""
345
- return {
346
- "model": self.model,
347
- "format": self.format,
348
- "options": {
426
+ def _chat_params(
427
+ self,
428
+ messages: List[BaseMessage],
429
+ stop: Optional[List[str]] = None,
430
+ **kwargs: Any,
431
+ ) -> Dict[str, Any]:
432
+ ollama_messages = self._convert_messages_to_ollama_messages(messages)
433
+
434
+ if self.stop is not None and stop is not None:
435
+ raise ValueError("`stop` found in both the input and default params.")
436
+ elif self.stop is not None:
437
+ stop = self.stop
438
+
439
+ options_dict = kwargs.pop(
440
+ "options",
441
+ {
349
442
  "mirostat": self.mirostat,
350
443
  "mirostat_eta": self.mirostat_eta,
351
444
  "mirostat_tau": self.mirostat_tau,
@@ -357,14 +450,28 @@ class ChatOllama(BaseChatModel):
357
450
  "repeat_penalty": self.repeat_penalty,
358
451
  "temperature": self.temperature,
359
452
  "seed": self.seed,
360
- "stop": self.stop,
453
+ "stop": self.stop if stop is None else stop,
361
454
  "tfs_z": self.tfs_z,
362
455
  "top_k": self.top_k,
363
456
  "top_p": self.top_p,
364
457
  },
365
- "keep_alive": self.keep_alive,
458
+ )
459
+
460
+ params = {
461
+ "messages": ollama_messages,
462
+ "stream": kwargs.pop("stream", True),
463
+ "model": kwargs.pop("model", self.model),
464
+ "format": kwargs.pop("format", self.format),
465
+ "options": Options(**options_dict),
466
+ "keep_alive": kwargs.pop("keep_alive", self.keep_alive),
467
+ **kwargs,
366
468
  }
367
469
 
470
+ if tools := kwargs.get("tools"):
471
+ params["tools"] = tools
472
+
473
+ return params
474
+
368
475
  @model_validator(mode="after")
369
476
  def _set_clients(self) -> Self:
370
477
  """Set clients to use for ollama."""
@@ -462,37 +569,13 @@ class ChatOllama(BaseChatModel):
462
569
  stop: Optional[List[str]] = None,
463
570
  **kwargs: Any,
464
571
  ) -> AsyncIterator[Union[Mapping[str, Any], str]]:
465
- ollama_messages = self._convert_messages_to_ollama_messages(messages)
572
+ chat_params = self._chat_params(messages, stop, **kwargs)
466
573
 
467
- stop = stop if stop is not None else self.stop
468
-
469
- params = self._default_params
470
-
471
- for key in self._default_params:
472
- if key in kwargs:
473
- params[key] = kwargs[key]
474
-
475
- params["options"]["stop"] = stop
476
- if "tools" in kwargs:
477
- yield await self._async_client.chat(
478
- model=params["model"],
479
- messages=ollama_messages,
480
- stream=False,
481
- options=Options(**params["options"]),
482
- keep_alive=params["keep_alive"],
483
- format=params["format"],
484
- tools=kwargs["tools"],
485
- ) # type:ignore
486
- else:
487
- async for part in await self._async_client.chat(
488
- model=params["model"],
489
- messages=ollama_messages,
490
- stream=True,
491
- options=Options(**params["options"]),
492
- keep_alive=params["keep_alive"],
493
- format=params["format"],
494
- ): # type:ignore
574
+ if chat_params["stream"]:
575
+ async for part in await self._async_client.chat(**chat_params):
495
576
  yield part
577
+ else:
578
+ yield await self._async_client.chat(**chat_params)
496
579
 
497
580
  def _create_chat_stream(
498
581
  self,
@@ -500,36 +583,12 @@ class ChatOllama(BaseChatModel):
500
583
  stop: Optional[List[str]] = None,
501
584
  **kwargs: Any,
502
585
  ) -> Iterator[Union[Mapping[str, Any], str]]:
503
- ollama_messages = self._convert_messages_to_ollama_messages(messages)
504
-
505
- stop = stop if stop is not None else self.stop
506
-
507
- params = self._default_params
586
+ chat_params = self._chat_params(messages, stop, **kwargs)
508
587
 
509
- for key in self._default_params:
510
- if key in kwargs:
511
- params[key] = kwargs[key]
512
-
513
- params["options"]["stop"] = stop
514
- if "tools" in kwargs:
515
- yield self._client.chat(
516
- model=params["model"],
517
- messages=ollama_messages,
518
- stream=False,
519
- options=Options(**params["options"]),
520
- keep_alive=params["keep_alive"],
521
- format=params["format"],
522
- tools=kwargs["tools"],
523
- )
588
+ if chat_params["stream"]:
589
+ yield from self._client.chat(**chat_params)
524
590
  else:
525
- yield from self._client.chat(
526
- model=params["model"],
527
- messages=ollama_messages,
528
- stream=True,
529
- options=Options(**params["options"]),
530
- keep_alive=params["keep_alive"],
531
- format=params["format"],
532
- )
591
+ yield self._client.chat(**chat_params)
533
592
 
534
593
  def _chat_stream_with_aggregation(
535
594
  self,
@@ -748,6 +807,8 @@ class ChatOllama(BaseChatModel):
748
807
  def bind_tools(
749
808
  self,
750
809
  tools: Sequence[Union[Dict[str, Any], Type, Callable, BaseTool]],
810
+ *,
811
+ tool_choice: Optional[Union[dict, str, Literal["auto", "any"], bool]] = None,
751
812
  **kwargs: Any,
752
813
  ) -> Runnable[LanguageModelInput, BaseMessage]:
753
814
  """Bind tool-like objects to this chat model.
@@ -758,8 +819,329 @@ class ChatOllama(BaseChatModel):
758
819
  tools: A list of tool definitions to bind to this chat model.
759
820
  Supports any tool definition handled by
760
821
  :meth:`langchain_core.utils.function_calling.convert_to_openai_tool`.
822
+ tool_choice: If provided, which tool for model to call. **This parameter
823
+ is currently ignored as it is not supported by Ollama.**
761
824
  kwargs: Any additional parameters are passed directly to
762
825
  ``self.bind(**kwargs)``.
763
826
  """ # noqa: E501
764
827
  formatted_tools = [convert_to_openai_tool(tool) for tool in tools]
765
828
  return super().bind(tools=formatted_tools, **kwargs)
829
+
830
+ def with_structured_output(
831
+ self,
832
+ schema: Union[Dict, type],
833
+ *,
834
+ method: Literal[
835
+ "function_calling", "json_mode", "json_schema"
836
+ ] = "function_calling",
837
+ include_raw: bool = False,
838
+ **kwargs: Any,
839
+ ) -> Runnable[LanguageModelInput, Union[Dict, BaseModel]]:
840
+ """Model wrapper that returns outputs formatted to match the given schema.
841
+
842
+ Args:
843
+ schema:
844
+ The output schema. Can be passed in as:
845
+
846
+ - a Pydantic class,
847
+ - a JSON schema
848
+ - a TypedDict class
849
+ - an OpenAI function/tool schema.
850
+
851
+ If ``schema`` is a Pydantic class then the model output will be a
852
+ Pydantic instance of that class, and the model-generated fields will be
853
+ validated by the Pydantic class. Otherwise the model output will be a
854
+ dict and will not be validated. See :meth:`langchain_core.utils.function_calling.convert_to_openai_tool`
855
+ for more on how to properly specify types and descriptions of
856
+ schema fields when specifying a Pydantic or TypedDict class.
857
+
858
+ method: The method for steering model generation, one of:
859
+
860
+ - "function_calling":
861
+ Uses Ollama's tool-calling API
862
+ - "json_schema":
863
+ Uses Ollama's structured output API: https://ollama.com/blog/structured-outputs
864
+ - "json_mode":
865
+ Specifies ``format="json"``. Note that if using JSON mode then you
866
+ must include instructions for formatting the output into the
867
+ desired schema into the model call.
868
+
869
+ include_raw:
870
+ If False then only the parsed structured output is returned. If
871
+ an error occurs during model output parsing it will be raised. If True
872
+ then both the raw model response (a BaseMessage) and the parsed model
873
+ response will be returned. If an error occurs during output parsing it
874
+ will be caught and returned as well. The final output is always a dict
875
+ with keys "raw", "parsed", and "parsing_error".
876
+
877
+ kwargs: Additional keyword args aren't supported.
878
+
879
+ Returns:
880
+ A Runnable that takes same inputs as a :class:`langchain_core.language_models.chat.BaseChatModel`.
881
+
882
+ | If ``include_raw`` is False and ``schema`` is a Pydantic class, Runnable outputs an instance of ``schema`` (i.e., a Pydantic object). Otherwise, if ``include_raw`` is False then Runnable outputs a dict.
883
+
884
+ | If ``include_raw`` is True, then Runnable outputs a dict with keys:
885
+
886
+ - "raw": BaseMessage
887
+ - "parsed": None if there was a parsing error, otherwise the type depends on the ``schema`` as described above.
888
+ - "parsing_error": Optional[BaseException]
889
+
890
+ .. versionchanged:: 0.2.2
891
+
892
+ Added support for structured output API via ``format`` parameter.
893
+
894
+ .. dropdown:: Example: schema=Pydantic class, method="function_calling", include_raw=False
895
+
896
+ .. code-block:: python
897
+
898
+ from typing import Optional
899
+
900
+ from langchain_ollama import ChatOllama
901
+ from pydantic import BaseModel, Field
902
+
903
+
904
+ class AnswerWithJustification(BaseModel):
905
+ '''An answer to the user question along with justification for the answer.'''
906
+
907
+ answer: str
908
+ justification: Optional[str] = Field(
909
+ default=..., description="A justification for the answer."
910
+ )
911
+
912
+
913
+ llm = ChatOllama(model="llama3.1", temperature=0)
914
+ structured_llm = llm.with_structured_output(
915
+ AnswerWithJustification
916
+ )
917
+
918
+ structured_llm.invoke(
919
+ "What weighs more a pound of bricks or a pound of feathers"
920
+ )
921
+
922
+ # -> AnswerWithJustification(
923
+ # answer='They weigh the same',
924
+ # 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.'
925
+ # )
926
+
927
+ .. dropdown:: Example: schema=Pydantic class, method="function_calling", include_raw=True
928
+
929
+ .. code-block:: python
930
+
931
+ from langchain_ollama import ChatOllama
932
+ from pydantic import BaseModel
933
+
934
+
935
+ class AnswerWithJustification(BaseModel):
936
+ '''An answer to the user question along with justification for the answer.'''
937
+
938
+ answer: str
939
+ justification: str
940
+
941
+
942
+ llm = ChatOllama(model="llama3.1", temperature=0)
943
+ structured_llm = llm.with_structured_output(
944
+ AnswerWithJustification, include_raw=True
945
+ )
946
+
947
+ structured_llm.invoke(
948
+ "What weighs more a pound of bricks or a pound of feathers"
949
+ )
950
+ # -> {
951
+ # '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'}]}),
952
+ # '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.'),
953
+ # 'parsing_error': None
954
+ # }
955
+
956
+ .. dropdown:: Example: schema=Pydantic class, method="json_schema", include_raw=False
957
+
958
+ .. code-block:: python
959
+
960
+ from typing import Optional
961
+
962
+ from langchain_ollama import ChatOllama
963
+ from pydantic import BaseModel, Field
964
+
965
+
966
+ class AnswerWithJustification(BaseModel):
967
+ '''An answer to the user question along with justification for the answer.'''
968
+
969
+ answer: str
970
+ justification: Optional[str] = Field(
971
+ default=..., description="A justification for the answer."
972
+ )
973
+
974
+
975
+ llm = ChatOllama(model="llama3.1", temperature=0)
976
+ structured_llm = llm.with_structured_output(
977
+ AnswerWithJustification, method="json_schema"
978
+ )
979
+
980
+ structured_llm.invoke(
981
+ "What weighs more a pound of bricks or a pound of feathers"
982
+ )
983
+
984
+ # -> AnswerWithJustification(
985
+ # answer='They weigh the same',
986
+ # 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.'
987
+ # )
988
+
989
+ .. dropdown:: Example: schema=TypedDict class, method="function_calling", include_raw=False
990
+
991
+ .. code-block:: python
992
+
993
+ # IMPORTANT: If you are using Python <=3.8, you need to import Annotated
994
+ # from typing_extensions, not from typing.
995
+ from typing_extensions import Annotated, TypedDict
996
+
997
+ from langchain_ollama import ChatOllama
998
+
999
+
1000
+ class AnswerWithJustification(TypedDict):
1001
+ '''An answer to the user question along with justification for the answer.'''
1002
+
1003
+ answer: str
1004
+ justification: Annotated[
1005
+ Optional[str], None, "A justification for the answer."
1006
+ ]
1007
+
1008
+
1009
+ llm = ChatOllama(model="llama3.1", temperature=0)
1010
+ structured_llm = llm.with_structured_output(AnswerWithJustification)
1011
+
1012
+ structured_llm.invoke(
1013
+ "What weighs more a pound of bricks or a pound of feathers"
1014
+ )
1015
+ # -> {
1016
+ # 'answer': 'They weigh the same',
1017
+ # '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.'
1018
+ # }
1019
+
1020
+ .. dropdown:: Example: schema=OpenAI function schema, method="function_calling", include_raw=False
1021
+
1022
+ .. code-block:: python
1023
+
1024
+ from langchain_ollama import ChatOllama
1025
+
1026
+ oai_schema = {
1027
+ 'name': 'AnswerWithJustification',
1028
+ 'description': 'An answer to the user question along with justification for the answer.',
1029
+ 'parameters': {
1030
+ 'type': 'object',
1031
+ 'properties': {
1032
+ 'answer': {'type': 'string'},
1033
+ 'justification': {'description': 'A justification for the answer.', 'type': 'string'}
1034
+ },
1035
+ 'required': ['answer']
1036
+ }
1037
+ }
1038
+
1039
+ llm = ChatOllama(model="llama3.1", temperature=0)
1040
+ structured_llm = llm.with_structured_output(oai_schema)
1041
+
1042
+ structured_llm.invoke(
1043
+ "What weighs more a pound of bricks or a pound of feathers"
1044
+ )
1045
+ # -> {
1046
+ # 'answer': 'They weigh the same',
1047
+ # '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.'
1048
+ # }
1049
+
1050
+ .. dropdown:: Example: schema=Pydantic class, method="json_mode", include_raw=True
1051
+
1052
+ .. code-block::
1053
+
1054
+ from langchain_ollama import ChatOllama
1055
+ from pydantic import BaseModel
1056
+
1057
+ class AnswerWithJustification(BaseModel):
1058
+ answer: str
1059
+ justification: str
1060
+
1061
+ llm = ChatOllama(model="llama3.1", temperature=0)
1062
+ structured_llm = llm.with_structured_output(
1063
+ AnswerWithJustification,
1064
+ method="json_mode",
1065
+ include_raw=True
1066
+ )
1067
+
1068
+ structured_llm.invoke(
1069
+ "Answer the following question. "
1070
+ "Make sure to return a JSON blob with keys 'answer' and 'justification'.\\n\\n"
1071
+ "What's heavier a pound of bricks or a pound of feathers?"
1072
+ )
1073
+ # -> {
1074
+ # '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}'),
1075
+ # '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.'),
1076
+ # 'parsing_error': None
1077
+ # }
1078
+ """ # noqa: E501, D301
1079
+ if kwargs:
1080
+ raise ValueError(f"Received unsupported arguments {kwargs}")
1081
+ is_pydantic_schema = _is_pydantic_class(schema)
1082
+ if method == "function_calling":
1083
+ if schema is None:
1084
+ raise ValueError(
1085
+ "schema must be specified when method is not 'json_mode'. "
1086
+ "Received None."
1087
+ )
1088
+ tool_name = convert_to_openai_tool(schema)["function"]["name"]
1089
+ llm = self.bind_tools([schema], tool_choice=tool_name)
1090
+ if is_pydantic_schema:
1091
+ output_parser: Runnable = PydanticToolsParser(
1092
+ tools=[schema], # type: ignore[list-item]
1093
+ first_tool_only=True,
1094
+ )
1095
+ else:
1096
+ output_parser = JsonOutputKeyToolsParser(
1097
+ key_name=tool_name, first_tool_only=True
1098
+ )
1099
+ elif method == "json_mode":
1100
+ llm = self.bind(format="json")
1101
+ output_parser = (
1102
+ PydanticOutputParser(pydantic_object=schema) # type: ignore[arg-type]
1103
+ if is_pydantic_schema
1104
+ else JsonOutputParser()
1105
+ )
1106
+ elif method == "json_schema":
1107
+ if schema is None:
1108
+ raise ValueError(
1109
+ "schema must be specified when method is not 'json_mode'. "
1110
+ "Received None."
1111
+ )
1112
+ if is_pydantic_schema:
1113
+ schema = cast(TypeBaseModel, schema)
1114
+ llm = self.bind(format=schema.model_json_schema())
1115
+ output_parser = PydanticOutputParser(pydantic_object=schema)
1116
+ else:
1117
+ if is_typeddict(schema):
1118
+ schema = cast(type, schema)
1119
+ response_format = convert_any_typed_dicts_to_pydantic(
1120
+ schema, visited={}
1121
+ ).schema() # type: ignore[attr-defined]
1122
+ if "required" not in response_format:
1123
+ response_format["required"] = list(
1124
+ response_format["properties"].keys()
1125
+ )
1126
+ else:
1127
+ # is JSON schema
1128
+ response_format = schema
1129
+ llm = self.bind(format=response_format)
1130
+ output_parser = JsonOutputParser()
1131
+ else:
1132
+ raise ValueError(
1133
+ f"Unrecognized method argument. Expected one of 'function_calling', "
1134
+ f"'json_schema', or 'json_mode'. Received: '{method}'"
1135
+ )
1136
+
1137
+ if include_raw:
1138
+ parser_assign = RunnablePassthrough.assign(
1139
+ parsed=itemgetter("raw") | output_parser, parsing_error=lambda _: None
1140
+ )
1141
+ parser_none = RunnablePassthrough.assign(parsed=lambda _: None)
1142
+ parser_with_fallback = parser_assign.with_fallbacks(
1143
+ [parser_none], exception_key="parsing_error"
1144
+ )
1145
+ return RunnableMap(raw=llm) | parser_with_fallback
1146
+ else:
1147
+ return llm | output_parser
@@ -1,3 +1,5 @@
1
+ """Ollama embeddings models."""
2
+
1
3
  from typing import (
2
4
  List,
3
5
  Optional,
@@ -132,12 +134,12 @@ class OllamaEmbeddings(BaseModel, Embeddings):
132
134
  For a full list of the params, see [this link](https://pydoc.dev/httpx/latest/httpx.Client.html)
133
135
  """
134
136
 
135
- _client: Client = PrivateAttr(default=None)
137
+ _client: Client = PrivateAttr(default=None) # type: ignore
136
138
  """
137
139
  The client to use for making requests.
138
140
  """
139
141
 
140
- _async_client: AsyncClient = PrivateAttr(default=None)
142
+ _async_client: AsyncClient = PrivateAttr(default=None) # type: ignore
141
143
  """
142
144
  The async client to use for making requests.
143
145
  """
@@ -116,23 +116,30 @@ class OllamaLLM(BaseLLM):
116
116
  For a full list of the params, see [this link](https://pydoc.dev/httpx/latest/httpx.Client.html)
117
117
  """
118
118
 
119
- _client: Client = PrivateAttr(default=None)
119
+ _client: Client = PrivateAttr(default=None) # type: ignore
120
120
  """
121
121
  The client to use for making requests.
122
122
  """
123
123
 
124
- _async_client: AsyncClient = PrivateAttr(default=None)
124
+ _async_client: AsyncClient = PrivateAttr(default=None) # type: ignore
125
125
  """
126
126
  The async client to use for making requests.
127
127
  """
128
128
 
129
- @property
130
- def _default_params(self) -> Dict[str, Any]:
131
- """Get the default parameters for calling Ollama."""
132
- return {
133
- "model": self.model,
134
- "format": self.format,
135
- "options": {
129
+ def _generate_params(
130
+ self,
131
+ prompt: str,
132
+ stop: Optional[List[str]] = None,
133
+ **kwargs: Any,
134
+ ) -> Dict[str, Any]:
135
+ if self.stop is not None and stop is not None:
136
+ raise ValueError("`stop` found in both the input and default params.")
137
+ elif self.stop is not None:
138
+ stop = self.stop
139
+
140
+ options_dict = kwargs.pop(
141
+ "options",
142
+ {
136
143
  "mirostat": self.mirostat,
137
144
  "mirostat_eta": self.mirostat_eta,
138
145
  "mirostat_tau": self.mirostat_tau,
@@ -143,14 +150,25 @@ class OllamaLLM(BaseLLM):
143
150
  "repeat_last_n": self.repeat_last_n,
144
151
  "repeat_penalty": self.repeat_penalty,
145
152
  "temperature": self.temperature,
146
- "stop": self.stop,
153
+ "stop": self.stop if stop is None else stop,
147
154
  "tfs_z": self.tfs_z,
148
155
  "top_k": self.top_k,
149
156
  "top_p": self.top_p,
150
157
  },
151
- "keep_alive": self.keep_alive,
158
+ )
159
+
160
+ params = {
161
+ "prompt": prompt,
162
+ "stream": kwargs.pop("stream", True),
163
+ "model": kwargs.pop("model", self.model),
164
+ "format": kwargs.pop("format", self.format),
165
+ "options": Options(**options_dict),
166
+ "keep_alive": kwargs.pop("keep_alive", self.keep_alive),
167
+ **kwargs,
152
168
  }
153
169
 
170
+ return params
171
+
154
172
  @property
155
173
  def _llm_type(self) -> str:
156
174
  """Return type of LLM."""
@@ -179,27 +197,10 @@ class OllamaLLM(BaseLLM):
179
197
  stop: Optional[List[str]] = None,
180
198
  **kwargs: Any,
181
199
  ) -> AsyncIterator[Union[Mapping[str, Any], str]]:
182
- if self.stop is not None and stop is not None:
183
- raise ValueError("`stop` found in both the input and default params.")
184
- elif self.stop is not None:
185
- stop = self.stop
186
-
187
- params = self._default_params
188
-
189
- for key in self._default_params:
190
- if key in kwargs:
191
- params[key] = kwargs[key]
192
-
193
- params["options"]["stop"] = stop
194
200
  async for part in await self._async_client.generate(
195
- model=params["model"],
196
- prompt=prompt,
197
- stream=True,
198
- options=Options(**params["options"]),
199
- keep_alive=params["keep_alive"],
200
- format=params["format"],
201
+ **self._generate_params(prompt, stop=stop, **kwargs)
201
202
  ): # type: ignore
202
- yield part
203
+ yield part # type: ignore
203
204
 
204
205
  def _create_generate_stream(
205
206
  self,
@@ -207,26 +208,9 @@ class OllamaLLM(BaseLLM):
207
208
  stop: Optional[List[str]] = None,
208
209
  **kwargs: Any,
209
210
  ) -> Iterator[Union[Mapping[str, Any], str]]:
210
- if self.stop is not None and stop is not None:
211
- raise ValueError("`stop` found in both the input and default params.")
212
- elif self.stop is not None:
213
- stop = self.stop
214
-
215
- params = self._default_params
216
-
217
- for key in self._default_params:
218
- if key in kwargs:
219
- params[key] = kwargs[key]
220
-
221
- params["options"]["stop"] = stop
222
211
  yield from self._client.generate(
223
- model=params["model"],
224
- prompt=prompt,
225
- stream=True,
226
- options=Options(**params["options"]),
227
- keep_alive=params["keep_alive"],
228
- format=params["format"],
229
- )
212
+ **self._generate_params(prompt, stop=stop, **kwargs)
213
+ ) # type: ignore
230
214
 
231
215
  async def _astream_with_aggregation(
232
216
  self,
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "langchain-ollama"
7
- version = "0.2.0.dev1"
7
+ version = "0.2.2"
8
8
  description = "An integration package connecting Ollama and LangChain"
9
9
  authors = []
10
10
  readme = "README.md"
@@ -20,11 +20,24 @@ disallow_untyped_defs = "True"
20
20
 
21
21
  [tool.poetry.dependencies]
22
22
  python = ">=3.9,<4.0"
23
- ollama = ">=0.3.0,<1"
24
- langchain-core = { version = "^0.3.0.dev4", allow-prereleases = true }
23
+ ollama = ">=0.4.4,<1"
24
+ langchain-core = "^0.3.27"
25
25
 
26
26
  [tool.ruff.lint]
27
- select = ["E", "F", "I", "T201"]
27
+ select = [
28
+ "E", # pycodestyle
29
+ "F", # pyflakes
30
+ "I", # isort
31
+ "T201", # print
32
+ "D", # pydocstyle
33
+
34
+ ]
35
+
36
+ [tool.ruff.lint.pydocstyle]
37
+ convention = "google"
38
+
39
+ [tool.ruff.lint.per-file-ignores]
40
+ "tests/**" = ["D"] # ignore docstring checks for tests
28
41
 
29
42
  [tool.coverage.run]
30
43
  omit = ["tests/*"]
@@ -32,7 +45,7 @@ omit = ["tests/*"]
32
45
  [tool.pytest.ini_options]
33
46
  addopts = "--snapshot-warn-unused --strict-markers --strict-config --durations=5"
34
47
  markers = [
35
- "compile: mark placeholder test used to compile integration tests without running them",
48
+ "compile: mark placeholder test used to compile integration tests without running them",
36
49
  ]
37
50
  asyncio_mode = "auto"
38
51
 
@@ -73,7 +86,7 @@ mypy = "^1.7.1"
73
86
  path = "../../core"
74
87
  develop = true
75
88
 
76
- [tool.poetry.group.test.dependencies.langchain-standard-tests]
89
+ [tool.poetry.group.test.dependencies.langchain-tests]
77
90
  path = "../../standard-tests"
78
91
  develop = true
79
92