mmar-mapi 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.
Potentially problematic release.
This version of mmar-mapi might be problematic. Click here for more details.
- mmar_mapi/__init__.py +11 -0
- mmar_mapi/api.py +62 -0
- mmar_mapi/file_storage.py +113 -0
- mmar_mapi/models/__init__.py +0 -0
- mmar_mapi/models/base.py +15 -0
- mmar_mapi/models/base_config_models/__init__.py +1 -0
- mmar_mapi/models/base_config_models/gigachat_config.py +14 -0
- mmar_mapi/models/chat.py +399 -0
- mmar_mapi/models/chat_item.py +157 -0
- mmar_mapi/models/enums.py +40 -0
- mmar_mapi/models/tracks.py +14 -0
- mmar_mapi/models/widget.py +22 -0
- mmar_mapi/type_union.py +63 -0
- mmar_mapi/utils.py +5 -0
- mmar_mapi-1.0.0.dist-info/METADATA +28 -0
- mmar_mapi-1.0.0.dist-info/RECORD +18 -0
- mmar_mapi-1.0.0.dist-info/WHEEL +4 -0
- mmar_mapi-1.0.0.dist-info/licenses/LICENSE +21 -0
mmar_mapi/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
__version__ = "5.2.4"
|
|
2
|
+
|
|
3
|
+
from .file_storage import FileStorage, ResourceId
|
|
4
|
+
from .models.base import Base
|
|
5
|
+
from .models.base_config_models import GigaChatConfig
|
|
6
|
+
from .models.chat import Chat, Context, ChatMessage, AIMessage, HumanMessage, MiscMessage, make_content
|
|
7
|
+
from .models.chat_item import ChatItem, OuterContextItem, InnerContextItem, ReplicaItem
|
|
8
|
+
from .models.enums import MTRSLabelEnum, DiagnosticsXMLTagEnum, MTRSXMLTagEnum, DoctorChoiceXMLTagEnum
|
|
9
|
+
from .models.tracks import TrackInfo, DomainInfo
|
|
10
|
+
from .models.widget import Widget
|
|
11
|
+
from .utils import make_session_id
|
mmar_mapi/api.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from mmar_mapi.models.chat import Chat, ChatMessage
|
|
2
|
+
from mmar_mapi.models.tracks import DomainInfo, TrackInfo
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
Value = str
|
|
7
|
+
Interpretation = str
|
|
8
|
+
ResourceId = str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ChatManagerAPI:
|
|
12
|
+
def get_domains(self, *, client_id: str, language_code: str = "ru") -> list[DomainInfo]:
|
|
13
|
+
raise NotImplementedError
|
|
14
|
+
|
|
15
|
+
def get_tracks(self, *, client_id: str, language_code: str = "ru") -> list[TrackInfo]:
|
|
16
|
+
raise NotImplementedError
|
|
17
|
+
|
|
18
|
+
def get_response(self, *, chat: Chat) -> list[ChatMessage]:
|
|
19
|
+
raise NotImplementedError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TextGeneratorAPI:
|
|
23
|
+
def process(self, *, chat: Chat) -> str:
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ContentInterpreterRemoteResponse(BaseModel):
|
|
28
|
+
interpretation: str
|
|
29
|
+
resource_fname: str
|
|
30
|
+
resource: bytes
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ContentInterpreterRemoteAPI:
|
|
34
|
+
def interpret_remote(
|
|
35
|
+
self, *, kind: str, query: str, resource: bytes, chat: Chat | None = None
|
|
36
|
+
) -> ContentInterpreterRemoteResponse:
|
|
37
|
+
raise NotImplementedError
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ClassifierAPI:
|
|
41
|
+
def get_values(self) -> list[Value]:
|
|
42
|
+
raise NotImplementedError
|
|
43
|
+
|
|
44
|
+
def evaluate(self, *, chat: Chat) -> Value:
|
|
45
|
+
raise NotImplementedError
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class CriticAPI:
|
|
49
|
+
def evaluate(self, *, text: str, chat: Chat | None = None) -> float:
|
|
50
|
+
raise NotImplementedError
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ContentInterpreterAPI:
|
|
54
|
+
def interpret(
|
|
55
|
+
self, *, kind: str, query: str, resource_id: str = "", chat: Chat | None = None
|
|
56
|
+
) -> tuple[Interpretation, ResourceId | None]:
|
|
57
|
+
raise NotImplementedError
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TextProcessorAPI:
|
|
61
|
+
def process(self, *, text: str, chat: Chat | None = None) -> str:
|
|
62
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import string
|
|
2
|
+
from hashlib import md5
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from zipfile import ZipFile, is_zipfile
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
ResourceId = str
|
|
8
|
+
ASCII_DIGITS = set(string.ascii_lowercase + string.digits)
|
|
9
|
+
SUFFIX_DIR = ".dir"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _validate_exist(files_dir):
|
|
13
|
+
if not files_dir.exists():
|
|
14
|
+
err = f"Failed to access file-storage directory: {files_dir}"
|
|
15
|
+
raise OSError(err)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _validate_dtype(dtype: str):
|
|
19
|
+
if all(map(ASCII_DIGITS.__contains__, dtype)):
|
|
20
|
+
return
|
|
21
|
+
raise ValueError(f"Bad dtype: {dtype}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def generate_fname(content, dtype):
|
|
25
|
+
fname_hash = md5(content).hexdigest()
|
|
26
|
+
fname = f"{fname_hash}.{dtype}"
|
|
27
|
+
return fname
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FileStorage:
|
|
31
|
+
def __init__(self, files_dir):
|
|
32
|
+
self.files_dir = Path(files_dir)
|
|
33
|
+
self.files_dir.mkdir(exist_ok=True, parents=True)
|
|
34
|
+
_validate_exist(self.files_dir)
|
|
35
|
+
|
|
36
|
+
def _generate_fname_path(self, content: bytes, dtype: str):
|
|
37
|
+
fpath = self.files_dir / generate_fname(content, dtype)
|
|
38
|
+
return fpath
|
|
39
|
+
|
|
40
|
+
def upload_maybe(self, content: bytes | None, dtype: str) -> ResourceId | None:
|
|
41
|
+
if not content:
|
|
42
|
+
return None
|
|
43
|
+
resource_id = self.upload(content, dtype)
|
|
44
|
+
return resource_id
|
|
45
|
+
|
|
46
|
+
def upload(self, content: bytes | str, dtype: str) -> ResourceId:
|
|
47
|
+
_validate_dtype(dtype)
|
|
48
|
+
if isinstance(content, str):
|
|
49
|
+
content = content.encode()
|
|
50
|
+
fpath = self._generate_fname_path(content, dtype)
|
|
51
|
+
fpath.write_bytes(content)
|
|
52
|
+
return str(fpath)
|
|
53
|
+
|
|
54
|
+
def upload_dir(self, resource_ids: list[ResourceId]) -> ResourceId:
|
|
55
|
+
content = "\n".join(resource_ids)
|
|
56
|
+
res = self.upload(content, "dir")
|
|
57
|
+
return res
|
|
58
|
+
|
|
59
|
+
def download(self, resource_id: ResourceId) -> bytes:
|
|
60
|
+
return Path(resource_id).read_bytes()
|
|
61
|
+
|
|
62
|
+
def download_text(self, resource_id: ResourceId) -> str:
|
|
63
|
+
return Path(resource_id).read_text(encoding="utf-8")
|
|
64
|
+
|
|
65
|
+
def read_dir_or_none(self, resource_id: ResourceId) -> list[ResourceId] | None:
|
|
66
|
+
if not self.is_dir(resource_id):
|
|
67
|
+
return None
|
|
68
|
+
res = self.download_text(resource_id).split("\n")
|
|
69
|
+
return res
|
|
70
|
+
|
|
71
|
+
def _get_path(self, resource_id: ResourceId | None) -> Path | None:
|
|
72
|
+
if not resource_id:
|
|
73
|
+
return None
|
|
74
|
+
path = Path(resource_id)
|
|
75
|
+
return path if (path.exists() and path.is_file()) else None
|
|
76
|
+
|
|
77
|
+
def is_valid(self, resource_id: ResourceId | None) -> bool:
|
|
78
|
+
path = self._get_path(resource_id)
|
|
79
|
+
return path is not None
|
|
80
|
+
|
|
81
|
+
def is_file(self, resource_id: ResourceId | None) -> bool:
|
|
82
|
+
path = self._get_path(resource_id)
|
|
83
|
+
return path and path.suffix != SUFFIX_DIR
|
|
84
|
+
|
|
85
|
+
def is_dir(self, resource_id: ResourceId | None) -> bool:
|
|
86
|
+
path = self._get_path(resource_id)
|
|
87
|
+
return path and path.suffix == SUFFIX_DIR
|
|
88
|
+
|
|
89
|
+
def get_dtype(self, resource_id: ResourceId | None) -> str | None:
|
|
90
|
+
return resource_id and resource_id.rsplit(".")[-1]
|
|
91
|
+
|
|
92
|
+
def unzip_file(self, resource_id: str) -> ResourceId:
|
|
93
|
+
""" takes resource_id which refer to zip-archive, unpacks it and returns directory ResourceId with content of zip-archive """
|
|
94
|
+
path = self._get_path(resource_id)
|
|
95
|
+
if not path:
|
|
96
|
+
raise ValueError(f"Not found path: {resource_id}")
|
|
97
|
+
if not is_zipfile(resource_id):
|
|
98
|
+
raise ValueError(f"Expected zip archive but found: {resource_id}")
|
|
99
|
+
|
|
100
|
+
resource_ids = []
|
|
101
|
+
|
|
102
|
+
with ZipFile(path, mode='r') as zip_file:
|
|
103
|
+
files_info = zip_file.filelist
|
|
104
|
+
|
|
105
|
+
for file_info in zip_file.filelist:
|
|
106
|
+
file_dtype = file_info.filename.rsplit('.')[-1]
|
|
107
|
+
file_bytes = zip_file.read(file_info)
|
|
108
|
+
rid = self.upload(file_bytes, file_dtype)
|
|
109
|
+
resource_ids.append(rid)
|
|
110
|
+
|
|
111
|
+
res = self.upload_dir(resource_ids)
|
|
112
|
+
return res
|
|
113
|
+
|
|
File without changes
|
mmar_mapi/models/base.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from pydantic import ConfigDict, BaseModel, model_validator
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Base(BaseModel):
|
|
8
|
+
model_config = ConfigDict(populate_by_name=True, str_strip_whitespace=True)
|
|
9
|
+
|
|
10
|
+
@model_validator(mode="before")
|
|
11
|
+
@classmethod
|
|
12
|
+
def validate_to_json(cls, value: str | Any) -> Any:
|
|
13
|
+
if isinstance(value, str):
|
|
14
|
+
return cls(**json.loads(value))
|
|
15
|
+
return value
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .gigachat_config import GigaChatConfig
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from typing import Self
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, model_validator, SecretStr
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GigaChatConfig(BaseModel):
|
|
7
|
+
client_id: SecretStr = SecretStr("")
|
|
8
|
+
client_secret: SecretStr = SecretStr("")
|
|
9
|
+
|
|
10
|
+
@model_validator(mode="after")
|
|
11
|
+
def empty_validator(self) -> Self:
|
|
12
|
+
if not (self.client_id and self.client_secret):
|
|
13
|
+
raise ValueError("creds for gigachat is not filled")
|
|
14
|
+
return self
|
mmar_mapi/models/chat.py
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from copy import deepcopy
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any, Literal, TypeVar
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
from mmar_mapi.models.chat_item import ChatItem, ReplicaItem, OuterContextItem
|
|
8
|
+
from mmar_mapi.models.widget import Widget
|
|
9
|
+
from mmar_mapi.type_union import TypeUnion
|
|
10
|
+
from pydantic import Field, ValidationError, Extra
|
|
11
|
+
|
|
12
|
+
from .base import Base
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_DT_FORMAT: str = "%Y-%m-%d-%H-%M-%S"
|
|
16
|
+
_EXAMPLE_DT: str = datetime(year=1970, month=1, day=1).strftime(_DT_FORMAT)
|
|
17
|
+
StrDict = dict[str, Any]
|
|
18
|
+
ContentBase = str | Widget | StrDict
|
|
19
|
+
Content = ContentBase | list[ContentBase]
|
|
20
|
+
T = TypeVar("T")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def now_pretty() -> str:
|
|
24
|
+
return datetime.now().strftime(_DT_FORMAT)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Context(Base):
|
|
28
|
+
client_id: str = Field("", examples=["543216789"])
|
|
29
|
+
user_id: str = Field("", examples=["123456789"])
|
|
30
|
+
session_id: str = Field(default_factory=now_pretty, examples=["987654321"])
|
|
31
|
+
track_id: str = Field("", examples=["Hello"])
|
|
32
|
+
extra: StrDict | None = Field(None, examples=[None])
|
|
33
|
+
|
|
34
|
+
def create_id(self, short: bool = False) -> str:
|
|
35
|
+
uid, sid, cid = self.user_id, self.session_id, self.client_id
|
|
36
|
+
if short:
|
|
37
|
+
return f"{cid}_{uid}_{sid}"
|
|
38
|
+
return f"client_{cid}_user_{uid}_session_{sid}"
|
|
39
|
+
|
|
40
|
+
def _get_deprecated_extra(self, field, default):
|
|
41
|
+
# legacy: eliminate after migration
|
|
42
|
+
res = (self.extra or {}).get(field, default)
|
|
43
|
+
warnings.warn(f"Deprecated property `{field}`, should be eliminated", stacklevel=2)
|
|
44
|
+
return res
|
|
45
|
+
|
|
46
|
+
# fmt: off
|
|
47
|
+
@property
|
|
48
|
+
def sex(self) -> bool: return self._get_deprecated_extra('sex', True)
|
|
49
|
+
@property
|
|
50
|
+
def age(self) -> int: return self._get_deprecated_extra('age', 0)
|
|
51
|
+
@property
|
|
52
|
+
def entrypoint_key(self) -> str: return self._get_deprecated_extra('entrypoint_key', '')
|
|
53
|
+
@property
|
|
54
|
+
def language_code(self) -> str: return self._get_deprecated_extra('language_code', '')
|
|
55
|
+
@property
|
|
56
|
+
def parent_session_id(self) -> str: return self._get_deprecated_extra('parent_session_id', '')
|
|
57
|
+
# fmt: on
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _get_field(obj: dict, field, val_type: type[T]) -> T | None:
|
|
61
|
+
if not isinstance(obj, dict):
|
|
62
|
+
return None
|
|
63
|
+
val = obj.get(field)
|
|
64
|
+
if val is not None and isinstance(val, val_type):
|
|
65
|
+
return val
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _get_text(obj: Content) -> str:
|
|
70
|
+
if isinstance(obj, str):
|
|
71
|
+
return obj
|
|
72
|
+
if isinstance(obj, list):
|
|
73
|
+
return "".join(map(_get_text, obj))
|
|
74
|
+
if isinstance(obj, dict) and obj.get("type") == "text":
|
|
75
|
+
return _get_field(obj, "text", str) or ""
|
|
76
|
+
return ""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _modify_text(obj: Content, callback: Callable[[str], str | None]) -> str:
|
|
80
|
+
if isinstance(obj, str):
|
|
81
|
+
return callback(obj)
|
|
82
|
+
if isinstance(obj, list):
|
|
83
|
+
return [_modify_text(el, callback) for el in obj]
|
|
84
|
+
if isinstance(obj, dict) and obj.get("type") == "text":
|
|
85
|
+
text = _get_field(obj, "text", str) or ""
|
|
86
|
+
text_upd = callback(text)
|
|
87
|
+
return {"type": "text", "text": text_upd}
|
|
88
|
+
return obj
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _get_resource_id(obj: Content) -> str | None:
|
|
92
|
+
if isinstance(obj, list):
|
|
93
|
+
return next((el for el in map(_get_resource_id, obj) if el), None)
|
|
94
|
+
if isinstance(obj, dict) and obj.get("type") == "resource_id":
|
|
95
|
+
return _get_field(obj, "resource_id", str)
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _get_command(obj: Content) -> dict | None:
|
|
100
|
+
if isinstance(obj, list):
|
|
101
|
+
return next((el for el in map(_get_command, obj) if el), None)
|
|
102
|
+
if isinstance(obj, dict) and obj.get("type") == "command":
|
|
103
|
+
return _get_field(obj, "command", dict)
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _get_widget(obj: Content) -> Widget | None:
|
|
108
|
+
if isinstance(obj, list):
|
|
109
|
+
return next((el for el in map(_get_widget, obj) if el), None)
|
|
110
|
+
if isinstance(obj, Widget):
|
|
111
|
+
return obj
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# todo fix: generalize functions _get_field
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class BaseMessage(Base):
|
|
119
|
+
type: str
|
|
120
|
+
content: Content = Field("", examples=["Привет"])
|
|
121
|
+
date_time: str = Field(default_factory=now_pretty, examples=[_EXAMPLE_DT])
|
|
122
|
+
extra: StrDict | None = Field(None, examples=[None])
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def text(self) -> str:
|
|
126
|
+
return _get_text(self.content)
|
|
127
|
+
|
|
128
|
+
def modify_text(self, callback: Callable[[str], str]) -> "BaseMessage":
|
|
129
|
+
content_upd = _modify_text(self.content, callback)
|
|
130
|
+
return self.model_copy(update=dict(content=content_upd))
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def body(self) -> str:
|
|
134
|
+
# legacy: eliminate after migration
|
|
135
|
+
return self.text
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def resource_id(self) -> str | None:
|
|
139
|
+
return _get_resource_id(self.content)
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def command(self) -> dict | None:
|
|
143
|
+
return _get_command(self.content)
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def widget(self) -> Widget | None:
|
|
147
|
+
return _get_widget(self.content)
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def DATETIME_FORMAT() -> str:
|
|
151
|
+
return _DT_FORMAT
|
|
152
|
+
|
|
153
|
+
def with_now_datetime(self):
|
|
154
|
+
return self.model_copy(update=dict(date_time=now_pretty()))
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def is_ai(self):
|
|
158
|
+
return self.type == "ai"
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def is_human(self):
|
|
162
|
+
return self.type == "human"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class HumanMessage(BaseMessage):
|
|
166
|
+
type: Literal["human"] = "human"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class AIMessage(BaseMessage):
|
|
170
|
+
type: Literal["ai"] = "ai"
|
|
171
|
+
state: str = Field("", examples=["COLLECTION"])
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def action(self) -> str:
|
|
175
|
+
return (self.extra or {}).get("action", "")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class MiscMessage(BaseMessage):
|
|
179
|
+
type: Literal["misc"] = "misc"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
ChatMessage = TypeUnion[HumanMessage, AIMessage, MiscMessage]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class Chat(Base):
|
|
186
|
+
context: Context = Field(default_factory=Context)
|
|
187
|
+
messages: list[ChatMessage] = Field(default_factory=list)
|
|
188
|
+
|
|
189
|
+
class Config:
|
|
190
|
+
extra = Extra.ignore
|
|
191
|
+
|
|
192
|
+
def __init__(self, **data):
|
|
193
|
+
extra_fields = set(data.keys()) - set(self.__fields__.keys())
|
|
194
|
+
if extra_fields:
|
|
195
|
+
warnings.warn(f"Chat initialization: extra fields will be ignored: {extra_fields}")
|
|
196
|
+
super().__init__(**data)
|
|
197
|
+
|
|
198
|
+
def create_id(self, short: bool = False) -> str:
|
|
199
|
+
return self.context.create_id(short)
|
|
200
|
+
|
|
201
|
+
@staticmethod
|
|
202
|
+
def parse(chat_obj: str | dict | ChatItem) -> "Chat":
|
|
203
|
+
return _parse_chat_compat(chat_obj)
|
|
204
|
+
|
|
205
|
+
def to_chat_item(self) -> ChatItem:
|
|
206
|
+
return convert_chat_to_chat_item(self)
|
|
207
|
+
|
|
208
|
+
def add_message(self, message: ChatMessage):
|
|
209
|
+
self.messages.append(message)
|
|
210
|
+
|
|
211
|
+
def add_messages(self, messages: list[ChatMessage]):
|
|
212
|
+
for message in messages:
|
|
213
|
+
self.messages.append(message)
|
|
214
|
+
|
|
215
|
+
def replace_messages(self, messages: list[ChatMessage]):
|
|
216
|
+
return self.model_copy(update=dict(messages=messages))
|
|
217
|
+
|
|
218
|
+
def get_last_state(self, default: str = "empty") -> str:
|
|
219
|
+
for ii in range(len(self.messages) - 1, -1, -1):
|
|
220
|
+
message = self.messages[ii]
|
|
221
|
+
if isinstance(message, AIMessage):
|
|
222
|
+
return message.state
|
|
223
|
+
return default
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def make_content(
|
|
227
|
+
text: str | None = None,
|
|
228
|
+
*,
|
|
229
|
+
resource_id: str | None = None,
|
|
230
|
+
command: dict | None = None,
|
|
231
|
+
widget: Widget | None = None,
|
|
232
|
+
) -> Content:
|
|
233
|
+
resource_id = (resource_id or None) and {"type": "resource_id", "resource_id": resource_id}
|
|
234
|
+
command = (command or None) and {"type": "command", "command": command}
|
|
235
|
+
|
|
236
|
+
content = list(filter(None, [text, resource_id, command, widget]))
|
|
237
|
+
if len(content) == 0:
|
|
238
|
+
content = ""
|
|
239
|
+
elif len(content) == 1:
|
|
240
|
+
content = content[0]
|
|
241
|
+
|
|
242
|
+
return content
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def convert_replica_item_to_message(replica: ReplicaItem) -> ChatMessage:
|
|
246
|
+
date_time = replica.date_time
|
|
247
|
+
content = make_content(
|
|
248
|
+
text=replica.body,
|
|
249
|
+
resource_id=replica.resource_id,
|
|
250
|
+
command=replica.command,
|
|
251
|
+
widget=replica.widget,
|
|
252
|
+
)
|
|
253
|
+
# legacy: eliminate after migration
|
|
254
|
+
resource_id = (replica.resource_id or None) and {"type": "resource_id", "resource_id": replica.resource_id}
|
|
255
|
+
body = replica.body
|
|
256
|
+
command = (replica.command or None) and {"type": "command", "command": replica.command}
|
|
257
|
+
widget = replica.widget
|
|
258
|
+
date_time = replica.date_time
|
|
259
|
+
|
|
260
|
+
content = list(filter(None, [body, resource_id, command, widget]))
|
|
261
|
+
if len(content) == 0:
|
|
262
|
+
content = ""
|
|
263
|
+
elif len(content) == 1:
|
|
264
|
+
content = content[0]
|
|
265
|
+
|
|
266
|
+
is_bot_message = replica.role
|
|
267
|
+
|
|
268
|
+
if is_bot_message:
|
|
269
|
+
kwargs = dict(
|
|
270
|
+
content=content,
|
|
271
|
+
date_time=date_time,
|
|
272
|
+
state=replica.state,
|
|
273
|
+
extra=dict(
|
|
274
|
+
**(replica.extra or {}),
|
|
275
|
+
action=replica.action,
|
|
276
|
+
moderation=replica.moderation,
|
|
277
|
+
),
|
|
278
|
+
)
|
|
279
|
+
res = AIMessage(**kwargs)
|
|
280
|
+
else:
|
|
281
|
+
kwargs = dict(content=content, date_time=date_time)
|
|
282
|
+
res = HumanMessage(**kwargs)
|
|
283
|
+
return res
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def convert_outer_context_to_context(octx: OuterContextItem) -> Context:
|
|
287
|
+
# legacy: eliminate after migration
|
|
288
|
+
context = Context(
|
|
289
|
+
client_id=octx.client_id,
|
|
290
|
+
user_id=octx.user_id,
|
|
291
|
+
session_id=octx.session_id,
|
|
292
|
+
track_id=octx.track_id,
|
|
293
|
+
extra=dict(
|
|
294
|
+
sex=octx.sex,
|
|
295
|
+
age=octx.age,
|
|
296
|
+
parent_session_id=octx.parent_session_id,
|
|
297
|
+
entrypoint_key=octx.entrypoint_key,
|
|
298
|
+
language_code=octx.language_code,
|
|
299
|
+
),
|
|
300
|
+
)
|
|
301
|
+
return context
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def convert_chat_item_to_chat(chat_item: ChatItem) -> Chat:
|
|
305
|
+
# legacy: eliminate after migration
|
|
306
|
+
context = convert_outer_context_to_context(chat_item.outer_context)
|
|
307
|
+
messages = list(map(convert_replica_item_to_message, chat_item.inner_context.replicas))
|
|
308
|
+
res = Chat(context=context, messages=messages)
|
|
309
|
+
return res
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def convert_context_to_outer_context(context: Context) -> OuterContextItem:
|
|
313
|
+
# legacy: eliminate after migration
|
|
314
|
+
extra = context.extra or {}
|
|
315
|
+
return OuterContextItem(
|
|
316
|
+
client_id=context.client_id,
|
|
317
|
+
user_id=context.user_id,
|
|
318
|
+
session_id=context.session_id,
|
|
319
|
+
track_id=context.track_id,
|
|
320
|
+
sex=extra.get("sex"),
|
|
321
|
+
age=extra.get("age"),
|
|
322
|
+
parent_session_id=extra.get("parent_session_id"),
|
|
323
|
+
entrypoint_key=extra.get("entrypoint_key"),
|
|
324
|
+
language_code=extra.get("language_code"),
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def convert_message_to_replica_item(message: ChatMessage) -> ReplicaItem | None:
|
|
329
|
+
# legacy: eliminate after migration
|
|
330
|
+
m_type = message.type
|
|
331
|
+
if m_type in {"ai", "human"}:
|
|
332
|
+
role = m_type == "ai"
|
|
333
|
+
else:
|
|
334
|
+
return None
|
|
335
|
+
|
|
336
|
+
extra = deepcopy(message.extra) if message.extra else {}
|
|
337
|
+
action = extra.pop("action", "")
|
|
338
|
+
moderation = extra.pop("moderation", "OK")
|
|
339
|
+
|
|
340
|
+
kwargs = dict(
|
|
341
|
+
role=role,
|
|
342
|
+
body=message.text,
|
|
343
|
+
resource_id=message.resource_id,
|
|
344
|
+
command=message.command,
|
|
345
|
+
widget=message.widget,
|
|
346
|
+
date_time=message.date_time,
|
|
347
|
+
extra=extra or None,
|
|
348
|
+
state=getattr(message, "state", ""),
|
|
349
|
+
action=action,
|
|
350
|
+
moderation=moderation,
|
|
351
|
+
)
|
|
352
|
+
return ReplicaItem(**kwargs)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def convert_chat_to_chat_item(chat: Chat) -> ChatItem:
|
|
356
|
+
# legacy: eliminate after migration
|
|
357
|
+
return ChatItem(
|
|
358
|
+
outer_context=convert_context_to_outer_context(chat.context),
|
|
359
|
+
inner_context=dict(replicas=list(map(convert_message_to_replica_item, chat.messages))),
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def parse_chat_item_as_chat(chat_obj: str | dict | ChatItem) -> Chat:
|
|
364
|
+
# legacy: eliminate after migration
|
|
365
|
+
if isinstance(chat_obj, ChatItem):
|
|
366
|
+
chat_item = chat_obj
|
|
367
|
+
else:
|
|
368
|
+
chat_item = ChatItem.parse(chat_obj)
|
|
369
|
+
res = convert_chat_item_to_chat(chat_item)
|
|
370
|
+
return res
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _parse_chat(chat_obj: str | dict) -> Chat:
|
|
374
|
+
if isinstance(chat_obj, dict):
|
|
375
|
+
return Chat.model_validate(chat_obj)
|
|
376
|
+
|
|
377
|
+
return Chat.model_validate_json(chat_obj)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def is_chat_item(chat_obj: str | dict | ChatItem) -> bool:
|
|
381
|
+
if isinstance(chat_obj, ChatItem):
|
|
382
|
+
return True
|
|
383
|
+
if isinstance(chat_obj, dict):
|
|
384
|
+
return "OuterContext" in chat_obj
|
|
385
|
+
if isinstance(chat_obj, str):
|
|
386
|
+
return "OuterContext" in chat_obj
|
|
387
|
+
warnings.warn(f"Unexpected chat object: {chat_obj} :: {type(chat_obj)}")
|
|
388
|
+
return False
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _parse_chat_compat(chat_obj: str | dict | ChatItem) -> Chat:
|
|
392
|
+
# legacy: eliminate after migration
|
|
393
|
+
if is_chat_item(chat_obj):
|
|
394
|
+
return parse_chat_item_as_chat(chat_obj)
|
|
395
|
+
try:
|
|
396
|
+
return _parse_chat(chat_obj)
|
|
397
|
+
except ValidationError as ex:
|
|
398
|
+
warnings.warn(f"Failed to parse chat: {ex}")
|
|
399
|
+
return parse_chat_item_as_chat(chat_obj)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Annotated, Any
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
from mmar_mapi.models.widget import Widget
|
|
6
|
+
from pydantic import Field, ConfigDict, BeforeValidator, AfterValidator
|
|
7
|
+
|
|
8
|
+
from .base import Base
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_DT_FORMAT: str = "%Y-%m-%d-%H-%M-%S"
|
|
12
|
+
_EXAMPLE_DT_0 = datetime(1970, 1, 1, 0, 0, 0)
|
|
13
|
+
_EXAMPLE_DT: str = _EXAMPLE_DT_0.strftime(_DT_FORMAT)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def now_pretty() -> str:
|
|
17
|
+
return datetime.now().strftime(ReplicaItem.DATETIME_FORMAT())
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OuterContextItem(Base):
|
|
21
|
+
# remove annoying warning for protected `model_` namespace
|
|
22
|
+
model_config = ConfigDict(protected_namespaces=())
|
|
23
|
+
|
|
24
|
+
sex: bool = Field(False, alias="Sex", description="True = male, False = female", examples=[True])
|
|
25
|
+
age: int = Field(0, alias="Age", examples=[20])
|
|
26
|
+
user_id: str = Field("", alias="UserId", examples=["123456789"])
|
|
27
|
+
parent_session_id: str | None = Field(None, alias="ParentSessionId", examples=["987654320"])
|
|
28
|
+
session_id: str = Field("", alias="SessionId", examples=["987654321"])
|
|
29
|
+
client_id: str = Field("", alias="ClientId", examples=["543216789"])
|
|
30
|
+
track_id: str = Field(default="Consultation", alias="TrackId")
|
|
31
|
+
entrypoint_key: str = Field("", alias="EntrypointKey", examples=["giga"])
|
|
32
|
+
language_code: str = Field("ru", alias="LanguageCode", examples=["ru"])
|
|
33
|
+
|
|
34
|
+
def create_id(self, short: bool = False) -> str:
|
|
35
|
+
uid, sid, cid = self.user_id, self.session_id, self.client_id
|
|
36
|
+
if short:
|
|
37
|
+
return f"{uid}_{sid}_{cid}"
|
|
38
|
+
return f"user_{uid}_session_{sid}_client_{cid}"
|
|
39
|
+
|
|
40
|
+
def to_dict(self) -> dict[str, Any]:
|
|
41
|
+
return self.model_dump(by_alias=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
LABELS = {
|
|
45
|
+
0: "OK",
|
|
46
|
+
1: "NON_MED",
|
|
47
|
+
2: "CHILD",
|
|
48
|
+
3: "ABSURD",
|
|
49
|
+
4: "GREETING",
|
|
50
|
+
5: "RECEIPT",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def fix_deprecated_moderation(moderation):
|
|
55
|
+
if isinstance(moderation, int):
|
|
56
|
+
return LABELS.get(moderation, "OK")
|
|
57
|
+
elif isinstance(moderation, str):
|
|
58
|
+
return moderation
|
|
59
|
+
else:
|
|
60
|
+
raise ValueError(f"Unsupported moderation: {moderation} :: {type(moderation)}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def nullify_empty(text: str) -> str | None:
|
|
64
|
+
return text or None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ReplicaItem(Base):
|
|
68
|
+
body: str = Field("", alias="Body", examples=["Привет"])
|
|
69
|
+
resource_id: Annotated[str | None, AfterValidator(nullify_empty)] = Field(
|
|
70
|
+
None, alias="ResourceId", examples=["<link-id>"]
|
|
71
|
+
)
|
|
72
|
+
widget: Widget | None = Field(None, alias="Widget", examples=[None])
|
|
73
|
+
command: dict | None = Field(None, alias="Command", examples=[None])
|
|
74
|
+
role: bool = Field(False, alias="Role", description="True = ai, False = client", examples=[False])
|
|
75
|
+
date_time: str = Field(
|
|
76
|
+
default_factory=now_pretty, alias="DateTime", examples=[_EXAMPLE_DT], description=f"Format: {_DT_FORMAT}"
|
|
77
|
+
)
|
|
78
|
+
state: str = Field("", alias="State", description="chat manager fsm state", examples=["COLLECTION"])
|
|
79
|
+
action: str = Field("", alias="Action", description="chat manager fsm action", examples=["DIAGNOSIS"])
|
|
80
|
+
# todo fix: support loading from `moderation: int`
|
|
81
|
+
moderation: Annotated[str, BeforeValidator(str)] = Field(
|
|
82
|
+
"OK", alias="Moderation", description="moderation outcome", examples=["OK"]
|
|
83
|
+
)
|
|
84
|
+
extra: dict | None = Field(None, alias="Extra", examples=[None])
|
|
85
|
+
|
|
86
|
+
def to_dict(self) -> dict[str, Any]:
|
|
87
|
+
return self.model_dump(by_alias=True)
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def DATETIME_FORMAT() -> str:
|
|
91
|
+
return _DT_FORMAT
|
|
92
|
+
|
|
93
|
+
def with_now_datetime(self):
|
|
94
|
+
return self.model_copy(update=dict(date_time=now_pretty()))
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def is_ai(self):
|
|
98
|
+
return self.role
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def is_human(self):
|
|
102
|
+
return not self.role
|
|
103
|
+
|
|
104
|
+
def modify_text(self, callback: Callable[[str], str]) -> "ReplicaItem":
|
|
105
|
+
body_upd = callback(self.body)
|
|
106
|
+
return self.model_copy(update=dict(body=body_upd))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class InnerContextItem(Base):
|
|
110
|
+
replicas: list[ReplicaItem] = Field(alias="Replicas")
|
|
111
|
+
attrs: dict[str, str | int] | None = Field(default={}, alias="Attrs")
|
|
112
|
+
|
|
113
|
+
def to_dict(self) -> dict[str, list]:
|
|
114
|
+
return self.model_dump(by_alias=True)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class ChatItem(Base):
|
|
118
|
+
outer_context: OuterContextItem = Field(alias="OuterContext")
|
|
119
|
+
inner_context: InnerContextItem = Field(alias="InnerContext")
|
|
120
|
+
|
|
121
|
+
def create_id(self, short: bool = False) -> str:
|
|
122
|
+
return self.outer_context.create_id(short)
|
|
123
|
+
|
|
124
|
+
def to_dict(self) -> dict[str, Any]:
|
|
125
|
+
return self.model_dump(by_alias=True)
|
|
126
|
+
|
|
127
|
+
def add_replica(self, replica: ReplicaItem):
|
|
128
|
+
self.inner_context.replicas.append(replica)
|
|
129
|
+
|
|
130
|
+
def add_replicas(self, replicas: list[ReplicaItem]):
|
|
131
|
+
for replica in replicas:
|
|
132
|
+
self.inner_context.replicas.append(replica)
|
|
133
|
+
|
|
134
|
+
def replace_replicas(self, replicas: list[ReplicaItem]):
|
|
135
|
+
return self.model_copy(update=dict(inner_context=InnerContextItem(replicas=replicas)))
|
|
136
|
+
|
|
137
|
+
def get_last_state(self, default: str = "empty") -> str:
|
|
138
|
+
replicas = self.inner_context.replicas
|
|
139
|
+
for ii in range(len(replicas) - 1, -1, -1):
|
|
140
|
+
replica = replicas[ii]
|
|
141
|
+
if replica.role:
|
|
142
|
+
return replica.state
|
|
143
|
+
return default
|
|
144
|
+
|
|
145
|
+
def zip_history(self, field: str) -> list[Any]:
|
|
146
|
+
return [replica.to_dict().get(field, None) for replica in self.inner_context.replicas]
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def parse(cls, chat_obj: str | dict) -> "ChatItem":
|
|
150
|
+
return _parse_chat_item(chat_obj)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _parse_chat_item(chat_obj: str | dict) -> ChatItem:
|
|
154
|
+
if isinstance(chat_obj, dict):
|
|
155
|
+
return ChatItem.model_validate(chat_obj)
|
|
156
|
+
|
|
157
|
+
return ChatItem.model_validate_json(chat_obj)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from enum import StrEnum, auto
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DoctorChoiceXMLTagEnum(StrEnum):
|
|
5
|
+
@staticmethod
|
|
6
|
+
def _generate_next_value_(name: str, start: int, count: int, last_values: list[str]) -> str:
|
|
7
|
+
return f"{name.lower()}"
|
|
8
|
+
|
|
9
|
+
DIAGNOSTICS = auto()
|
|
10
|
+
SUMMARIZATION = auto()
|
|
11
|
+
MTRS = auto()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MTRSXMLTagEnum(StrEnum):
|
|
15
|
+
@staticmethod
|
|
16
|
+
def _generate_next_value_(name: str, start: int, count: int, last_values: list[str]) -> str:
|
|
17
|
+
return f"mtrs_{name.lower()}"
|
|
18
|
+
|
|
19
|
+
NAME = auto()
|
|
20
|
+
LABEL = auto()
|
|
21
|
+
DESC = auto()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MTRSLabelEnum(StrEnum):
|
|
25
|
+
@staticmethod
|
|
26
|
+
def _generate_next_value_(name: str, start: int, count: int, last_values: list[str]) -> str:
|
|
27
|
+
return name.upper()
|
|
28
|
+
|
|
29
|
+
LABORATORY = auto()
|
|
30
|
+
INSTRUMENTAL = auto()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DiagnosticsXMLTagEnum(StrEnum):
|
|
34
|
+
@staticmethod
|
|
35
|
+
def _generate_next_value_(name: str, start: int, count: int, last_values: list[str]) -> str:
|
|
36
|
+
return f"diag_{name.lower()}"
|
|
37
|
+
|
|
38
|
+
DIAG = auto()
|
|
39
|
+
DOC = auto()
|
|
40
|
+
DESC = auto()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from pydantic import Field
|
|
2
|
+
|
|
3
|
+
from .base import Base
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TrackInfo(Base):
|
|
7
|
+
track_id: str = Field(alias="TrackId")
|
|
8
|
+
name: str = Field(alias="Name")
|
|
9
|
+
domain_id: str = Field(alias="DomainId")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DomainInfo(Base):
|
|
13
|
+
domain_id: str = Field(alias="DomainId")
|
|
14
|
+
name: str = Field(alias="Name")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import Self, Literal
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, model_validator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Widget(BaseModel):
|
|
7
|
+
type: Literal["widget"] = "widget"
|
|
8
|
+
buttons: list[list[str]] | None = None
|
|
9
|
+
ibuttons: list[list[str]] | None = None
|
|
10
|
+
|
|
11
|
+
@model_validator(mode="after")
|
|
12
|
+
def check(self) -> Self:
|
|
13
|
+
if not self.buttons and not self.ibuttons:
|
|
14
|
+
raise ValueError("Empty widget is not allowed!")
|
|
15
|
+
if not self.ibuttons:
|
|
16
|
+
return self
|
|
17
|
+
for row in self.ibuttons:
|
|
18
|
+
for btn in row:
|
|
19
|
+
if ":" in btn:
|
|
20
|
+
continue
|
|
21
|
+
raise ValueError(f"Expected buttons like `<callback>:<caption>`, found: {btn}")
|
|
22
|
+
return self
|
mmar_mapi/type_union.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from typing import Any, Union, Annotated, get_args, get_origin, Literal
|
|
2
|
+
|
|
3
|
+
from pydantic import Discriminator, BaseModel, Tag
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class _TypeUnionMeta(type):
|
|
7
|
+
def __getitem__(cls, types):
|
|
8
|
+
if not isinstance(types, tuple):
|
|
9
|
+
types = (types,)
|
|
10
|
+
|
|
11
|
+
for tp in types:
|
|
12
|
+
if not isinstance(tp, type) or not issubclass(tp, BaseModel):
|
|
13
|
+
raise ValueError(f"Type {tp} must derived from BaseModel")
|
|
14
|
+
|
|
15
|
+
# Create tagged union
|
|
16
|
+
tagged_types = []
|
|
17
|
+
type_map = {}
|
|
18
|
+
type_names = set()
|
|
19
|
+
for t in types:
|
|
20
|
+
type_annot = t.__annotations__.get("type")
|
|
21
|
+
if not type_annot or get_origin(type_annot) != Literal:
|
|
22
|
+
raise ValueError(f"Type {t} must have a 'type' Literal[..] field for discrimination")
|
|
23
|
+
type_name = get_args(type_annot)[0]
|
|
24
|
+
type_map[type_name] = t
|
|
25
|
+
tagged_types.append(Annotated[t, Tag(type_name)])
|
|
26
|
+
type_names.add(type_name)
|
|
27
|
+
|
|
28
|
+
# Create the union type
|
|
29
|
+
union_type = Union[tuple(tagged_types)]
|
|
30
|
+
|
|
31
|
+
# Create discriminator function
|
|
32
|
+
def type_discriminator(v: Any) -> str:
|
|
33
|
+
if isinstance(v, types):
|
|
34
|
+
return v.type
|
|
35
|
+
if isinstance(v, dict):
|
|
36
|
+
tp = v.get("type")
|
|
37
|
+
return tp if tp in type_names else None
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
# Add discriminator
|
|
41
|
+
return Annotated[union_type, Discriminator(type_discriminator)]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TypeUnion(metaclass=_TypeUnionMeta):
|
|
45
|
+
"""
|
|
46
|
+
Wrapper around Union which derive type from field 'type' for effective deserialization via TypeAdapter.
|
|
47
|
+
|
|
48
|
+
Usage example:
|
|
49
|
+
```
|
|
50
|
+
class BaseFruit(BaseModel):
|
|
51
|
+
type: str
|
|
52
|
+
|
|
53
|
+
class Apple(BaseFruit):
|
|
54
|
+
type: Literal['apple'] = 'apple'
|
|
55
|
+
|
|
56
|
+
class Orange(BaseFruit):
|
|
57
|
+
type: Literal['orange'] = 'orange'
|
|
58
|
+
|
|
59
|
+
Fruit = TypeUnion[Apple, Orange]
|
|
60
|
+
```
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
pass
|
mmar_mapi/utils.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mmar-mapi
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Common pure/IO utilities for multi-modal architectures team
|
|
5
|
+
Keywords:
|
|
6
|
+
Author: Eugene Tagin
|
|
7
|
+
Author-email: Eugene Tagin <tagin@airi.net>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Documentation
|
|
19
|
+
Classifier: Topic :: Software Development
|
|
20
|
+
Classifier: Topic :: Utilities
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Dist: pydantic>=2.9.2,<=3.0.0
|
|
23
|
+
Requires-Python: >=3.12
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# mmar-mapi
|
|
27
|
+
|
|
28
|
+
Multimodal architectures Maestro API
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
mmar_mapi/__init__.py,sha256=EUZFZGPHZyrlXo9jVyfk0LRoUm8saSMbwlJH0r1cevI,570
|
|
2
|
+
mmar_mapi/api.py,sha256=C9Sr8dISvf51xfEznPjccI_odaG4coQE3HI_0jVpjMQ,1677
|
|
3
|
+
mmar_mapi/file_storage.py,sha256=bwfpoYivOu6uXV5rg2OZQuk8MOpxbT5Zp4TaYD4XjdY,3841
|
|
4
|
+
mmar_mapi/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
mmar_mapi/models/base.py,sha256=mKtXV2x51XVj7W-et9tjGcPMDUUUMelW-BywMgFc2p0,411
|
|
6
|
+
mmar_mapi/models/base_config_models/__init__.py,sha256=KjS_bSCka8BOMsigwcIML-e6eNB2ouMU6gxlhRmzeuY,44
|
|
7
|
+
mmar_mapi/models/base_config_models/gigachat_config.py,sha256=QvKTnp0VioXzd3_PUgNBeYx5RDTZT-45j-Ll-wXEI_o,421
|
|
8
|
+
mmar_mapi/models/chat.py,sha256=KisHhE-wRU2ulwmizqkfUojqe9kWB2LMwgwRjvIZ7-M,12358
|
|
9
|
+
mmar_mapi/models/chat_item.py,sha256=ZfCKvTqr7gpuJSAuHVxWRnlTefRwki_IVNA2N_CXGdg,5557
|
|
10
|
+
mmar_mapi/models/enums.py,sha256=J-GNpql9MCnKnWiV9aJRQGI-pAybvV86923RZs99grA,1006
|
|
11
|
+
mmar_mapi/models/tracks.py,sha256=HKDp-BX1p7AlDfSEKfOKCu0TRSK9cD4Dmq1vJt8oRjw,307
|
|
12
|
+
mmar_mapi/models/widget.py,sha256=KXSP3d4yQNeificMfzRxROdByw9IkxwCjkDD0kc7tcQ,704
|
|
13
|
+
mmar_mapi/type_union.py,sha256=diwmzcnbqkpGFckPHNw9o8zyQ955mOGNvhTlcBJ0RMI,1905
|
|
14
|
+
mmar_mapi/utils.py,sha256=hcKJVslvTBLw2vjZ9zcKZxh_tqk48obHcVs_i3Rxn3M,112
|
|
15
|
+
mmar_mapi-1.0.0.dist-info/licenses/LICENSE,sha256=2A90w8WjhOgQXnFuUijKJYazaqZ4_NTokYb9Po4y-9k,1061
|
|
16
|
+
mmar_mapi-1.0.0.dist-info/WHEEL,sha256=NHRAbdxxzyL9K3IO2LjmlNqKSyPZnKv2BD16YYVKo18,79
|
|
17
|
+
mmar_mapi-1.0.0.dist-info/METADATA,sha256=e27-V4h-lNzeHaKOU8txiBFEkdu0snxwY0DBHpOcaek,921
|
|
18
|
+
mmar_mapi-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 AIRI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|