langchain-ollama 0.2.0.dev1__py3-none-any.whl → 0.2.2__py3-none-any.whl
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.
- langchain_ollama/__init__.py +6 -0
- langchain_ollama/chat_models.py +463 -81
- langchain_ollama/embeddings.py +4 -2
- langchain_ollama/llms.py +33 -49
- {langchain_ollama-0.2.0.dev1.dist-info → langchain_ollama-0.2.2.dist-info}/METADATA +4 -3
- langchain_ollama-0.2.2.dist-info/RECORD +9 -0
- {langchain_ollama-0.2.0.dev1.dist-info → langchain_ollama-0.2.2.dist-info}/WHEEL +1 -1
- langchain_ollama-0.2.0.dev1.dist-info/RECORD +0 -9
- {langchain_ollama-0.2.0.dev1.dist-info → langchain_ollama-0.2.2.dist-info}/LICENSE +0 -0
langchain_ollama/__init__.py
CHANGED
langchain_ollama/chat_models.py
CHANGED
@@ -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
|
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
|
70
|
-
for tc in
|
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
|
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
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
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
|
-
|
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
|
-
|
572
|
+
chat_params = self._chat_params(messages, stop, **kwargs)
|
466
573
|
|
467
|
-
|
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
|
-
|
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
|
-
|
510
|
-
|
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
|
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
|
langchain_ollama/embeddings.py
CHANGED
@@ -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
|
"""
|
langchain_ollama/llms.py
CHANGED
@@ -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
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
224
|
-
|
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,
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: langchain-ollama
|
3
|
-
Version: 0.2.
|
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
|
-
|
15
|
-
Requires-Dist:
|
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
|
@@ -0,0 +1,9 @@
|
|
1
|
+
langchain_ollama/__init__.py,sha256=SxPRrWcPayJpbwhheTtlqCaPp9ffiAAgZMM5Wc1yYpM,634
|
2
|
+
langchain_ollama/chat_models.py,sha256=_kEf5o3CGtP7JL0HKZ8sIH9xdzktnGbBVdXULR2_sdo,47323
|
3
|
+
langchain_ollama/embeddings.py,sha256=svqdPF44qX5qbFpZmLiXrzTC-AldmMlZRS5wBfY-EmA,5056
|
4
|
+
langchain_ollama/llms.py,sha256=ojnYU0efhN10xhUINu1dCR2Erw79J_mYS6_l45J7Vls,12778
|
5
|
+
langchain_ollama/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
|
+
langchain_ollama-0.2.2.dist-info/LICENSE,sha256=2btS8uNUDWD_UNjw9ba6ZJt_00aUjEw9CGyK-xIHY8c,1072
|
7
|
+
langchain_ollama-0.2.2.dist-info/METADATA,sha256=8eb8LP6TyzNBiiO60iS5ZMLKK0XW6PQUdKKQPHwYD7M,1876
|
8
|
+
langchain_ollama-0.2.2.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
9
|
+
langchain_ollama-0.2.2.dist-info/RECORD,,
|
@@ -1,9 +0,0 @@
|
|
1
|
-
langchain_ollama/__init__.py,sha256=HhQZqbCjhrbr2dC_9Dkw12pg4HPjnDXUoInROMNJKqA,518
|
2
|
-
langchain_ollama/chat_models.py,sha256=q_URs_NzgY87XZ0RBDu-TY_seTh2lKXbtCXB7xY_utE,30423
|
3
|
-
langchain_ollama/embeddings.py,sha256=46gmGxzK5Cm0GYesTSSgWupJYmJ2ywN7FQUAl0fzpxE,4991
|
4
|
-
langchain_ollama/llms.py,sha256=uwQfKwDHXhWWVSAFzHpuv8SirBwKp0H4irnA8lqU0M4,13259
|
5
|
-
langchain_ollama/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
|
-
langchain_ollama-0.2.0.dev1.dist-info/LICENSE,sha256=2btS8uNUDWD_UNjw9ba6ZJt_00aUjEw9CGyK-xIHY8c,1072
|
7
|
-
langchain_ollama-0.2.0.dev1.dist-info/METADATA,sha256=qI7Sy504_I0CEJJNrXBZwHTz1b_f6QMKYdjEowTETh4,1834
|
8
|
-
langchain_ollama-0.2.0.dev1.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
9
|
-
langchain_ollama-0.2.0.dev1.dist-info/RECORD,,
|
File without changes
|