mmar-mapi 1.0.26__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.26
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.26"
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
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
 
@@ -51,10 +49,12 @@ class Context(Base):
51
49
 
52
50
  def create_id(self, short: bool = False) -> str:
53
51
  uid, sid, cid = self.user_id, self.session_id, self.client_id
54
- if short:
55
- return f"{cid}_{uid}_{sid}"
56
52
  return f"client_{cid}_user_{uid}_session_{sid}"
57
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
+
58
58
  def _get_deprecated_extra(self, field, default):
59
59
  # legacy: eliminate after migration
60
60
  res = (self.extra or {}).get(field, default)
@@ -259,11 +259,8 @@ class Chat(Base):
259
259
  return self.context.create_id(short)
260
260
 
261
261
  @staticmethod
262
- def parse(chat_obj: str | dict | ChatItem) -> "Chat":
263
- return _parse_chat_compat(chat_obj)
264
-
265
- def to_chat_item(self, failsafe: bool = False) -> ChatItem:
266
- return convert_chat_to_chat_item(self, failsafe)
262
+ def parse(chat_obj: str | dict) -> "Chat":
263
+ return _parse_chat(chat_obj)
267
264
 
268
265
  def add_message(self, message: ChatMessage):
269
266
  self.messages.append(message)
@@ -295,7 +292,10 @@ class Chat(Base):
295
292
  message = messages[-1]
296
293
  return message if isinstance(message, HumanMessage) else None
297
294
 
298
- 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)
299
299
  return sum(map(func, self.messages))
300
300
 
301
301
 
