mmar-mapi 1.0.25__tar.gz → 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mmar-mapi might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mmar-mapi
3
- Version: 1.0.25
3
+ Version: 1.1.0
4
4
  Summary: Common pure/IO utilities for multi-modal architectures team
5
5
  Keywords:
6
6
  Author: Eugene Tagin
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "mmar-mapi"
3
3
  # dynamic version is not supported yet on uv_build
4
- version = "1.0.25"
4
+ version = "1.1.0"
5
5
  description = "Common pure/IO utilities for multi-modal architectures team"
6
6
  authors = [{name = "Eugene Tagin", email = "tagin@airi.net"}]
7
7
  license = "MIT"
@@ -11,7 +11,6 @@ from .models.chat import (
11
11
  Content,
12
12
  BaseMessage,
13
13
  )
14
- from .models.chat_item import ChatItem, OuterContextItem, InnerContextItem, ReplicaItem
15
14
  from .models.enums import MTRSLabelEnum, DiagnosticsXMLTagEnum, MTRSXMLTagEnum, DoctorChoiceXMLTagEnum
16
15
  from .models.tracks import TrackInfo, DomainInfo
17
16
  from .models.widget import Widget
@@ -25,7 +24,6 @@ __all__ = [
25
24
  "Base",
26
25
  "BaseMessage",
27
26
  "Chat",
28
- "ChatItem",
29
27
  "ChatMessage",
30
28
  "Content",
31
29
  "Context",
@@ -34,12 +32,9 @@ __all__ = [
34
32
  "DomainInfo",
35
33
  "FileStorage",
36
34
  "HumanMessage",
37
- "InnerContextItem",
38
35
  "MTRSLabelEnum",
39
36
  "MTRSXMLTagEnum",
40
37
  "MiscMessage",
41
- "OuterContextItem",
42
- "ReplicaItem",
43
38
  "ResourceId",
44
39
  "TrackInfo",
45
40
  "Widget",
@@ -1,12 +1,10 @@
1
1
  import warnings
2
2
  from collections.abc import Callable
3
- from copy import deepcopy
4
3
  from datetime import datetime
5
- from typing import Any, Literal, TypeVar
4
+ from typing import Any, Literal, NotRequired, TypedDict, TypeVar
6
5
 
7
- from pydantic import Field, ValidationError
6
+ from pydantic import Field
8
7
 
9
- from mmar_mapi.models.chat_item import ChatItem, OuterContextItem, ReplicaItem
10
8
  from mmar_mapi.models.widget import Widget
11
9
  from mmar_mapi.type_union import TypeUnion
12
10
 
@@ -15,7 +13,25 @@ from .base import Base
15
13
  _DT_FORMAT: str = "%Y-%m-%d-%H-%M-%S"
16
14
  _EXAMPLE_DT: str = datetime(year=1970, month=1, day=1).strftime(_DT_FORMAT)
17
15
  StrDict = dict[str, Any]
18
- ContentBase = str | Widget | StrDict
16
+
17
+
18
+ class ResourceDict(TypedDict):
19
+ type: Literal["resource_id"]
20
+ resource_id: str
21
+ resource_name: NotRequired[str]
22
+
23
+
24
+ class TextDict(TypedDict):
25
+ type: Literal["text"]
26
+ text: str
27
+
28
+
29
+ class CommandDict(TypedDict):
30
+ type: Literal["command"]
31
+ command: StrDict
32
+
33
+
34
+ ContentBase = str | Widget | ResourceDict | CommandDict | TextDict | StrDict
19
35
  Content = ContentBase | list[ContentBase]
20
36
  T = TypeVar("T")
21
37
 
@@ -33,10 +49,12 @@ class Context(Base):
33
49
 
34
50
  def create_id(self, short: bool = False) -> str:
35
51
  uid, sid, cid = self.user_id, self.session_id, self.client_id
36
- if short:
37
- return f"{cid}_{uid}_{sid}"
38
52
  return f"client_{cid}_user_{uid}_session_{sid}"
39
53
 
54
+ def create_trace_id(self) -> str:
55
+ uid, sid, cid = self.user_id, self.session_id, self.client_id
56
+ return f"{cid}_{uid}_{sid}"
57
+
40
58
  def _get_deprecated_extra(self, field, default):
41
59
  # legacy: eliminate after migration
42
60
  res = (self.extra or {}).get(field, default)
@@ -241,11 +259,8 @@ class Chat(Base):
241
259
  return self.context.create_id(short)
242
260
 
243
261
  @staticmethod
244
- def parse(chat_obj: str | dict | ChatItem) -> "Chat":
245
- return _parse_chat_compat(chat_obj)
246
-
247
- def to_chat_item(self, failsafe: bool=False) -> ChatItem:
248
- return convert_chat_to_chat_item(self, failsafe)
262
+ def parse(chat_obj: str | dict) -> "Chat":
263
+ return _parse_chat(chat_obj)
249
264
 
250
265
  def add_message(self, message: ChatMessage):
251
266
  self.messages.append(message)
@@ -277,7 +292,10 @@ class Chat(Base):
277
292
  message = messages[-1]
278
293
  return message if isinstance(message, HumanMessage) else None
279
294
 
280
- def count_messages(self, func: Callable[[ChatMessage], bool]) -> int:
295
+ def count_messages(self, func: Callable[[ChatMessage], bool] | type) -> int:
296
+ if isinstance(func, type):
297
+ msg_type = func
298
+ func = lambda msg: isinstance(msg, msg_type)
281
299
  return sum(map(func, self.messages))
282
300
 
283
301
 
@@ -319,171 +337,11 @@ def make_content(
319
337
  return content
320
338
 
321
339
 
322
- def convert_replica_item_to_message(replica: ReplicaItem) -> ChatMessage:
323
- date_time = replica.date_time
324
- content = make_content(
325
- text=replica.body,
326
- resource_id=replica.resource_id,
327
- command=replica.command,
328
- widget=replica.widget,
329
- )
330
- # legacy: eliminate after migration
331
- if resource_id := replica.resource_id:
332
- resource = {"type": "resource_id", "resource_id": resource_id}
333
- resource_name = replica.resource_name
334
- if resource_name:
335
- resource["resource_name"] = resource_name
336
- else:
337
- resource = None
338
- body = replica.body
339
- command = (replica.command or None) and {"type": "command", "command": replica.command}
340
- widget = replica.widget
341
- date_time = replica.date_time
342
-
343
- content = list(filter(None, [body, resource, command, widget]))
344
- if len(content) == 0:
345
- content = ""
346
- elif len(content) == 1:
347
- content = content[0]
348
-
349
- is_bot_message = replica.role
350
-
351
- if is_bot_message:
352
- kwargs = dict(
353
- content=content,
354
- date_time=date_time,
355
- state=replica.state,
356
- extra=dict(
357
- **(replica.extra or {}),
358
- action=replica.action,
359
- moderation=replica.moderation,
360
- ),
361
- )
362
- res = AIMessage(**kwargs)
363
- else:
364
- kwargs = dict(content=content, date_time=date_time)
365
- res = HumanMessage(**kwargs)
366
- return res
367
-
368
-
369
- def convert_outer_context_to_context(octx: OuterContextItem) -> Context:
370
- # legacy: eliminate after migration
371
- context = Context(
372
- client_id=octx.client_id,
373
- user_id=octx.user_id,
374
- session_id=octx.session_id,
375
- track_id=octx.track_id,
376
- extra=dict(
377
- sex=octx.sex,
378
- age=octx.age,
379
- parent_session_id=octx.parent_session_id,
380
- entrypoint_key=octx.entrypoint_key,
381
- language_code=octx.language_code,
382
- ),
383
- )
384
- return context
385
-
386
-
387
- def convert_chat_item_to_chat(chat_item: ChatItem) -> Chat:
388
- # legacy: eliminate after migration
389
- context = convert_outer_context_to_context(chat_item.outer_context)
390
- messages = list(map(convert_replica_item_to_message, chat_item.inner_context.replicas))
391
- res = Chat(context=context, messages=messages)
392
- return res
393
-
394
-
395
- def convert_context_to_outer_context(context: Context, failsafe: bool=False) -> OuterContextItem:
396
- # legacy: eliminate after migration
397
- extra = context.extra or {}
398
- if failsafe:
399
- extra['sex'] = extra.get('sex') or True
400
- extra['age'] = extra.get('age') or 42
401
- extra['language_code'] = extra.get('language_code') or ''
402
- extra['entrypoint_key'] = extra.get('entrypoint_key') or ''
403
- return OuterContextItem(
404
- client_id=context.client_id,
405
- user_id=context.user_id,
406
- session_id=context.session_id,
407
- track_id=context.track_id,
408
- sex=extra["sex"],
409
- age=extra["age"],
410
- entrypoint_key=extra["entrypoint_key"],
411
- language_code=extra["language_code"],
412
- parent_session_id=extra.get("parent_session_id"),
413
- )
414
-
415
-
416
- def convert_message_to_replica_item(message: ChatMessage) -> ReplicaItem | None:
417
- # legacy: eliminate after migration
418
- m_type = message.type
419
- if m_type in {"ai", "human"}:
420
- role = m_type == "ai"
421
- else:
422
- return None
423
-
424
- extra = deepcopy(message.extra) if message.extra else {}
425
- action = extra.pop("action", "")
426
- moderation = extra.pop("moderation", "OK")
427
-
428
- kwargs = dict(
429
- role=role,
430
- body=message.text,
431
- resource_id=message.resource_id,
432
- resource_name=message.resource_name,
433
- command=message.command,
434
- widget=message.widget,
435
- date_time=message.date_time,
436
- extra=extra or None,
437
- state=getattr(message, "state", ""),
438
- action=action,
439
- moderation=moderation,
440
- )
441
- return ReplicaItem(**kwargs)
442
-
443
-
444
- def convert_chat_to_chat_item(chat: Chat, failsafe: bool=False) -> ChatItem:
445
- # legacy: eliminate after migration
446
- res = ChatItem(
447
- outer_context=convert_context_to_outer_context(chat.context, failsafe=failsafe),
448
- inner_context=dict(replicas=list(map(convert_message_to_replica_item, chat.messages))),
449
- )
450
- return res
451
-
452
-
453
- def parse_chat_item_as_chat(chat_obj: str | dict | ChatItem) -> Chat:
454
- # legacy: eliminate after migration
455
- if isinstance(chat_obj, ChatItem):
456
- chat_item = chat_obj
457
- else:
458
- chat_item = ChatItem.parse(chat_obj)
459
- res = convert_chat_item_to_chat(chat_item)
460
- return res
461
-
462
-
463
- def _parse_chat(chat_obj: str | dict) -> Chat:
340
+ def _parse_chat(chat_obj: str | dict | Chat) -> Chat:
341
+ if isinstance(chat_obj, Chat):
342
+ return chat_obj
464
343
  if isinstance(chat_obj, dict):
465
344
  return Chat.model_validate(chat_obj)
466
-
467
- return Chat.model_validate_json(chat_obj)
468
-
469
-
470
- def is_chat_item(chat_obj: str | dict | ChatItem) -> bool:
471
- if isinstance(chat_obj, ChatItem):
472
- return True
473
- if isinstance(chat_obj, dict):
474
- return "OuterContext" in chat_obj
475
345
  if isinstance(chat_obj, str):
476
- return "OuterContext" in chat_obj
477
- warnings.warn(f"Unexpected chat object: {chat_obj} :: {type(chat_obj)}")
478
- return False
479
-
480
-
481
- def _parse_chat_compat(chat_obj: str | dict | ChatItem) -> Chat:
482
- # legacy: eliminate after migration
483
- if is_chat_item(chat_obj):
484
- return parse_chat_item_as_chat(chat_obj)
485
- try:
486
- return _parse_chat(chat_obj)
487
- except ValidationError as ex:
488
- warnings.warn(f"Failed to parse chat: {ex}")
489
- return parse_chat_item_as_chat(chat_obj)
346
+ return Chat.model_validate_json(chat_obj)
347
+ raise ValueError(f"Bad chat_obj {type(chat_obj)}: {chat_obj}")
@@ -1,160 +0,0 @@
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
- resource_name: Annotated[str | None, AfterValidator(nullify_empty)] = Field(
73
- None, alias="ResourceName", examples=["filename"]
74
- )
75
- widget: Widget | None = Field(None, alias="Widget", examples=[None])
76
- command: dict | None = Field(None, alias="Command", examples=[None])
77
- role: bool = Field(False, alias="Role", description="True = ai, False = client", examples=[False])
78
- date_time: str = Field(
79
- default_factory=now_pretty, alias="DateTime", examples=[_EXAMPLE_DT], description=f"Format: {_DT_FORMAT}"
80
- )
81
- state: str = Field("", alias="State", description="chat manager fsm state", examples=["COLLECTION"])
82
- action: str = Field("", alias="Action", description="chat manager fsm action", examples=["DIAGNOSIS"])
83
- # todo fix: support loading from `moderation: int`
84
- moderation: Annotated[str, BeforeValidator(str)] = Field(
85
- "OK", alias="Moderation", description="moderation outcome", examples=["OK"]
86
- )
87
- extra: dict | None = Field(None, alias="Extra", examples=[None])
88
-
89
- def to_dict(self) -> dict[str, Any]:
90
- return self.model_dump(by_alias=True)
91
-
92
- @staticmethod
93
- def DATETIME_FORMAT() -> str:
94
- return _DT_FORMAT
95
-
96
- def with_now_datetime(self):
97
- return self.model_copy(update=dict(date_time=now_pretty()))
98
-
99
- @property
100
- def is_ai(self):
101
- return self.role
102
-
103
- @property
104
- def is_human(self):
105
- return not self.role
106
-
107
- def modify_text(self, callback: Callable[[str], str]) -> "ReplicaItem":
108
- body_upd = callback(self.body)
109
- return self.model_copy(update=dict(body=body_upd))
110
-
111
-
112
- class InnerContextItem(Base):
113
- replicas: list[ReplicaItem] = Field(alias="Replicas")
114
- attrs: dict[str, str | int] | None = Field(default={}, alias="Attrs")
115
-
116
- def to_dict(self) -> dict[str, list]:
117
- return self.model_dump(by_alias=True)
118
-
119
-
120
- class ChatItem(Base):
121
- outer_context: OuterContextItem = Field(alias="OuterContext")
122
- inner_context: InnerContextItem = Field(alias="InnerContext")
123
-
124
- def create_id(self, short: bool = False) -> str:
125
- return self.outer_context.create_id(short)
126
-
127
- def to_dict(self) -> dict[str, Any]:
128
- return self.model_dump(by_alias=True)
129
-
130
- def add_replica(self, replica: ReplicaItem):
131
- self.inner_context.replicas.append(replica)
132
-
133
- def add_replicas(self, replicas: list[ReplicaItem]):
134
- for replica in replicas:
135
- self.inner_context.replicas.append(replica)
136
-
137
- def replace_replicas(self, replicas: list[ReplicaItem]):
138
- return self.model_copy(update=dict(inner_context=InnerContextItem(replicas=replicas)))
139
-
140
- def get_last_state(self, default: str = "empty") -> str:
141
- replicas = self.inner_context.replicas
142
- for ii in range(len(replicas) - 1, -1, -1):
143
- replica = replicas[ii]
144
- if replica.role:
145
- return replica.state
146
- return default
147
-
148
- def zip_history(self, field: str) -> list[Any]:
149
- return [replica.to_dict().get(field, None) for replica in self.inner_context.replicas]
150
-
151
- @classmethod
152
- def parse(cls, chat_obj: str | dict) -> "ChatItem":
153
- return _parse_chat_item(chat_obj)
154
-
155
-
156
- def _parse_chat_item(chat_obj: str | dict) -> ChatItem:
157
- if isinstance(chat_obj, dict):
158
- return ChatItem.model_validate(chat_obj)
159
-
160
- return ChatItem.model_validate_json(chat_obj)
File without changes
File without changes