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.
- mistral_common/__init__.py +0 -0
- mistral_common/base.py +9 -0
- mistral_common/data/mistral_instruct_tokenizer_240216.model.v2 +0 -0
- mistral_common/data/mistral_instruct_tokenizer_240216.model.v3 +0 -0
- mistral_common/data/tokenizer.model.v1 +0 -0
- mistral_common/exceptions.py +67 -0
- mistral_common/protocol/__init__.py +0 -0
- mistral_common/protocol/base.py +9 -0
- mistral_common/protocol/embedding/request.py +10 -0
- mistral_common/protocol/embedding/response.py +20 -0
- mistral_common/protocol/instruct/__init__.py +0 -0
- mistral_common/protocol/instruct/messages.py +56 -0
- mistral_common/protocol/instruct/request.py +29 -0
- mistral_common/protocol/instruct/response.py +66 -0
- mistral_common/protocol/instruct/tool_calls.py +36 -0
- mistral_common/protocol/instruct/validator.py +282 -0
- mistral_common/protocol/utils.py +5 -0
- mistral_common/py.typed +0 -0
- mistral_common/tokens/__init__.py +0 -0
- mistral_common/tokens/instruct/normalize.py +141 -0
- mistral_common/tokens/instruct/request.py +15 -0
- mistral_common/tokens/tokenizers/base.py +61 -0
- mistral_common/tokens/tokenizers/mistral.py +117 -0
- mistral_common/tokens/tokenizers/sentencepiece.py +334 -0
- mistral_common/tokens/tokenizers/utils.py +6 -0
- mistral_common-1.0.0.dist-info/METADATA +110 -0
- mistral_common-1.0.0.dist-info/RECORD +28 -0
- mistral_common-1.0.0.dist-info/WHEEL +4 -0
|
File without changes
|
mistral_common/base.py
ADDED
|
Binary file
|
|
Binary file
|
|
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,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)
|
mistral_common/py.typed
ADDED
|
File without changes
|
|
File without changes
|