@@ -337,171 +337,11 @@ def make_content(
337
337
  return content
338
338
 
339
339
 
340
- def convert_replica_item_to_message(replica: ReplicaItem) -> ChatMessage:
341
- date_time = replica.date_time
342
- content = make_content(
343
- text=replica.body,
344
- resource_id=replica.resource_id,
345
- command=replica.command,
346
- widget=replica.widget,
347
- )
348
- # legacy: eliminate after migration
349
- if resource_id := replica.resource_id:
350
- resource = {"type": "resource_id", "resource_id": resource_id}
351
- resource_name = replica.resource_name
352
- if resource_name:
353
- resource["resource_name"] = resource_name
354
- else:
355
- resource = None
356
- body = replica.body
357
- command = (replica.command or None) and {"type": "command", "command": replica.command}
358
- widget = replica.widget
359
- date_time = replica.date_time
360
-
361
- content = list(filter(None, [body, resource, command, widget]))
362
- if len(content) == 0:
363
- content = ""
364
- elif len(content) == 1:
365
- content = content[0]
366
-
367
- is_bot_message = replica.role
368
-
369
- if is_bot_message:
370
- kwargs = dict(
371
- content=content,
372
- date_time=date_time,
373
- state=replica.state,
374
- extra=dict(
375
- **(replica.extra or {}),
376
- action=replica.action,
377
- moderation=replica.moderation,
378
- ),
379
- )
380
- res = AIMessage(**kwargs)
381
- else:
382
- kwargs = dict(content=content, date_time=date_time)
383
- res = HumanMessage(**kwargs)
384
- return res
385
-
386
-
387
- def convert_outer_context_to_context(octx: OuterContextItem) -> Context:
388
- # legacy: eliminate after migration
389
- context = Context(
390
- client_id=octx.client_id,
391
- user_id=octx.user_id,
392
- session_id=octx.session_id,
393
- track_id=octx.track_id,
394
- extra=dict(
395
- sex=octx.sex,
396
- age=octx.age,
397
- parent_session_id=octx.parent_session_id,
398
- entrypoint_key=octx.entrypoint_key,
399
- language_code=octx.language_code,
400
- ),
401
- )
402
- return context
403
-
404
-
405
- def convert_chat_item_to_chat(chat_item: ChatItem) -> Chat:
406
- # legacy: eliminate after migration
407
- context = convert_outer_context_to_context(chat_item.outer_context)
408
- messages = list(map(convert_replica_item_to_message, chat_item.inner_context.replicas))
409
- res = Chat(context=context, messages=messages)
410
- return res
411
-
412
-
413
- def convert_context_to_outer_context(context: Context, failsafe: bool = False) -> OuterContextItem:
414
- # legacy: eliminate after migration
415
- extra = context.extra or {}
416
- if failsafe:
417
- extra["sex"] = extra.get("sex") or True
418
- extra["age"] = extra.get("age") or 42
419
- extra["language_code"] = extra.get("language_code") or ""
420
- extra["entrypoint_key"] = extra.get("entrypoint_key") or ""
421
- return OuterContextItem(
422
- client_id=context.client_id,
423
- user_id=context.user_id,
424
- session_id=context.session_id,
425
- track_id=context.track_id,
426
- sex=extra["sex"],
427
- age=extra["age"],
428
- entrypoint_key=extra["entrypoint_key"],
429
- language_code=extra["language_code"],
430
- parent_session_id=extra.get("parent_session_id"),
431
- )
432
-
433
-
434
- def convert_message_to_replica_item(message: ChatMessage) -> ReplicaItem | None:
435
- # legacy: eliminate after migration
436
- m_type = message.type
437
- if m_type in {"ai", "human"}:
438
- role = m_type == "ai"
439
- else:
440
- return None
441
-
442
- extra = deepcopy(message.extra) if message.extra else {}
443
- action = extra.pop("action", "")
444
- moderation = extra.pop("moderation", "OK")
445
-
446
- kwargs = dict(
447
- role=role,
448
- body=message.text,
449
- resource_id=message.resource_id,
450
- resource_name=message.resource_name,
451
- command=message.command,
452
- widget=message.widget,
453
- date_time=message.date_time,
454
- extra=extra or None,
455
- state=getattr(message, "state", ""),
456
- action=action,
457
- moderation=moderation,
458
- )
459
- return ReplicaItem(**kwargs)
460
-
461
-
462
- def convert_chat_to_chat_item(chat: Chat, failsafe: bool = False) -> ChatItem:
463
- # legacy: eliminate after migration
464
- res = ChatItem(
465
- outer_context=convert_context_to_outer_context(chat.context, failsafe=failsafe),
466
- inner_context=dict(replicas=list(map(convert_message_to_replica_item, chat.messages))),
467
- )
468
- return res
469
-
470
-
471
- def parse_chat_item_as_chat(chat_obj: str | dict | ChatItem) -> Chat:
472
- # legacy: eliminate after migration
473
- if isinstance(chat_obj, ChatItem):
474
- chat_item = chat_obj
475
- else:
476
- chat_item = ChatItem.parse(chat_obj)
477
- res = convert_chat_item_to_chat(chat_item)
478
- return res
479
-
480
-
481
- 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
482
343
  if isinstance(chat_obj, dict):
483
344
  return Chat.model_validate(chat_obj)
484
-
485
- return Chat.model_validate_json(chat_obj)
486
-
487
-
488
- def is_chat_item(chat_obj: str | dict | ChatItem) -> bool:
489
- if isinstance(chat_obj, ChatItem):
490
- return True
491
- if isinstance(chat_obj, dict):
492
- return "OuterContext" in chat_obj
493
345
  if isinstance(chat_obj, str):
494
- return "OuterContext" in chat_obj
495
- warnings.warn(f"Unexpected chat object: {chat_obj} :: {type(chat_obj)}")
496
- return False
497
-
498
-
499
- def _parse_chat_compat(chat_obj: str | dict | ChatItem) -> Chat:
500
- # legacy: eliminate after migration
501
- if is_chat_item(chat_obj):
502
- return parse_chat_item_as_chat(chat_obj)
503
- try:
504
- return _parse_chat(chat_obj)
505
- except ValidationError as ex:
506
- warnings.warn(f"Failed to parse chat: {ex}")
507
- 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