mistral_common 1.0.0__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.
File without changes
mistral_common/base.py ADDED
@@ -0,0 +1,9 @@
1
+ from pydantic import BaseModel, ConfigDict
2
+
3
+
4
+ class MistralBase(BaseModel):
5
+ """
6
+ Base class for all Mistral Pydantic models.
7
+ """
8
+
9
+ model_config = ConfigDict(extra="forbid", strict=True, validate_default=True, use_enum_values=True)
Binary file
@@ -0,0 +1,67 @@
1
+ from typing import Optional
2
+
3
+
4
+ class MistralCommonException(Exception):
5
+ message: str = "Internal server error"
6
+
7
+ def __init__(
8
+ self,
9
+ message: Optional[str] = None,
10
+ ) -> None:
11
+ if message:
12
+ self.message = message
13
+
14
+
15
+ class TokenizerException(MistralCommonException):
16
+ def __init__(self, message: str) -> None:
17
+ super().__init__(message)
18
+
19
+
20
+ class UnsupportedTokenizerFeatureException(MistralCommonException):
21
+ def __init__(self, message: str) -> None:
22
+ super().__init__(message)
23
+
24
+
25
+ class InvalidRequestException(MistralCommonException):
26
+ def __init__(self, message: str) -> None:
27
+ super().__init__(message)
28
+
29
+
30
+ class InvalidSystemPromptException(MistralCommonException):
31
+ def __init__(self, message: str) -> None:
32
+ super().__init__(message)
33
+
34
+
35
+ class InvalidMessageStructureException(MistralCommonException):
36
+ def __init__(self, message: str) -> None:
37
+ super().__init__(message)
38
+
39
+
40
+ class InvalidAssistantMessageException(MistralCommonException):
41
+ def __init__(self, message: str) -> None:
42
+ super().__init__(message)
43
+
44
+
45
+ class InvalidToolMessageException(MistralCommonException):
46
+ def __init__(self, message: str) -> None:
47
+ super().__init__(message)
48
+
49
+
50
+ class InvalidToolSchemaException(MistralCommonException):
51
+ def __init__(self, message: str) -> None:
52
+ super().__init__(message)
53
+
54
+
55
+ class InvalidUserMessageException(MistralCommonException):
56
+ def __init__(self, message: str) -> None:
57
+ super().__init__(message)
58
+
59
+
60
+ class InvalidFunctionCallException(MistralCommonException):
61
+ def __init__(self, message: str) -> None:
62
+ super().__init__(message)
63
+
64
+
65
+ class InvalidToolException(MistralCommonException):
66
+ def __init__(self, message: str) -> None:
67
+ super().__init__(message)
File without changes
@@ -0,0 +1,9 @@
1
+ from typing import Optional
2
+
3
+ from mistral_common.base import MistralBase
4
+
5
+
6
+ class UsageInfo(MistralBase):
7
+ prompt_tokens: int = 0
8
+ total_tokens: int = 0
9
+ completion_tokens: Optional[int] = 0
@@ -0,0 +1,10 @@
1
+ from typing import List, Optional, Union
2
+
3
+ from mistral_common.base import MistralBase
4
+ from pydantic import Field
5
+
6
+
7
+ class EmbeddingRequest(MistralBase):
8
+ input: Union[str, List[str]] = Field(description="Text to embed.")
9
+ model: str = Field(description="ID of the model to use.")
10
+ encoding_format: Optional[str] = Field(default="float", description="The format to return the embeddings in.")
@@ -0,0 +1,20 @@
1
+ from typing import List
2
+
3
+ from mistral_common.base import MistralBase
4
+ from mistral_common.protocol.base import UsageInfo
5
+ from mistral_common.protocol.utils import random_uuid
6
+ from pydantic import Field
7
+
8
+
9
+ class EmbeddingObject(MistralBase):
10
+ object: str = Field(default="embedding", description="The type of the object returned.")
11
+ embedding: List[float] = Field(description="The type of the object returned.")
12
+ index: int = Field(description="The index of the embedding in the input text.")
13
+
14
+
15
+ class EmbeddingResponse(MistralBase):
16
+ id: str = Field(default_factory=lambda: f"embd-{random_uuid()}")
17
+ object: str = Field(default="list", description="The type of the object returned.")
18
+ data: List[EmbeddingObject] = Field(description="List of embeddings.")
19
+ model: str = Field(description="The model used to generate the embeddings.")
20
+ usage: UsageInfo
File without changes
@@ -0,0 +1,56 @@
1
+ from enum import Enum
2
+ from typing import List, Literal, Optional, Union
3
+
4
+ from pydantic import Field
5
+ from typing_extensions import Annotated # compatibility with 3.8
6
+
7
+ from mistral_common.base import MistralBase
8
+ from mistral_common.protocol.instruct.tool_calls import ToolCall
9
+
10
+
11
+ class Roles(str, Enum):
12
+ system = "system"
13
+ user = "user"
14
+ assistant = "assistant"
15
+ tool = "tool"
16
+
17
+
18
+ class ChunkTypes(str, Enum):
19
+ text = "text"
20
+
21
+
22
+ class ContentChunk(MistralBase):
23
+ type: ChunkTypes = ChunkTypes.text
24
+ text: str
25
+
26
+
27
+ class BaseMessage(MistralBase):
28
+ role: Literal[Roles.system, Roles.user, Roles.assistant, Roles.tool]
29
+
30
+
31
+ class UserMessage(BaseMessage):
32
+ role: Literal[Roles.user] = Roles.user
33
+ content: Union[str, List[ContentChunk]]
34
+
35
+
36
+ class SystemMessage(BaseMessage):
37
+ role: Literal[Roles.system] = Roles.system
38
+ content: Union[str, List[ContentChunk]]
39
+
40
+
41
+ class AssistantMessage(BaseMessage):
42
+ role: Literal[Roles.assistant] = Roles.assistant
43
+ content: Optional[str] = None
44
+ tool_calls: Optional[List[ToolCall]] = None
45
+
46
+
47
+ class ToolMessage(BaseMessage):
48
+ content: str
49
+ role: Literal[Roles.tool] = Roles.tool
50
+ tool_call_id: Optional[str] = None
51
+
52
+ # Deprecated in V3 tokenization
53
+ name: Optional[str] = None
54
+
55
+
56
+ ChatMessage = Annotated[Union[SystemMessage, UserMessage, AssistantMessage, ToolMessage], Field(discriminator="role")]
@@ -0,0 +1,29 @@
1
+ from enum import Enum
2
+ from typing import List, Optional
3
+
4
+ from pydantic import Field
5
+
6
+ from mistral_common.base import MistralBase
7
+ from mistral_common.protocol.instruct.messages import ChatMessage
8
+ from mistral_common.protocol.instruct.tool_calls import Tool, ToolChoice
9
+
10
+
11
+ class ResponseFormats(str, Enum):
12
+ text: str = "text"
13
+ json: str = "json_object"
14
+
15
+
16
+ class ResponseFormat(MistralBase):
17
+ type: ResponseFormats = ResponseFormats.text
18
+
19
+
20
+ class ChatCompletionRequest(MistralBase):
21
+ model: Optional[str] = None
22
+ messages: List[ChatMessage]
23
+ response_format: ResponseFormat = Field(default_factory=ResponseFormat)
24
+ tools: List[Tool] = Field(default_factory=list)
25
+ tool_choice: ToolChoice = ToolChoice.auto
26
+ temperature: float = Field(default=0.7, ge=0.0, le=1.0)
27
+ top_p: float = Field(default=1.0, ge=0.0, le=1.0)
28
+ max_tokens: Optional[int] = Field(default=None, ge=0)
29
+ random_seed: Optional[int] = Field(default=None, ge=0)
@@ -0,0 +1,66 @@
1
+ import time
2
+ from enum import Enum
3
+ from typing import List, Optional
4
+
5
+ from pydantic import Field
6
+
7
+ from mistral_common.base import MistralBase
8
+ from mistral_common.protocol.base import UsageInfo
9
+ from mistral_common.protocol.instruct.tool_calls import ToolCall
10
+ from mistral_common.protocol.utils import random_uuid
11
+
12
+
13
+ class FinishReason(str, Enum):
14
+ stop: str = "stop"
15
+ length: str = "length"
16
+ model_length: str = "model_length"
17
+ error: str = "error"
18
+ tool_call: str = "tool_calls"
19
+
20
+
21
+ class ChatCompletionTokenLogprobs(MistralBase):
22
+ token: str
23
+ logprob: float
24
+ bytes: List[int]
25
+
26
+
27
+ class ChatCompletionResponseChoiceLogprobs(MistralBase):
28
+ content: List[ChatCompletionTokenLogprobs]
29
+
30
+
31
+ class DeltaMessage(MistralBase):
32
+ role: Optional[str] = None
33
+ content: Optional[str] = None
34
+ tool_calls: Optional[List[ToolCall]] = None
35
+
36
+
37
+ class ChatCompletionResponseChoice(MistralBase):
38
+ index: int
39
+ message: DeltaMessage
40
+ finish_reason: Optional[FinishReason] = None
41
+ logprobs: Optional[ChatCompletionResponseChoiceLogprobs] = None
42
+
43
+
44
+ class ChatCompletionResponse(MistralBase):
45
+ id: str = Field(default_factory=lambda: f"chatcmpl-{random_uuid()}")
46
+ object: str = "chat.completion"
47
+ created: int = Field(default_factory=lambda: int(time.time()))
48
+ model: str
49
+ choices: List[ChatCompletionResponseChoice]
50
+ usage: UsageInfo
51
+
52
+
53
+ class ChatCompletionResponseStreamChoice(MistralBase):
54
+ index: int
55
+ delta: DeltaMessage
56
+ finish_reason: Optional[FinishReason] = None
57
+ logprobs: Optional[ChatCompletionResponseChoiceLogprobs] = None
58
+
59
+
60
+ class ChatCompletionStreamResponse(MistralBase):
61
+ id: str = Field(default_factory=lambda: f"chatcmpl-{random_uuid()}")
62
+ object: str = "chat.completion.chunk"
63
+ created: int = Field(default_factory=lambda: int(time.time()))
64
+ model: str
65
+ choices: List[ChatCompletionResponseStreamChoice]
66
+ usage: Optional[UsageInfo] = None
@@ -0,0 +1,36 @@
1
+ from enum import Enum
2
+ from typing import Any, Dict
3
+
4
+ from mistral_common.base import MistralBase
5
+
6
+
7
+ class Function(MistralBase):
8
+ name: str
9
+ description: str = ""
10
+ parameters: Dict[str, Any]
11
+
12
+
13
+ class ToolType(str, Enum):
14
+ function = "function"
15
+
16
+
17
+ class ToolChoice(str, Enum):
18
+ auto: str = "auto"
19
+ none: str = "none"
20
+ any: str = "any"
21
+
22
+
23
+ class Tool(MistralBase):
24
+ type: ToolType = ToolType.function
25
+ function: Function
26
+
27
+
28
+ class FunctionCall(MistralBase):
29
+ name: str
30
+ arguments: str
31
+
32
+
33
+ class ToolCall(MistralBase):
34
+ id: str = "null" # required for V3 tokenization
35
+ type: ToolType = ToolType.function
36
+ function: FunctionCall
@@ -0,0 +1,282 @@
1
+ import re
2
+ from enum import Enum
3
+ from typing import List
4
+
5
+ from jsonschema import Draft7Validator, SchemaError
6
+
7
+ from mistral_common.exceptions import (
8
+ InvalidAssistantMessageException,
9
+ InvalidFunctionCallException,
10
+ InvalidMessageStructureException,
11
+ InvalidRequestException,
12
+ InvalidSystemPromptException,
13
+ InvalidToolException,
14
+ InvalidToolMessageException,
15
+ InvalidToolSchemaException,
16
+ )
17
+ from mistral_common.protocol.instruct.messages import (
18
+ AssistantMessage,
19
+ ChatMessage,
20
+ Roles,
21
+ SystemMessage,
22
+ ToolMessage,
23
+ UserMessage,
24
+ )
25
+ from mistral_common.protocol.instruct.request import ChatCompletionRequest
26
+ from mistral_common.protocol.instruct.tool_calls import (
27
+ Function,
28
+ FunctionCall,
29
+ Tool,
30
+ ToolCall,
31
+ )
32
+
33
+
34
+ class ValidationMode(Enum):
35
+ serving = "serving"
36
+ test = "test"
37
+
38
+
39
+ class MistralRequestValidator:
40
+ def __init__(self, mode: ValidationMode = ValidationMode.test):
41
+ self._mode = mode
42
+
43
+ def validate_messages(self, messages: List[ChatMessage]) -> None:
44
+ """
45
+ Validates the list of messages
46
+ """
47
+ self._validate_message_list_structure(messages)
48
+ self._validate_message_list_content(messages)
49
+
50
+ def validate_request(self, request: ChatCompletionRequest) -> ChatCompletionRequest:
51
+ """
52
+ Validates the request
53
+ """
54
+
55
+ if self._mode == ValidationMode.serving:
56
+ if request.model is None:
57
+ raise InvalidRequestException("Model name parameter is required for serving mode")
58
+
59
+ # Validate the messages
60
+ self.validate_messages(request.messages)
61
+
62
+ # Validate the tools
63
+ self._validate_tools(request.tools)
64
+
65
+ return request
66
+
67
+ def _validate_function(self, function: Function) -> None:
68
+ """
69
+ Checks:
70
+ - That the function schema is valid
71
+ """
72
+ try:
73
+ Draft7Validator.check_schema(function.parameters)
74
+ except SchemaError as e:
75
+ raise InvalidToolSchemaException(f"Invalid tool schema: {e.message}")
76
+
77
+ if not re.match(r"^[a-zA-Z0-9_-]{1,64}$", function.name):
78
+ raise InvalidToolException(
79
+ f"Function name was {function.name} but must be a-z, A-Z, 0-9, "
80
+ "or contain underscores and dashes, with a maximum length of 64."
81
+ )
82
+
83
+ def _validate_tools(self, tools: List[Tool]) -> None:
84
+ """
85
+ Checks:
86
+ - That the tool schemas are valid
87
+ """
88
+
89
+ for tool in tools:
90
+ self._validate_function(tool.function)
91
+
92
+ def _validate_user_message(self, message: UserMessage) -> None:
93
+ pass
94
+
95
+ def _validate_tool_message(self, message: ToolMessage) -> None:
96
+ """
97
+ Checks:
98
+ - The tool name is valid
99
+ """
100
+ if message.name is not None:
101
+ if not re.match(r"^[a-zA-Z0-9_-]{1,64}$", message.name):
102
+ raise InvalidToolMessageException(
103
+ f"Function name was {message.name} but must be a-z, A-Z, 0-9, "
104
+ "or contain underscores and dashes, with a maximum length of 64."
105
+ )
106
+
107
+ def _validate_system_message(self, message: SystemMessage) -> None:
108
+ """
109
+ Checks:
110
+ - That the system prompt has content
111
+ """
112
+ if message.content is None:
113
+ raise InvalidSystemPromptException("System prompt must have content")
114
+
115
+ def _validate_function_call(self, function_call: FunctionCall) -> None:
116
+ """
117
+ Checks:
118
+ - That the function call has a valid name
119
+ """
120
+ if not re.match(r"^[a-zA-Z0-9_-]{1,64}$", function_call.name):
121
+ raise InvalidFunctionCallException(
122
+ f"Function name was {function_call.name} but must be a-z, A-Z, 0-9, "
123
+ "or contain underscores and dashes, with a maximum length of 64."
124
+ )
125
+
126
+ def _validate_tool_call(self, tool_call: ToolCall) -> None:
127
+ """
128
+ Checks:
129
+ - That the tool call has a valid function
130
+ """
131
+
132
+ self._validate_function_call(tool_call.function)
133
+
134
+ def _validate_assistant_message(self, message: AssistantMessage) -> None:
135
+ """
136
+ Checks:
137
+ - That the assistant message has either text or tool_calls, but not both
138
+ - That the tool calls are valid
139
+ """
140
+
141
+ # Validate that the message has either text or tool_calls, but not both
142
+ if (message.content is None) == (message.tool_calls is None):
143
+ raise InvalidAssistantMessageException("Assistant message must have either content or tool_calls")
144
+
145
+ # If we have tool calls, validate them
146
+ if message.tool_calls is not None:
147
+ # Validate that the tool calls are valid
148
+ for tool_call in message.tool_calls:
149
+ self._validate_tool_call(tool_call)
150
+
151
+ def _validate_tool_calls_followed_by_tool_messages(self, messages: List[ChatMessage]) -> None:
152
+ """
153
+ Checks:
154
+ - That the number of tool calls and tool messages are the same
155
+ - That the tool calls are followed by tool messages
156
+ """
157
+ prev_role = None
158
+ expected_tool_messages = 0
159
+ for message in messages:
160
+ if prev_role is None:
161
+ prev_role = message.role
162
+ continue
163
+
164
+ if message.role == Roles.tool:
165
+ expected_tool_messages -= 1
166
+ elif message.role == Roles.assistant:
167
+ # if we have an assistant message and we have not recieved all the function calls
168
+ # we need to raise an exception
169
+ if expected_tool_messages != 0:
170
+ raise InvalidMessageStructureException("Not the same number of function calls and responses")
171
+
172
+ if message.tool_calls is not None:
173
+ # Validate that the number of function calls and responses are the same
174
+ expected_tool_messages = len(message.tool_calls)
175
+
176
+ prev_role = message.role
177
+
178
+ if expected_tool_messages != 0:
179
+ raise InvalidMessageStructureException("Not the same number of function calls and responses")
180
+
181
+ def _validate_message_order(self, messages: List[ChatMessage]) -> None:
182
+ """
183
+ Validates the order of the messages, for example user -> assistant -> user -> assistant -> ...
184
+ """
185
+ previous_role = None
186
+ for message in messages:
187
+ current_role = message.role
188
+
189
+ if previous_role is not None:
190
+ if previous_role == Roles.system:
191
+ expected_roles = {Roles.user, Roles.assistant, Roles.system}
192
+ elif previous_role == Roles.user:
193
+ expected_roles = {Roles.assistant, Roles.system, Roles.user}
194
+ elif previous_role == Roles.assistant:
195
+ expected_roles = {Roles.user, Roles.tool}
196
+ elif previous_role == Roles.tool:
197
+ expected_roles = {Roles.assistant, Roles.tool}
198
+
199
+ if current_role not in expected_roles:
200
+ raise InvalidMessageStructureException(
201
+ f"Unexpected role '{current_role.value}' after role '{previous_role.value}'"
202
+ )
203
+
204
+ previous_role = current_role
205
+
206
+ def _validate_message_list_structure(self, messages: List[ChatMessage]) -> None:
207
+ """
208
+ Validates the structure of the list of messages
209
+
210
+ For example the messages must be in the correct order of user/assistant/tool
211
+ """
212
+
213
+ if len(messages) == 0:
214
+ raise InvalidMessageStructureException("Conversation must have at least one message")
215
+
216
+ # If we have one message it must be a user or a system message
217
+ if len(messages) == 1:
218
+ if messages[0].role not in {Roles.user, Roles.system}:
219
+ raise InvalidMessageStructureException("Conversation must start with a user message or system message")
220
+
221
+ else:
222
+ # The last message must be a user or tool message
223
+ last_message_role = messages[-1].role
224
+ expected_roles = {Roles.user, Roles.tool}
225
+
226
+ if last_message_role not in expected_roles:
227
+ expected_roles_str = ", ".join(role.value for role in expected_roles)
228
+ raise InvalidMessageStructureException(
229
+ f"Expected last role to be one of: [{expected_roles_str}] but got {last_message_role.value}"
230
+ )
231
+
232
+ self._validate_message_order(messages)
233
+ self._validate_tool_calls_followed_by_tool_messages(messages)
234
+
235
+ def _validate_message_list_content(self, messages: List[ChatMessage]) -> None:
236
+ """
237
+ Validates the content of the messages
238
+ """
239
+
240
+ for message in messages:
241
+ if message.role == Roles.user:
242
+ self._validate_user_message(message)
243
+ elif message.role == Roles.assistant:
244
+ self._validate_assistant_message(message)
245
+ elif message.role == Roles.tool:
246
+ self._validate_tool_message(message)
247
+ elif message.role == Roles.system:
248
+ self._validate_system_message(message)
249
+ else:
250
+ raise InvalidRequestException(f"Unsupported message type {type(message)}")
251
+
252
+
253
+ class MistralRequestValidatorV3(MistralRequestValidator):
254
+ def _validate_tool_message(self, message: ToolMessage) -> None:
255
+ """
256
+ Checks:
257
+ - The tool name is valid
258
+ """
259
+ if message.name is not None:
260
+ if not re.match(r"^[a-zA-Z0-9_-]{1,64}$", message.name):
261
+ raise InvalidToolMessageException(
262
+ f"Function name was {message.name} but must be a-z, A-Z, 0-9, "
263
+ "or contain underscores and dashes, with a maximum length of 64."
264
+ )
265
+
266
+ if message.tool_call_id is not None:
267
+ if not re.match(r"^[a-zA-Z0-9]{9}$", message.tool_call_id):
268
+ raise InvalidToolMessageException(
269
+ f"Tool call id was {message.tool_call_id} but must be a-z, A-Z, 0-9, with a length of 9."
270
+ )
271
+
272
+ def _validate_tool_call(self, tool_call: ToolCall) -> None:
273
+ """
274
+ Validate that the tool call has a valid ID
275
+ """
276
+
277
+ if not re.match(r"^[a-zA-Z0-9]{9}$", tool_call.id):
278
+ raise InvalidFunctionCallException(
279
+ f"Tool call id was {tool_call.id} but must be a-z, A-Z, 0-9, with a length of 9."
280
+ )
281
+
282
+ self._validate_function_call(tool_call.function)
@@ -0,0 +1,5 @@
1
+ import uuid
2
+
3
+
4
+ def random_uuid() -> str:
5
+ return str(uuid.uuid4().hex)
File without changes
File without changes