mistral-haystack 1.2.0__tar.gz → 1.3.0__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.
Files changed (29) hide show
  1. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/CHANGELOG.md +7 -0
  2. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/PKG-INFO +2 -1
  3. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/pyproject.toml +15 -2
  4. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/src/haystack_integrations/components/converters/mistral/ocr_document_converter.py +4 -3
  5. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/src/haystack_integrations/components/embedders/mistral/document_embedder.py +2 -1
  6. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/src/haystack_integrations/components/embedders/mistral/text_embedder.py +1 -1
  7. mistral_haystack-1.3.0/src/haystack_integrations/components/generators/mistral/chat/chat_generator.py +506 -0
  8. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/tests/test_mistral_chat_generator.py +411 -22
  9. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/tests/test_mistral_chat_generator_async.py +109 -24
  10. mistral_haystack-1.2.0/src/haystack_integrations/components/generators/mistral/chat/chat_generator.py +0 -248
  11. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/.gitignore +0 -0
  12. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/LICENSE.txt +0 -0
  13. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/README.md +0 -0
  14. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/examples/chat_generator_with_structured_output.py +0 -0
  15. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/examples/indexing_ocr_pipeline.py +0 -0
  16. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/examples/indexing_pipeline.py +0 -0
  17. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/examples/streaming_chat_with_rag.py +0 -0
  18. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/pydoc/config_docusaurus.yml +0 -0
  19. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/src/haystack_integrations/components/converters/mistral/__init__.py +0 -0
  20. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/src/haystack_integrations/components/converters/py.typed +0 -0
  21. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/src/haystack_integrations/components/embedders/mistral/__init__.py +0 -0
  22. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/src/haystack_integrations/components/embedders/py.typed +0 -0
  23. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/src/haystack_integrations/components/generators/mistral/__init__.py +0 -0
  24. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/src/haystack_integrations/components/generators/mistral/chat/__init__.py +0 -0
  25. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/src/haystack_integrations/components/generators/py.typed +0 -0
  26. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/tests/__init__.py +0 -0
  27. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/tests/test_mistral_document_embedder.py +0 -0
  28. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/tests/test_mistral_text_embedder.py +0 -0
  29. {mistral_haystack-1.2.0 → mistral_haystack-1.3.0}/tests/test_ocr_document_converter.py +0 -0
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/mistral-v1.2.0] - 2026-03-13
4
+
5
+ ### 🧹 Chores
6
+
7
+ - Update mistral integration to work with mistralai>=2.0.0 (#2957)
8
+
9
+
3
10
  ## [integrations/mistral-v1.1.0] - 2026-03-12
4
11
 
5
12
  ### 🚀 Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mistral-haystack
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/mistral#readme
5
5
  Project-URL: Issues, https://github.com/deepset-ai/haystack-core-integrations/issues
6
6
  Project-URL: Source, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/mistral
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.10
14
14
  Classifier: Programming Language :: Python :: 3.11
15
15
  Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
17
18
  Classifier: Programming Language :: Python :: Implementation :: CPython
18
19
  Classifier: Programming Language :: Python :: Implementation :: PyPy
19
20
  Requires-Python: >=3.10
@@ -19,6 +19,7 @@ classifiers = [
19
19
  "Programming Language :: Python :: 3.11",
20
20
  "Programming Language :: Python :: 3.12",
21
21
  "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
22
23
  "Programming Language :: Python :: Implementation :: CPython",
23
24
  "Programming Language :: Python :: Implementation :: PyPy",
24
25
  ]
@@ -64,7 +65,8 @@ dependencies = [
64
65
  unit = 'pytest -m "not integration" {args:tests}'
65
66
  integration = 'pytest -m "integration" {args:tests}'
66
67
  all = 'pytest {args:tests}'
67
- cov-retry = 'pytest --cov=haystack_integrations --reruns 3 --reruns-delay 30 -x {args:tests}'
68
+ unit-cov-retry = 'pytest --cov=haystack_integrations --reruns 3 --reruns-delay 30 -x -m "not integration" {args:tests}'
69
+ integration-cov-append-retry = 'pytest --cov=haystack_integrations --cov-append --reruns 3 --reruns-delay 30 -x -m "integration" {args:tests}'
68
70
 
69
71
  types = """mypy -p haystack_integrations.components.embedders.mistral \
70
72
  -p haystack_integrations.components.generators.mistral \
@@ -82,9 +84,17 @@ line-length = 120
82
84
  [tool.ruff.lint]
83
85
  select = [
84
86
  "A",
87
+ "ANN",
85
88
  "ARG",
86
89
  "B",
87
90
  "C",
91
+ "D102", # Missing docstring in public method
92
+ "D103", # Missing docstring in public function
93
+ "D205", # 1 blank line required between summary line and description
94
+ "D209", # Closing triple quotes go to new line
95
+ "D213", # summary lines must be positioned on the second physical line of the docstring
96
+ "D417", # Missing argument descriptions in the docstring
97
+ "D419", # Docstring is empty
88
98
  "DTZ",
89
99
  "E",
90
100
  "EM",
@@ -122,6 +132,8 @@ ignore = [
122
132
  # Misc
123
133
  "B008",
124
134
  "S101",
135
+ # Allow `Any` - used legitimately for dynamic types and SDK boundaries
136
+ "ANN401",
125
137
  ]
126
138
 
127
139
  [tool.ruff.lint.isort]
@@ -132,11 +144,12 @@ ban-relative-imports = "parents"
132
144
 
133
145
  [tool.ruff.lint.per-file-ignores]
134
146
  # Tests can use magic values, assertions, and relative imports
135
- "tests/**/*" = ["PLR2004", "S101", "TID252"]
147
+ "tests/**/*" = ["D", "PLR2004", "S101", "TID252", "ANN"]
136
148
 
137
149
  [tool.coverage.run]
138
150
  source = ["haystack_integrations"]
139
151
  branch = true
152
+ relative_files = true
140
153
  parallel = false
141
154
 
142
155
 
@@ -26,8 +26,9 @@ logger = logging.getLogger(__name__)
26
26
  @component
27
27
  class MistralOCRDocumentConverter:
28
28
  """
29
- This component extracts text from documents using Mistral's OCR API, with optional structured
30
- annotations for both individual image regions (bounding boxes) and full documents.
29
+ Extract text from documents using Mistral's OCR API with optional structured annotations.
30
+
31
+ Supports optional structured annotations for individual image regions (bounding boxes) and full documents.
31
32
 
32
33
  Accepts document sources in various formats (str/Path for local files, ByteStream for in-memory data,
33
34
  DocumentURLChunk for document URLs, ImageURLChunk for image URLs, or FileChunk for Mistral file IDs)
@@ -117,7 +118,7 @@ class MistralOCRDocumentConverter:
117
118
  image_limit: int | None = None,
118
119
  image_min_size: int | None = None,
119
120
  cleanup_uploaded_files: bool = True,
120
- ):
121
+ ) -> None:
121
122
  """
122
123
  Creates a MistralOCRDocumentConverter component.
123
124
 
@@ -12,6 +12,7 @@ from haystack.utils.auth import Secret
12
12
  class MistralDocumentEmbedder(OpenAIDocumentEmbedder):
13
13
  """
