agi-med-common 5.0.22__py3-none-any.whl → 5.0.24__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.
@@ -1,4 +1,4 @@
1
- __version__ = "5.0.22"
1
+ __version__ = "5.0.24"
2
2
 
3
3
  from .models import (
4
4
  MTRSLabelEnum,
@@ -1,11 +1,13 @@
1
+ import warnings
2
+ from copy import deepcopy
1
3
  from datetime import datetime
2
- from typing import Any, List, Dict, Literal
4
+ from typing import Any, List, Dict, Literal, TypeVar
3
5
 
4
6
  from agi_med_common.models.chat_item import ChatItem, ReplicaItem, OuterContextItem
5
7
  from agi_med_common.models.widget import Widget
6
8
  from agi_med_common.type_union import TypeUnion
7
9
  from agi_med_common.utils import first_nonnull
8
- from pydantic import Field
10
+ from pydantic import Field, ValidationError
9
11
 
10
12
  from ._base import _Base
11
13
 
@@ -15,6 +17,7 @@ _EXAMPLE_DT: str = datetime(year=1970, month=1, day=1).strftime(_DT_FORMAT)
15
17
  StrDict = Dict[str, Any]
16
18
  ContentBase = str | Widget | StrDict
17
19
  Content = ContentBase | List[ContentBase]
20
+ T = TypeVar('T')
18
21
 
19
22
 
20
23
  def now_pretty() -> str:
@@ -34,13 +37,32 @@ class Context(_Base):
34
37
  return f"{cid}_{uid}_{sid}"
35
38
  return f"client_{cid}_user_{uid}_session_{sid}"
36
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
37
45
 
38
- def _get_str_field(obj: dict, field) -> str | None:
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:
39
61
  if not isinstance(obj, dict):
40
62
  return None
41
- text = obj.get(field)
42
- if text is not None and isinstance(text, str):
43
- return text
63
+ val = obj.get(field)
64
+ if val is not None and isinstance(val, val_type):
65
+ return val
44
66
  return None
45
67
 
46
68
 
@@ -50,7 +72,7 @@ def _get_text(obj: Content) -> str:
50
72
  if isinstance(obj, list):
51
73
  return "".join(map(_get_text, obj))
52
74
  if isinstance(obj, dict) and obj.get("type") == "text":
53
- return _get_str_field(obj, "text") or ""
75
+ return _get_field(obj, "text", str) or ""
54
76
  return ""
55
77
 
56
78
 
@@ -58,7 +80,7 @@ def _get_resource_id(obj: Content) -> str | None:
58
80
  if isinstance(obj, list):
59
81
  return first_nonnull(map(_get_resource_id, obj))
60
82
  if isinstance(obj, dict) and obj.get("type") == "resource_id":
61
- return _get_str_field(obj, "resource_id")
83
+ return _get_field(obj, "resource_id", str)
62
84
  return None
63
85
 
64
86
 
@@ -66,7 +88,7 @@ def _get_command(obj: Content) -> dict | None:
66
88
  if isinstance(obj, list):
67
89
  return first_nonnull(map(_get_command, obj))
68
90
  if isinstance(obj, dict) and obj.get("type") == "command":
69
- return _get_str_field(obj, "command")
91
+ return _get_field(obj, "command", dict)
70
92
  return None
71
93
 
72
94
 
@@ -91,6 +113,11 @@ class BaseMessage(_Base):
91
113
  def text(self) -> str:
92
114
  return _get_text(self.content)
93
115
 
116
+ @property
117
+ def body(self) -> str:
118
+ # legacy: eliminate after migration
119
+ return self.text
120
+
94
121
  @property
95
122
  def resource_id(self) -> str | None:
96
123
  return _get_resource_id(self.content)
@@ -129,21 +156,26 @@ ChatMessage = TypeUnion[HumanMessage, AIMessage, MiscMessage]
129
156
 
130
157
  class Chat(_Base):
131
158
  context: Context
132
- messages: List[ChatMessage] = []
159
+ messages: List[ChatMessage] = Field(default_factory=list)
133
160
 
134
161
  def create_id(self, short: bool = False) -> str:
135
162
  return self.context.create_id(short)
136
163
 
137
- @classmethod
138
- def parse(self, chat_obj: str | dict) -> "Chat":
164
+ @staticmethod
165
+ def parse(chat_obj: str | dict | ChatItem) -> "Chat":
139
166
  return _parse_chat_compat(chat_obj)
140
167
 
168
+ def to_chat_item(self) -> ChatItem:
169
+ return convert_chat_to_chat_item(self)
170
+
141
171
 
142
172
  def convert_replica_item_to_message(replica: ReplicaItem) -> ChatMessage:
173
+ # legacy: eliminate after migration
143
174
  resource_id = (replica.resource_id or None) and {"type": "resource_id", "resource_id": replica.resource_id}
144
175
  body = replica.body
145
176
  command = replica.command
146
177
  widget = replica.widget
178
+ date_time = replica.date_time
147
179
 
148
180
  content = list(filter(None, [body, resource_id, command, widget]))
149
181
  if len(content) == 0:
@@ -151,25 +183,28 @@ def convert_replica_item_to_message(replica: ReplicaItem) -> ChatMessage:
151
183
  elif len(content) == 1:
152
184
  content = content[0]
153
185
 
154
- kwargs = dict(
155
- content=content,
156
- date_time=replica.date_time,
157
- extra=replica.extra,
158
- )
159
- if not replica.role:
160
- return HumanMessage(**kwargs)
161
- return AIMessage(
162
- **kwargs,
163
- state=replica.state,
164
- extra=dict(
165
- action=replica.action,
166
- moderation=replica.moderation,
167
- ),
168
- )
186
+ is_bot_message = replica.role
187
+
188
+ if is_bot_message:
189
+ kwargs = dict(
190
+ content=content,
191
+ date_time=date_time,
192
+ state=replica.state,
193
+ extra=dict(
194
+ **(replica.extra or {}),
195
+ action=replica.action,
196
+ moderation=replica.moderation,
197
+ ),
198
+ )
199
+ res = AIMessage(**kwargs)
200
+ else:
201
+ kwargs = dict(content=content, date_time=date_time)
202
+ res = HumanMessage(**kwargs)
203
+ return res
169
204
 
170
205
 
171
206
  def convert_outer_context_to_context(octx: OuterContextItem) -> Context:
172
- # legacy: eliminate
207
+ # legacy: eliminate after migration
173
208
  context = Context(
174
209
  client_id=octx.client_id,
175
210
  user_id=octx.user_id,
@@ -187,15 +222,15 @@ def convert_outer_context_to_context(octx: OuterContextItem) -> Context:
187
222
 
188
223
 
189
224
  def convert_chat_item_to_chat(chat_item: ChatItem) -> Chat:
190
- # legacy: eliminate
225
+ # legacy: eliminate after migration
191
226
  context = convert_outer_context_to_context(chat_item.outer_context)
192
- messages = [convert_replica_item_to_message(replica) for replica in chat_item.inner_context.replicas]
227
+ messages = list(map(convert_replica_item_to_message, chat_item.inner_context.replicas))
193
228
  res = Chat(context=context, messages=messages)
194
229
  return res
195
230
 
196
231
 
197
232
  def convert_context_to_outer_context(context: Context) -> OuterContextItem:
198
- # legacy: eliminate
233
+ # legacy: eliminate after migration
199
234
  extra = context.extra or {}
200
235
  return OuterContextItem(
201
236
  client_id=context.client_id,
@@ -211,20 +246,20 @@ def convert_context_to_outer_context(context: Context) -> OuterContextItem:
211
246
 
212
247
 
213
248
  def convert_message_to_replica_item(message: ChatMessage) -> ReplicaItem | None:
214
- # legacy: eliminate
249
+ # legacy: eliminate after migration
215
250
  m_type = message.type
216
251
  if m_type in {"ai", "human"}:
217
252
  role = m_type == "ai"
218
253
  else:
219
254
  return None
220
255
 
221
- extra = message.extra or {}
222
- action = extra.pop("action")
223
- moderation = extra.pop("moderation")
256
+ extra = deepcopy(message.extra) if message.extra else {}
257
+ action = extra.pop("action", "")
258
+ moderation = extra.pop("moderation", "OK")
224
259
 
225
260
  kwargs = dict(
226
261
  role=role,
227
- body=message.body,
262
+ body=message.text,
228
263
  resource_id=message.resource_id,
229
264
  command=message.command,
230
265
  widget=message.widget,
@@ -238,16 +273,19 @@ def convert_message_to_replica_item(message: ChatMessage) -> ReplicaItem | None:
238
273
 
239
274
 
240
275
  def convert_chat_to_chat_item(chat: Chat) -> ChatItem:
241
- # legacy: eliminate
276
+ # legacy: eliminate after migration
242
277
  return ChatItem(
243
278
  outer_context=convert_context_to_outer_context(chat.context),
244
279
  inner_context=dict(replicas=list(map(convert_message_to_replica_item, chat.messages))),
245
280
  )
246
281
 
247
282
 
248
- def parse_chat_item_as_chat(chat_obj: str | dict) -> Chat:
249
- # legacy: eliminate
250
- chat_item = ChatItem.parse(chat_obj)
283
+ def parse_chat_item_as_chat(chat_obj: str | dict | ChatItem) -> Chat:
284
+ # legacy: eliminate after migration
285
+ if isinstance(chat_obj, ChatItem):
286
+ chat_item = chat_obj
287
+ else:
288
+ chat_item = ChatItem.parse(chat_obj)
251
289
  res = convert_chat_item_to_chat(chat_item)
252
290
  return res
253
291
 
@@ -259,9 +297,10 @@ def _parse_chat(chat_obj: str | dict) -> Chat:
259
297
  return Chat.model_validate_json(chat_obj)
260
298
 
261
299
 
262
- def _parse_chat_compat(chat_obj: str | dict) -> Chat:
263
- # legacy: eliminate
300
+ def _parse_chat_compat(chat_obj: str | dict | ChatItem) -> Chat:
301
+ # legacy: eliminate after migration
264
302
  try:
265
303
  return _parse_chat(chat_obj)
266
- except Exception:
304
+ except ValidationError as ex:
305
+ warnings.warn(f"Failed to parse chat: {ex}")
267
306
  return parse_chat_item_as_chat(chat_obj)
@@ -2,7 +2,7 @@ from datetime import datetime
2
2
  from typing import Annotated, Any, List
3
3
 
4
4
  from agi_med_common.models.widget import Widget
5
- from pydantic import Field, ConfigDict, BeforeValidator
5
+ from pydantic import Field, ConfigDict, BeforeValidator, AfterValidator
6
6
 
7
7
  from ._base import _Base
8
8
 
@@ -39,6 +39,7 @@ class OuterContextItem(_Base):
39
39
  def to_dict(self) -> dict[str, Any]:
40
40
  return self.model_dump(by_alias=True)
41
41
 
42
+
42
43
  LABELS = {
43
44
  0: "OK",
44
45
  1: "NON_MED",
@@ -58,9 +59,15 @@ def fix_deprecated_moderation(moderation):
58
59
  raise ValueError(f"Unsupported moderation: {moderation} :: {type(moderation)}")
59
60
 
60
61
 
62
+ def nullify_empty(text: str) -> str | None:
63
+ return text or None
64
+
65
+
61
66
  class ReplicaItem(_Base):
62
67
  body: str = Field("", alias="Body", examples=["Привет"])
63
- resource_id: str | None = Field(None, alias="ResourceId", examples=["<link-id>"])
68
+ resource_id: Annotated[str | None, AfterValidator(nullify_empty)] = Field(
69
+ None, alias="ResourceId", examples=["<link-id>"]
70
+ )
64
71
  widget: Widget | None = Field(None, alias="Widget", examples=[None])
65
72
  command: dict | None = Field(None, alias="Command", examples=[None])
66
73
  role: bool = Field(False, alias="Role", description="True = ai, False = client", examples=[False])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agi_med_common
3
- Version: 5.0.22
3
+ Version: 5.0.24
4
4
  Summary: Сommon for agi-med team
5
5
  Author: AGI-MED-TEAM
6
6
  Requires-Python: >=3.11
@@ -1,4 +1,4 @@
1
- agi_med_common/__init__.py,sha256=fcjGkG64NHcHGY7u8OoKVNoB5hlBcJ7dZPqegeU2mTg,740
1
+ agi_med_common/__init__.py,sha256=MKI-hB8aFY-Yktyicq9m-8w-pX37IyFztVaKV5Q3Ipk,740
2
2
  agi_med_common/api.py,sha256=kGLKiebbF91s_7V2S55ETS7R6tQgZNkoFMoawdjeMYw,1860
3
3
  agi_med_common/api_v2.py,sha256=gj6BPEAOvpT6GHWeE7bzmKIR-pvq0yORZ3L0ADvI8ps,1828
4
4
  agi_med_common/file_storage.py,sha256=T0Hbs4W-pWO6HdWcmlVqABrQHYdq7lLZH4_Vu-YNVbw,1802
@@ -9,14 +9,14 @@ agi_med_common/validators.py,sha256=vMoPN42XzC8re-zdjekk5_lNQYHuTiAWD56YLvj2Z2w,
9
9
  agi_med_common/xml_parser.py,sha256=VvLIX_XCZao9i0qqpTVx8nx0vbFXSe8pEbdJdXnj97g,568
10
10
  agi_med_common/models/__init__.py,sha256=dqr2kP-RuxFfAZhCr103PQzTVZFKIcdxyzTYiHhdTsE,375
11
11
  agi_med_common/models/_base.py,sha256=qNdH8x3x3mYbo5XgWtR9VpEarxsEvXvzynadUlDvHmU,149
12
- agi_med_common/models/chat.py,sha256=LihfERvN_owk4zsW8eQPKOoXyYCz9lmsD5zw7db6vYk,7583
13
- agi_med_common/models/chat_item.py,sha256=aF_sUtucV5hXeQ42Z-Jtsz9G1sWtjSFtElRs6hr0AQQ,4648
12
+ agi_med_common/models/chat.py,sha256=0VXu-SUPlOeGRO8SMCFPfGoAiVoPIm_v3hKMG-nGhNI,9230
13
+ agi_med_common/models/chat_item.py,sha256=bg_l6B4RAqiSO84vF50nQN7kYCkLB6JHAzItwm1-RzM,4791
14
14
  agi_med_common/models/enums.py,sha256=J-GNpql9MCnKnWiV9aJRQGI-pAybvV86923RZs99grA,1006
15
15
  agi_med_common/models/tracks.py,sha256=UP-jeWqDiCK6dyoMDfs7hemgl_xsJKee_DApjBf-XYc,311
16
16
  agi_med_common/models/widget.py,sha256=aJZ2vWx_PTFN02Wz16eokz9IIVrxqNuZYVDqLG36toE,710
17
17
  agi_med_common/models/base_config_models/__init__.py,sha256=KjS_bSCka8BOMsigwcIML-e6eNB2ouMU6gxlhRmzeuY,44
18
18
  agi_med_common/models/base_config_models/gigachat_config.py,sha256=WNSCTO8Fjpxc1v2LRUHfKqo9aeMDpXltTHYBFgTD2N0,422
19
- agi_med_common-5.0.22.dist-info/METADATA,sha256=TZUf6S7Qxo1qyi-chVFW9oVW9SniMqdGyu49iUg5v0Y,518
20
- agi_med_common-5.0.22.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
- agi_med_common-5.0.22.dist-info/top_level.txt,sha256=26o565jF_7wYQj7-YJfTedtT9yDxDcf8RNikOYuPq78,15
22
- agi_med_common-5.0.22.dist-info/RECORD,,
19
+ agi_med_common-5.0.24.dist-info/METADATA,sha256=2stOgKcSCPgY9VKJ4XJLKmAFnsyCXchTasY9WTaGgks,518
20
+ agi_med_common-5.0.24.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
+ agi_med_common-5.0.24.dist-info/top_level.txt,sha256=26o565jF_7wYQj7-YJfTedtT9yDxDcf8RNikOYuPq78,15
22
+ agi_med_common-5.0.24.dist-info/RECORD,,