langchain-ollama 0.2.1__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.1
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
@@ -12,8 +12,8 @@ Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
- Requires-Dist: langchain-core (>=0.3.20,<0.4.0)
16
- Requires-Dist: ollama (>=0.3.0,<1)
15
+ Requires-Dist: langchain-core (>=0.3.27,<0.4.0)
16
+ Requires-Dist: ollama (>=0.4.4,<1)
17
17
  Project-URL: Repository, https://github.com/langchain-ai/langchain
18
18
  Project-URL: Release Notes, https://github.com/langchain-ai/langchain/releases?q=tag%3A%22langchain-ollama%3D%3D0%22&expanded=true
19
19
  Project-URL: Source Code, https://github.com/langchain-ai/langchain/tree/master/libs/partners/ollama
@@ -1,6 +1,7 @@
1
1
  """Ollama chat models."""
2
2
 
3
3
  import json
4
+ from operator import itemgetter
4
5
  from typing import (
5
6
  Any,
6
7
  AsyncIterator,
@@ -36,13 +37,24 @@ from langchain_core.messages import (
36
37
  )
37
38
  from langchain_core.messages.ai import UsageMetadata
38
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
+ )
39
46
  from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult
40
- from langchain_core.runnables import Runnable
47
+ from langchain_core.runnables import Runnable, RunnableMap, RunnablePassthrough
41
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
+ )
42
52
  from langchain_core.utils.function_calling import convert_to_openai_tool
53
+ from langchain_core.utils.pydantic import TypeBaseModel, is_basemodel_subclass
43
54
  from ollama import AsyncClient, Client, Message, Options
44
- from pydantic import PrivateAttr, model_validator
45
- 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
46
58
 
47
59
 
48
60
  def _get_usage_metadata_from_generation_info(
@@ -157,6 +169,10 @@ def _lc_tool_call_to_openai_tool_call(tool_call: ToolCall) -> dict:
157
169
  }
158
170
 
159
171
 
172
+ def _is_pydantic_class(obj: Any) -> bool:
173
+ return isinstance(obj, type) and is_basemodel_subclass(obj)
174
+
175
+
160
176
  class ChatOllama(BaseChatModel):
161
177
  r"""Ollama chat model integration.
162
178
 
@@ -290,8 +306,6 @@ class ChatOllama(BaseChatModel):
290
306
  '{"location": "Pune, India", "time_of_day": "morning"}'
291
307
 
292
308
  Tool Calling:
293
- .. warning::
294
- Ollama currently does not support streaming for tools
295
309
 
296
310
  .. code-block:: python
297
311
 
@@ -385,8 +399,8 @@ class ChatOllama(BaseChatModel):
385
399
  to more diverse text, while a lower value (e.g., 0.5) will
386
400
  generate more focused and conservative text. (Default: 0.9)"""
387
401
 
388
- format: Literal["", "json"] = ""
389
- """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)."""
390
404
 
391
405
  keep_alive: Optional[Union[int, str]] = None
392
406
  """How long the model will stay loaded into memory."""
@@ -443,12 +457,9 @@ class ChatOllama(BaseChatModel):
443
457
  },
444
458
  )
445
459
 
446
- tools = kwargs.get("tools")
447
- default_stream = not bool(tools)
448
-
449
460
  params = {
450
461
  "messages": ollama_messages,
451
- "stream": kwargs.pop("stream", default_stream),
462
+ "stream": kwargs.pop("stream", True),
452
463
  "model": kwargs.pop("model", self.model),
453
464
  "format": kwargs.pop("format", self.format),
454
465
  "options": Options(**options_dict),
@@ -456,7 +467,7 @@ class ChatOllama(BaseChatModel):
456
467
  **kwargs,
457
468
  }
458
469
 
459
- if tools:
470
+ if tools := kwargs.get("tools"):
460
471
  params["tools"] = tools
461
472
 
462
473
  return params
@@ -815,3 +826,322 @@ class ChatOllama(BaseChatModel):
815
826
  """ # noqa: E501
816
827
  formatted_tools = [convert_to_openai_tool(tool) for tool in tools]
817
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
@@ -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.1"
7
+ version = "0.2.2"
8
8
  description = "An integration package connecting Ollama and LangChain"
9
9
  authors = []
10
10
  readme = "README.md"
@@ -20,8 +20,8 @@ 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 = "^0.3.20"
23
+ ollama = ">=0.4.4,<1"
24
+ langchain-core = "^0.3.27"
25
25
 
26
26
  [tool.ruff.lint]
27
27
  select = [