14
14
  A component for computing Document embeddings using Mistral models.
15
+
15
16
  The embedding of each Document is stored in the `embedding` field of the Document.
16
17
 
17
18
  Usage example:
@@ -55,7 +56,7 @@ class MistralDocumentEmbedder(OpenAIDocumentEmbedder):
55
56
  timeout: float | None = None,
56
57
  max_retries: int | None = None,
57
58
  http_client_kwargs: dict[str, Any] | None = None,
58
- ):
59
+ ) -> None:
59
60
  """
60
61
  Creates a MistralDocumentEmbedder component.
61
62
 
@@ -49,7 +49,7 @@ class MistralTextEmbedder(OpenAITextEmbedder):
49
49
  timeout: float | None = None,
50
50
  max_retries: int | None = None,
51
51
  http_client_kwargs: dict[str, Any] | None = None,
52
- ):
52
+ ) -> None:
53
53
  """
54
54
  Creates an MistralTextEmbedder component.
55
55
 
@@ -0,0 +1,506 @@
1
+ # SPDX-FileCopyrightText: 2023-present deepset GmbH <info@deepset.ai>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import json
6
+ from typing import Any, ClassVar
7
+
8
+ from haystack import component, default_to_dict, logging
9
+ from haystack.components.generators.chat import OpenAIChatGenerator
10
+ from haystack.components.generators.chat.openai import _check_finish_reason
11
+ from haystack.dataclasses import (
12
+ ChatMessage,
13
+ ReasoningContent,
14
+ StreamingCallbackT,
15
+ ToolCall,
16
+ select_streaming_callback,
17
+ )
18
+ from haystack.tools import ToolsType, serialize_tools_or_toolset
19
+ from haystack.utils import serialize_callable
20
+ from haystack.utils.auth import Secret
21
+ from openai.lib._pydantic import to_strict_json_schema
22
+ from pydantic import BaseModel
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ def _parse_mistral_content(content: Any) -> tuple[str | None, ReasoningContent | None]:
28
+ """Parse Mistral message content which can be a string or an array of typed blocks."""
29
+ if content is None:
30
+ return None, None
31
+ if isinstance(content, str):
32
+ return content or None, None
33
+ if not isinstance(content, list):
34
+ return str(content), None
35
+
36
+ text_parts: list[str] = []
37
+ thinking_parts: list[str] = []
38
+
39
+ for block in content:
40
+ if not isinstance(block, dict):
41
+ continue
42
+ block_type = block.get("type", "")
43
+ if block_type == "thinking":
44
+ for item in block.get("thinking", []):
45
+ if isinstance(item, dict) and item.get("type") == "text":
46
+ thinking_parts.append(item.get("text", ""))
47
+ elif block_type == "text":
48
+ text_parts.append(block.get("text", ""))
49
+
50
+ text = "".join(text_parts) or None
51
+ reasoning = None
52
+ if thinking_parts:
53
+ reasoning = ReasoningContent(reasoning_text="".join(thinking_parts))
54
+
55
+ return text, reasoning
56
+
57
+
58
+ def _convert_mistral_response_to_chat_messages(response_data: dict[str, Any] | str) -> list[ChatMessage]:
59
+ """Convert a raw Mistral API JSON response to a list of ChatMessages, handling array content."""
60
+ data: dict[str, Any] = json.loads(response_data) if isinstance(response_data, str) else response_data
61
+ completions: list[ChatMessage] = []
62
+ usage = data.get("usage")
63
+ model = data.get("model", "")
64
+
65
+ for choice in data.get("choices", []):
66
+ message = choice.get("message", {})
67
+ text, reasoning = _parse_mistral_content(message.get("content"))
68
+
69
+ tool_calls: list[ToolCall] = []
70
+ for tc in message.get("tool_calls") or []:
71
+ func = tc.get("function", {})
72
+ try:
73
+ arguments = json.loads(func.get("arguments", "{}"))
74
+ tool_calls.append(ToolCall(id=tc.get("id"), tool_name=func.get("name"), arguments=arguments))
75
+ except json.JSONDecodeError:
76
+ logger.warning(
77
+ "Mistral returned malformed JSON for tool call arguments. "
78
+ "Tool call ID: {_id}, Tool name: {_name}, Arguments: {_arguments}",
79
+ _id=tc.get("id"),
80
+ _name=func.get("name"),
81
+ _arguments=func.get("arguments"),
82
+ )
83
+
84
+ meta: dict[str, Any] = {
85
+ "model": model,
86
+ "index": choice.get("index", 0),
87
+ "finish_reason": choice.get("finish_reason"),
88
+ "usage": usage,
89
+ }
90
+
91
+ completions.append(ChatMessage.from_assistant(text=text, tool_calls=tool_calls, meta=meta, reasoning=reasoning))
92
+
93
+ return completions
94
+
95
+
96
+ @component
97
+ class MistralChatGenerator(OpenAIChatGenerator):
98
+ """
99
+ Enables text generation using Mistral AI generative models.
100
+
101
+ For supported models, see [Mistral AI docs](https://docs.mistral.ai/getting-started/models).
102
+
103
+ Users can pass any text generation parameters valid for the Mistral Chat Completion API
104
+ directly to this component via the `generation_kwargs` parameter in `__init__` or the `generation_kwargs`
105
+ parameter in `run` method.
106
+
107
+ Key Features and Compatibility:
108
+ - **Primary Compatibility**: Compatible with the Mistral API Chat Completion endpoint.
109
+ - **Streaming Support**: Supports streaming responses from the Mistral API Chat Completion endpoint.
110
+ - **Customizability**: Supports all parameters supported by the Mistral API Chat Completion endpoint.
111
+ - **Reasoning Support**: Extracts reasoning/thinking content from models that support it
112
+ (e.g., mistral-small with `reasoning_effort`, magistral models) and stores it in the
113
+ `ReasoningContent` field on `ChatMessage`.
114
+
115
+ This component uses the ChatMessage format for structuring both input and output,
116
+ ensuring coherent and contextually relevant responses in chat-based text generation scenarios.
117
+ Details on the ChatMessage format can be found in the
118
+ [Haystack docs](https://docs.haystack.deepset.ai/docs/data-classes#chatmessage)
119
+
120
+ For more details on the parameters supported by the Mistral API, refer to the
121
+ [Mistral API Docs](https://docs.mistral.ai/api/).
122
+
123
+ Usage example:
124
+ ```python
125
+ from haystack_integrations.components.generators.mistral import MistralChatGenerator
126
+ from haystack.dataclasses import ChatMessage
127
+
128
+ messages = [ChatMessage.from_user("What's Natural Language Processing?")]
129
+
130
+ client = MistralChatGenerator()
131
+ response = client.run(messages)
132
+ print(response)
133
+
134
+ >>{'replies': [ChatMessage(_role=<ChatRole.ASSISTANT: 'assistant'>, _content=[TextContent(text=
135
+ >> "Natural Language Processing (NLP) is a branch of artificial intelligence
136
+ >> that focuses on enabling computers to understand, interpret, and generate human language in a way that is
137
+ >> meaningful and useful.")], _name=None,
138
+ >> _meta={'model': 'mistral-small-latest', 'index': 0, 'finish_reason': 'stop',
139
+ >> 'usage': {'prompt_tokens': 15, 'completion_tokens': 36, 'total_tokens': 51}})]}
140
+ ```
141
+
142
+ Reasoning usage example:
143
+ ```python
144
+ from haystack_integrations.components.generators.mistral import MistralChatGenerator
145
+ from haystack.dataclasses import ChatMessage
146
+
147
+ messages = [ChatMessage.from_user("Solve: if x + 3 = 7, what is x?")]
148
+
149
+ client = MistralChatGenerator(
150
+ model="mistral-small-latest",
151
+ generation_kwargs={"reasoning_effort": "high"},
152
+ )
153
+ response = client.run(messages)
154
+ print(response["replies"][0].reasoning) # Access reasoning content
155
+ print(response["replies"][0].text) # Access final answer
156
+ ```
157
+ """
158
+
159
+ SUPPORTED_MODELS: ClassVar[list[str]] = [
160
+ "mistral-medium-2505",
161
+ "mistral-medium-2508",
162
+ "mistral-medium-latest",
163
+ "mistral-medium",
164
+ "mistral-vibe-cli-with-tools",
165
+ "open-mistral-nemo",
166
+ "open-mistral-nemo-2407",
167
+ "mistral-tiny-2407",
168
+ "mistral-tiny-latest",
169
+ "codestral-2508",
170
+ "codestral-latest",
171
+ "devstral-2512",
172
+ "mistral-vibe-cli-latest",
173
+ "devstral-medium-latest",
174
+ "devstral-latest",
175
+ "mistral-small-2506",
176
+ "mistral-small-latest",
177
+ "labs-mistral-small-creative",
178
+ "magistral-medium-2509",
179
+ "magistral-medium-latest",
180
+ "magistral-small-2509",
181
+ "magistral-small-latest",
182
+ "voxtral-small-2507",
183
+ "voxtral-small-latest",
184
+ "mistral-large-2512",
185
+ "mistral-large-latest",
186
+ "ministral-3b-2512",
187
+ "ministral-3b-latest",
188
+ "ministral-8b-2512",
189
+ "ministral-8b-latest",
190
+ "ministral-14b-2512",
191
+ "ministral-14b-latest",
192
+ "mistral-large-2411",
193
+ "pixtral-large-2411",
194
+ "pixtral-large-latest",
195
+ "mistral-large-pixtral-2411",
196
+ "devstral-small-2507",
197
+ "devstral-medium-2507",
198
+ "labs-devstral-small-2512",
199
+ "devstral-small-latest",
200
+ "voxtral-mini-2507",
201
+ "voxtral-mini-latest",
202
+ "voxtral-mini-2602",
203
+ ]
204
+ """A list of models supported by Mistral AI
205
+ see [Mistral AI docs](https://docs.mistral.ai/getting-started/models) for more information
206
+ and send a GET HTTP request to "https://api.mistral.ai/v1/models" for a full list of model IDs."""
207
+
208
+ def __init__(
209
+ self,
210
+ api_key: Secret = Secret.from_env_var("MISTRAL_API_KEY"),
211
+ model: str = "mistral-small-latest",
212
+ streaming_callback: StreamingCallbackT | None = None,
213
+ api_base_url: str | None = "https://api.mistral.ai/v1",
214
+ generation_kwargs: dict[str, Any] | None = None,
215
+ tools: ToolsType | None = None,
216
+ *,
217
+ timeout: float | None = None,
218
+ max_retries: int | None = None,
219
+ http_client_kwargs: dict[str, Any] | None = None,
220
+ ) -> None:
221
+ """
222
+ Creates an instance of MistralChatGenerator.
223
+
224
+ Unless specified otherwise in the `model`, this is for Mistral's `mistral-small-latest` model.
225
+
226
+ :param api_key:
227
+ The Mistral API key.
228
+ :param model:
229
+ The name of the Mistral chat completion model to use.
230
+ :param streaming_callback:
231
+ A callback function that is called when a new token is received from the stream.
232
+ The callback function accepts StreamingChunk as an argument.
233
+ :param api_base_url:
234
+ The Mistral API Base url.
235
+ For more details, see Mistral [docs](https://docs.mistral.ai/api/).
236
+ :param generation_kwargs:
237
+ Other parameters to use for the model. These parameters are all sent directly to
238
+ the Mistral endpoint. See [Mistral API docs](https://docs.mistral.ai/api/) for more details.
239
+ Some of the supported parameters:
240
+ - `max_tokens`: The maximum number of tokens the output text can have.
241
+ - `temperature`: What sampling temperature to use. Higher values mean the model will take more risks.
242
+ Try 0.9 for more creative applications and 0 (argmax sampling) for ones with a well-defined answer.
243
+ - `top_p`: An alternative to sampling with temperature, called nucleus sampling, where the model
244
+ considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens
245
+ comprising the top 10% probability mass are considered.
246
+ - `stream`: Whether to stream back partial progress. If set, tokens will be sent as data-only server-sent
247
+ events as they become available, with the stream terminated by a data: [DONE] message.
248
+ - `safe_prompt`: Whether to inject a safety prompt before all conversations.
249
+ - `random_seed`: The seed to use for random sampling.
250
+ - `reasoning_effort`: Controls reasoning/thinking tokens for models that support adjustable reasoning
251
+ (e.g., `mistral-small-latest`, `mistral-medium`). Accepted values: `"high"`, `"none"`.
252
+ See [Mistral reasoning docs](https://docs.mistral.ai/capabilities/reasoning/).
253
+ - `prompt_mode`: For native reasoning models (magistral). Set to `"reasoning"` to use the default
254
+ reasoning system prompt, or omit for the model's default behavior.
255
+ - `response_format`: A JSON schema or a Pydantic model that enforces the structure of the model's response.
256
+ If provided, the output will always be validated against this
257
+ format (unless the model returns a tool call).
258
+ For details, see the [OpenAI Structured Outputs documentation](https://platform.openai.com/docs/guides/structured-outputs).
259
+ Notes:
260
+ - For structured outputs with streaming,
261
+ the `response_format` must be a JSON schema and not a Pydantic model.
262
+ :param tools:
263
+ A list of Tool and/or Toolset objects, or a single Toolset for which the model can prepare calls.
264
+ Each tool should have a unique name.
265
+ :param timeout:
266
+ The timeout for the Mistral API call. If not set, it defaults to either the `OPENAI_TIMEOUT`
267
+ environment variable, or 30 seconds.
268
+ :param max_retries:
269
+ Maximum number of retries to contact OpenAI after an internal error.
270
+ If not set, it defaults to either the `OPENAI_MAX_RETRIES` environment variable, or set to 5.
271
+ :param http_client_kwargs:
272
+ A dictionary of keyword arguments to configure a custom `httpx.Client`or `httpx.AsyncClient`.
273
+ For more information, see the [HTTPX documentation](https://www.python-httpx.org/api/#client).
274
+ """
275
+ super(MistralChatGenerator, self).__init__( # noqa: UP008
276
+ api_key=api_key,
277
+ model=model,
278
+ streaming_callback=streaming_callback,
279
+ api_base_url=api_base_url,
280
+ organization=None,
281
+ generation_kwargs=generation_kwargs,
282
+ tools=tools,
283
+ timeout=timeout,
284
+ max_retries=max_retries,
285
+ http_client_kwargs=http_client_kwargs,
286
+ )
287
+
288
+ def _prepare_api_call(
289
+ self,
290
+ *,
291
+ messages: list[ChatMessage],
292
+ streaming_callback: StreamingCallbackT | None = None,
293
+ generation_kwargs: dict[str, Any] | None = None,
294
+ tools: ToolsType | None = None,
295
+ tools_strict: bool | None = None,
296
+ ) -> dict[str, Any]:
297
+ api_args = super(MistralChatGenerator, self)._prepare_api_call( # noqa: UP008
298
+ messages=messages,
299
+ streaming_callback=streaming_callback,
300
+ generation_kwargs=generation_kwargs,
301
+ tools=tools,
302
+ tools_strict=tools_strict,
303
+ )
304
+
305
+ if "response_format" in api_args and api_args["response_format"] is None:
306
+ api_args.pop("response_format")
307
+
308
+ extra_body: dict[str, Any] = {}
309
+ for param in ("reasoning_effort", "prompt_mode", "safe_prompt"):
310
+ if param in api_args:
311
+ extra_body[param] = api_args.pop(param)
312
+ if extra_body:
313
+ api_args.setdefault("extra_body", {}).update(extra_body)
314
+
315
+ for i, chat_msg in enumerate(messages):
316
+ if chat_msg.reasoning and chat_msg.reasoning.reasoning_text:
317
+ formatted = api_args["messages"][i]
318
+ text_content = formatted.get("content", "") or ""
319
+ formatted["content"] = [
320
+ {"type": "thinking", "thinking": [{"type": "text", "text": chat_msg.reasoning.reasoning_text}]},
321
+ {"type": "text", "text": text_content},
322
+ ]
323
+
324
+ return api_args
325
+
326
+ @component.output_types(replies=list[ChatMessage])
327
+ def run(
328
+ self,
329
+ messages: list[ChatMessage],
330
+ streaming_callback: StreamingCallbackT | None = None,
331
+ generation_kwargs: dict[str, Any] | None = None,
332
+ *,
333
+ tools: ToolsType | None = None,
334
+ tools_strict: bool | None = None,
335
+ ) -> dict[str, list[ChatMessage]]:
336
+ """
337
+ Invokes chat completion on the Mistral API.
338
+
339
+ :param messages:
340
+ A list of ChatMessage instances representing the input messages.
341
+ :param streaming_callback:
342
+ A callback function that is called when a new token is received from the stream.
343
+ :param generation_kwargs:
344
+ Additional keyword arguments for text generation. These parameters will
345
+ override the parameters passed during component initialization.
346
+ For details on Mistral API parameters, see
347
+ [Mistral docs](https://docs.mistral.ai/api/).
348
+ :param tools: A list of Tool and/or Toolset objects, or a single Toolset for which the model can prepare calls.
349
+ If set, it will override the `tools` parameter provided during initialization.
350
+ :param tools_strict:
351
+ Whether to enable strict schema adherence for tool calls.
352
+
353
+ :returns:
354
+ A dictionary with the following key:
355
+ - `replies`: A list containing the generated responses as ChatMessage instances.
356
+ """
357
+ if not self._is_warmed_up:
358
+ self.warm_up()
359
+
360
+ if len(messages) == 0:
361
+ return {"replies": []}
362
+
363
+ streaming_callback = select_streaming_callback(
364
+ init_callback=self.streaming_callback, runtime_callback=streaming_callback, requires_async=False
365
+ )
366
+
367
+ if streaming_callback is not None:
368
+ merged_kwargs = {**self.generation_kwargs, **(generation_kwargs or {})}
369
+ if merged_kwargs.get("reasoning_effort") or merged_kwargs.get("prompt_mode"):
370
+ logger.warning(
371
+ "Streaming with reasoning parameters is active. Reasoning content from thinking "
372
+ "blocks will not be captured during streaming. Use non-streaming mode to extract "
373
+ "reasoning content."
374
+ )
375
+
376
+ api_args = self._prepare_api_call(
377
+ messages=messages,
378
+ streaming_callback=streaming_callback,
379
+ generation_kwargs=generation_kwargs,
380
+ tools=tools,
381
+ tools_strict=tools_strict,
382
+ )
383
+ openai_endpoint = api_args.pop("openai_endpoint")
384
+
385
+ if streaming_callback is not None:
386
+ chat_completion = getattr(self.client.chat.completions, openai_endpoint)(**api_args)
387
+ completions = self._handle_stream_response(chat_completion, streaming_callback)
388
+ else:
389
+ raw_response = getattr(self.client.chat.completions.with_raw_response, openai_endpoint)(**api_args)
390
+ completions = _convert_mistral_response_to_chat_messages(raw_response.text)
391
+
392
+ for message in completions:
393
+ _check_finish_reason(message.meta)
394
+
395
+ return {"replies": completions}
396
+
397
+ @component.output_types(replies=list[ChatMessage])
398
+ async def run_async(
399
+ self,
400
+ messages: list[ChatMessage],
401
+ streaming_callback: StreamingCallbackT | None = None,
402
+ generation_kwargs: dict[str, Any] | None = None,
403
+ *,
404
+ tools: ToolsType | None = None,
405
+ tools_strict: bool | None = None,
406
+ ) -> dict[str, list[ChatMessage]]:
407
+ """
408
+ Asynchronously invokes chat completion on the Mistral API.
409
+
410
+ :param messages:
411
+ A list of ChatMessage instances representing the input messages.
412
+ :param streaming_callback:
413
+ A callback function that is called when a new token is received from the stream.
414
+ Must be a coroutine.
415
+ :param generation_kwargs:
416
+ Additional keyword arguments for text generation.
417
+ :param tools: A list of Tool and/or Toolset objects, or a single Toolset.
418
+ :param tools_strict:
419
+ Whether to enable strict schema adherence for tool calls.
420
+
421
+ :returns:
422
+ A dictionary with the following key:
423
+ - `replies`: A list containing the generated responses as ChatMessage instances.
424
+ """
425
+ if not self._is_warmed_up:
426
+ self.warm_up()
427
+
428
+ if len(messages) == 0:
429
+ return {"replies": []}
430
+
431
+ streaming_callback = select_streaming_callback(
432
+ init_callback=self.streaming_callback, runtime_callback=streaming_callback, requires_async=True
433
+ )
434
+
435
+ if streaming_callback is not None:
436
+ merged_kwargs = {**self.generation_kwargs, **(generation_kwargs or {})}
437
+ if merged_kwargs.get("reasoning_effort") or merged_kwargs.get("prompt_mode"):
438
+ logger.warning(
439
+ "Streaming with reasoning parameters is active. Reasoning content from thinking "
440
+ "blocks will not be captured during streaming. Use non-streaming mode to extract "
441
+ "reasoning content."
442
+ )
443
+
444
+ api_args = self._prepare_api_call(
445
+ messages=messages,
446
+ streaming_callback=streaming_callback,
447
+ generation_kwargs=generation_kwargs,
448
+ tools=tools,
449
+ tools_strict=tools_strict,
450
+ )
451
+ openai_endpoint = api_args.pop("openai_endpoint")
452
+
453
+ if streaming_callback is not None:
454
+ chat_completion = await getattr(self.async_client.chat.completions, openai_endpoint)(**api_args)
455
+ completions = await self._handle_async_stream_response(chat_completion, streaming_callback)
456
+ else:
457
+ raw_response = await getattr(self.async_client.chat.completions.with_raw_response, openai_endpoint)(
458
+ **api_args
459
+ )
460
+ completions = _convert_mistral_response_to_chat_messages(raw_response.text)
461
+
462
+ for message in completions:
463
+ _check_finish_reason(message.meta)
464
+
465
+ return {"replies": completions}
466
+
467
+ def to_dict(self) -> dict[str, Any]:
468
+ """
469
+ Serialize this component to a dictionary.
470
+
471
+ :returns:
472
+ The serialized component as a dictionary.
473
+ """
474
+ callback_name = serialize_callable(self.streaming_callback) if self.streaming_callback else None
475
+ generation_kwargs = self.generation_kwargs.copy()
476
+ response_format = generation_kwargs.get("response_format")
477
+
478
+ # If the response format is a Pydantic model, it's converted to openai's json schema format
479
+ # If it's already a json schema, it's left as is
480
+ if response_format and isinstance(response_format, type) and issubclass(response_format, BaseModel):
481
+ json_schema = {
482
+ "type": "json_schema",
483
+ "json_schema": {
484
+ "name": response_format.__name__,
485
+ "strict": True,
486
+ "schema": to_strict_json_schema(response_format),
487
+ },
488
+ }
489
+
490
+ generation_kwargs["response_format"] = json_schema
491
+
492
+ # if we didn't implement the to_dict method here then the to_dict method of the superclass would be used
493
+ # which would serialiaze some fields that we don't want to serialize (e.g. the ones we don't have in
494
+ # the __init__)
495
+ return default_to_dict(
496
+ self,
497
+ model=self.model,
498
+ streaming_callback=callback_name,
499
+ api_base_url=self.api_base_url,
500
+ generation_kwargs=generation_kwargs,
501
+ api_key=self.api_key.to_dict(),
502
+ tools=serialize_tools_or_toolset(self.tools),
503
+ timeout=self.timeout,
504
+ max_retries=self.max_retries,
505
+ http_client_kwargs=self.http_client_kwargs,
506
+ )