agi-med-common 5.0.15__tar.gz → 5.0.16__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.
Files changed (36) hide show
  1. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/PKG-INFO +1 -2
  2. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/pyproject.toml +5 -5
  3. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/src/agi_med_common/__init__.py +9 -4
  4. agi_med_common-5.0.16/src/agi_med_common/api/classifier_api.py +14 -0
  5. agi_med_common-5.0.16/src/agi_med_common/api/text_generator_api.py +6 -0
  6. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/src/agi_med_common/models/__init__.py +1 -0
  7. agi_med_common-5.0.16/src/agi_med_common/models/chat.py +107 -0
  8. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/src/agi_med_common/models/chat_item.py +11 -7
  9. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/src/agi_med_common/models/widget.py +3 -1
  10. agi_med_common-5.0.16/src/agi_med_common/type_union.py +63 -0
  11. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/src/agi_med_common/utils.py +11 -0
  12. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/src/agi_med_common.egg-info/PKG-INFO +1 -2
  13. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/src/agi_med_common.egg-info/SOURCES.txt +4 -6
  14. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/src/agi_med_common.egg-info/requires.txt +0 -1
  15. agi_med_common-5.0.15/requirements.txt +0 -3
  16. agi_med_common-5.0.15/src/agi_med_common/api/critic_api.py +0 -6
  17. agi_med_common-5.0.15/src/agi_med_common/api/text_processor_api.py +0 -6
  18. agi_med_common-5.0.15/src/agi_med_common/logger/__init__.py +0 -2
  19. agi_med_common-5.0.15/src/agi_med_common/logger/log_level_enum.py +0 -15
  20. agi_med_common-5.0.15/src/agi_med_common/logger/logger.py +0 -28
  21. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/README.md +0 -0
  22. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/setup.cfg +0 -0
  23. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/src/agi_med_common/api/chat_manager_api.py +0 -0
  24. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/src/agi_med_common/api/content_interpreter_api.py +0 -0
  25. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/src/agi_med_common/api/content_interpreter_remote_api.py +0 -0
  26. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/src/agi_med_common/file_storage.py +0 -0
  27. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/src/agi_med_common/models/_base.py +0 -0
  28. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/src/agi_med_common/models/base_config_models/__init__.py +0 -0
  29. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/src/agi_med_common/models/base_config_models/gigachat_config.py +0 -0
  30. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/src/agi_med_common/models/enums.py +0 -0
  31. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/src/agi_med_common/models/tracks.py +0 -0
  32. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/src/agi_med_common/parallel_map.py +0 -0
  33. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/src/agi_med_common/validators.py +0 -0
  34. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/src/agi_med_common/xml_parser.py +0 -0
  35. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/src/agi_med_common.egg-info/dependency_links.txt +0 -0
  36. {agi_med_common-5.0.15 → agi_med_common-5.0.16}/src/agi_med_common.egg-info/top_level.txt +0 -0
@@ -1,12 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agi_med_common
3
- Version: 5.0.15
3
+ Version: 5.0.16
4
4
  Summary: Сommon for agi-med team
5
5
  Author: AGI-MED-TEAM
6
6
  Requires-Python: >=3.11
7
7
  Description-Content-Type: text/markdown
8
8
  Requires-Dist: pydantic<=3.0.0,>=2.9.2
9
- Requires-Dist: loguru~=0.7.2
10
9
  Requires-Dist: tqdm==4.67.*
11
10
 
12
11
  # agi-med-common
@@ -5,17 +5,17 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = 'agi_med_common'
7
7
  requires-python = ">= 3.11"
8
-
9
8
  authors = [{ name = 'AGI-MED-TEAM' }]
10
9
  description = 'Сommon for agi-med team'
11
10
  readme = 'README.md'
12
-
13
- dynamic = ['version', 'dependencies']
11
+ dynamic = ['version']
12
+ dependencies = [
13
+ "pydantic>=2.9.2,<=3.0.0",
14
+ "tqdm==4.67.*",
15
+ ]
14
16
 
15
17
  [tool.setuptools.packages.find]
16
18
  where = ["src"]
17
19
 
18
-
19
20
  [tool.setuptools.dynamic]
20
21
  version = { attr = "agi_med_common.__version__" }
21
- dependencies = { file = ['requirements.txt'] }
@@ -1,12 +1,17 @@
1
- __version__ = "5.0.15"
1
+ __version__ = "5.0.16"
2
2
 
3
- from .logger import LogLevelEnum, logger_init, log_llm_error
4
3
  from .models import (
5
4
  MTRSLabelEnum,
6
5
  ChatItem,
7
6
  InnerContextItem,
8
7
  OuterContextItem,
9
8
  ReplicaItem,
9
+ Chat,
10
+ Context,
11
+ ChatMessage,
12
+ AIMessage,
13
+ HumanMessage,
14
+ MiscMessage,
10
15
  )
11
16
  from .models.widget import Widget
12
17
  from .file_storage import FileStorage, ResourceId
@@ -19,5 +24,5 @@ from .models.tracks import TrackInfo, DomainInfo
19
24
  from .api.chat_manager_api import ChatManagerAPI
20
25
  from .api.content_interpreter_api import ContentInterpreterAPI, Interpretation
21
26
  from .api.content_interpreter_remote_api import ContentInterpreterRemoteAPI
22
- from .api.text_processor_api import TextProcessorAPI
23
- from .api.critic_api import CriticAPI
27
+ from .api.text_generator_api import TextGeneratorAPI
28
+ from .api.classifier_api import ClassifierAPI
@@ -0,0 +1,14 @@
1
+ from typing import List
2
+
3
+ from agi_med_common.models import ChatItem
4
+
5
+
6
+ Value = str
7
+
8
+
9
+ class ClassifierAPI:
10
+ def get_values(self) -> List[Value]:
11
+ raise NotImplementedError
12
+
13
+ def evaluate(self, chat: ChatItem, request_id: str = "") -> Value:
14
+ raise NotImplementedError
@@ -0,0 +1,6 @@
1
+ from agi_med_common.models import ChatItem
2
+
3
+
4
+ class TextGeneratorAPI:
5
+ def process(self, chat: ChatItem, request_id: str = "") -> str:
6
+ raise NotImplementedError
@@ -2,5 +2,6 @@ from ._base import _Base
2
2
 
3
3
  from .enums import MTRSLabelEnum, DiagnosticsXMLTagEnum, MTRSXMLTagEnum, DoctorChoiceXMLTagEnum
4
4
  from .chat_item import ChatItem, OuterContextItem, InnerContextItem, ReplicaItem
5
+ from .chat import Chat, Context, ChatMessage, AIMessage, HumanMessage, MiscMessage
5
6
  from .base_config_models import GigaChatConfig
6
7
  from .tracks import TrackInfo, DomainInfo
@@ -0,0 +1,107 @@
1
+ from datetime import datetime
2
+ from typing import Any, List, Dict, Literal
3
+
4
+ from agi_med_common.models.widget import Widget
5
+ from agi_med_common.type_union import TypeUnion
6
+ from agi_med_common.utils import first_nonnull
7
+ from pydantic import Field
8
+
9
+ from ._base import _Base
10
+
11
+
12
+ _DT_FORMAT: str = "%Y-%m-%d-%H-%M-%S"
13
+ _EXAMPLE_DT: str = datetime(year=1970, month=1, day=1).strftime(_DT_FORMAT)
14
+ StrDict = Dict[str, Any]
15
+ ContentBase = str | Widget | StrDict
16
+ Content = ContentBase | List[ContentBase]
17
+
18
+
19
+ def now_pretty() -> str:
20
+ return datetime.now().strftime(_DT_FORMAT)
21
+
22
+
23
+ class Context(_Base):
24
+ client_id: str = Field("", examples=["543216789"])
25
+ user_id: str = Field("", examples=["123456789"])
26
+ session_id: str = Field("", examples=["987654321"])
27
+ track_id: str = Field(examples=["Hello"])
28
+ extra: StrDict | None = Field(None, examples=[None])
29
+
30
+ def create_id(self, short: bool = False) -> str:
31
+ uid, sid, cid = self.user_id, self.session_id, self.client_id
32
+ if short:
33
+ return f"{cid}_{uid}_{sid}"
34
+ return f"client_{cid}_user_{uid}_session_{sid}"
35
+
36
+
37
+ def _get_str_field(obj: dict, field) -> str | None:
38
+ if not isinstance(obj, dict):
39
+ return None
40
+ text = obj.get(field)
41
+ if text is not None and isinstance(text, str):
42
+ return text
43
+ return None
44
+
45
+
46
+ def _get_text(obj: Content) -> str:
47
+ if isinstance(obj, str):
48
+ return obj
49
+ if isinstance(obj, list):
50
+ return "".join(map(_get_text, obj))
51
+ if isinstance(obj, dict) and obj.get("type") == "text":
52
+ return _get_str_field(obj, "text") or ""
53
+ return ""
54
+
55
+
56
+ def _get_resource_id(obj: Content) -> str | None:
57
+ if isinstance(obj, list):
58
+ return first_nonnull(_get_str_field(el, "resource_id") for el in obj)
59
+ if isinstance(obj, dict) and obj.get("type") == "resource_id":
60
+ return _get_str_field(obj, "resource_id")
61
+ return None
62
+
63
+
64
+ class BaseMessage(_Base):
65
+ type: str
66
+ content: Content = Field("", examples=["Привет"])
67
+ date_time: str = Field(default_factory=now_pretty, examples=[_EXAMPLE_DT])
68
+ extra: StrDict | None = Field(None, examples=[None])
69
+
70
+ @property
71
+ def text(self) -> str:
72
+ return _get_text(self.content)
73
+
74
+ @property
75
+ def resource_id(self) -> str | None:
76
+ return _get_resource_id(self.content)
77
+
78
+ @staticmethod
79
+ def DATETIME_FORMAT() -> str:
80
+ return _DT_FORMAT
81
+
82
+ def with_now_datetime(self):
83
+ return self.model_copy(update=dict(date_time=now_pretty()))
84
+
85
+
86
+ class HumanMessage(BaseMessage):
87
+ type: Literal["human"] = "human"
88
+
89
+
90
+ class AIMessage(BaseMessage):
91
+ type: Literal["ai"] = "ai"
92
+ state: str = Field("", examples=["COLLECTION"])
93
+
94
+
95
+ class MiscMessage(BaseMessage):
96
+ type: Literal["misc"] = "misc"
97
+
98
+
99
+ ChatMessage = TypeUnion[HumanMessage, AIMessage, MiscMessage]
100
+
101
+
102
+ class Chat(_Base):
103
+ context: Context
104
+ messages: List[ChatMessage] = []
105
+
106
+ def create_id(self, short: bool = False) -> str:
107
+ return self.context.create_id(short)
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import warnings
2
3
  from datetime import datetime
3
4
  from typing import Any, List
4
5
 
@@ -97,8 +98,8 @@ class ChatItem(_Base):
97
98
  return [replica.to_dict().get(field, None) for replica in self.inner_context.replicas]
98
99
 
99
100
  @classmethod
100
- def parse(cls, chat_text: str) -> "ChatItem":
101
- return _parse_chat(chat_text)
101
+ def parse(cls, chat_obj: str | dict) -> "ChatItem":
102
+ return _parse_chat(chat_obj)
102
103
 
103
104
 
104
105
  def _is_moderation_int_error(err):
@@ -117,17 +118,20 @@ def _is_moderation_int_errors(ex):
117
118
  return all(map(_is_moderation_int_error, errs))
118
119
 
119
120
 
120
- def _parse_chat(chat_text: str) -> ChatItem:
121
+ def _parse_chat(chat_obj: str | dict) -> ChatItem:
122
+ if isinstance(chat_obj, dict):
123
+ return ChatItem(**chat_obj)
121
124
  try:
122
- return ChatItem.model_validate_json(chat_text)
125
+ return ChatItem.model_validate_json(chat_obj)
123
126
  except ValidationError as ex:
124
127
  if _is_moderation_int_errors(ex):
125
- logger.warning("Failed to parse ChatItem, fallback to old version with `Moderation:int`")
128
+ msg = "Failed to parse ChatItem, fallback to old version with `Moderation:int`"
126
129
  else:
127
- logger.error(f"Failed to parse: {ex}")
130
+ msg = f"Failed to parse: {ex}"
131
+ warnings.warn(msg)
128
132
 
129
133
  # old version
130
- chat_dict = json.loads(chat_text)
134
+ chat_dict = json.loads(chat_obj)
131
135
  for rep in chat_dict["InnerContext"]["Replicas"]:
132
136
  rep["Moderation"] = "OK"
133
137
 
@@ -1,8 +1,10 @@
1
- from typing import List, Self
1
+ from typing import List, Self, Literal
2
+
2
3
  from pydantic import BaseModel, model_validator
3
4
 
4
5
 
5
6
  class Widget(BaseModel):
7
+ type: Literal["widget"] = "widget"
6
8
  buttons: List[List[str]] | None = None
7
9
  ibuttons: List[List[str]] | None = None
8
10
 
@@ -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
@@ -3,6 +3,7 @@ import json
3
3
  import os
4
4
  from datetime import datetime
5
5
  from pathlib import Path
6
+ from typing import Iterable
6
7
 
7
8
 
8
9
  def make_session_id() -> str:
@@ -36,17 +37,20 @@ def try_parse_int(text: str) -> int | None:
36
37
  except (ValueError, TypeError):
37
38
  return None
38
39
 
40
+
39
41
  def try_parse_float(text: str) -> float | None:
40
42
  try:
41
43
  return float(text)
42
44
  except (ValueError, TypeError):
43
45
  return None
44
46
 
47
+
45
48
  def try_parse_bool(v: str | bool) -> bool:
46
49
  if isinstance(v, bool):
47
50
  return v
48
51
  return v.lower() in ("yes", "true", "t", "1")
49
52
 
53
+
50
54
  def pretty_line(text: str, cut_count: int = 100) -> str:
51
55
  if len(text) > 100:
52
56
  text_cut = text[:cut_count]
@@ -56,3 +60,10 @@ def pretty_line(text: str, cut_count: int = 100) -> str:
56
60
  text_pretty = text
57
61
  text_pretty = text_pretty.replace("\n", "\\n")
58
62
  return text_pretty
63
+
64
+
65
+ def first_nonnull(obj: Iterable):
66
+ for elem in obj:
67
+ if elem:
68
+ return elem
69
+ return None
@@ -1,12 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agi_med_common
3
- Version: 5.0.15
3
+ Version: 5.0.16
4
4
  Summary: Сommon for agi-med team
5
5
  Author: AGI-MED-TEAM
6
6
  Requires-Python: >=3.11
7
7
  Description-Content-Type: text/markdown
8
8
  Requires-Dist: pydantic<=3.0.0,>=2.9.2
9
- Requires-Dist: loguru~=0.7.2
10
9
  Requires-Dist: tqdm==4.67.*
11
10
 
12
11
  # agi-med-common
@@ -1,9 +1,9 @@
1
1
  README.md
2
2
  pyproject.toml
3
- requirements.txt
4
3
  src/agi_med_common/__init__.py
5
4
  src/agi_med_common/file_storage.py
6
5
  src/agi_med_common/parallel_map.py
6
+ src/agi_med_common/type_union.py
7
7
  src/agi_med_common/utils.py
8
8
  src/agi_med_common/validators.py
9
9
  src/agi_med_common/xml_parser.py
@@ -13,15 +13,13 @@ src/agi_med_common.egg-info/dependency_links.txt
13
13
  src/agi_med_common.egg-info/requires.txt
14
14
  src/agi_med_common.egg-info/top_level.txt
15
15
  src/agi_med_common/api/chat_manager_api.py
16
+ src/agi_med_common/api/classifier_api.py
16
17
  src/agi_med_common/api/content_interpreter_api.py
17
18
  src/agi_med_common/api/content_interpreter_remote_api.py
18
- src/agi_med_common/api/critic_api.py
19
- src/agi_med_common/api/text_processor_api.py
20
- src/agi_med_common/logger/__init__.py
21
- src/agi_med_common/logger/log_level_enum.py
22
- src/agi_med_common/logger/logger.py
19
+ src/agi_med_common/api/text_generator_api.py
23
20
  src/agi_med_common/models/__init__.py
24
21
  src/agi_med_common/models/_base.py
22
+ src/agi_med_common/models/chat.py
25
23
  src/agi_med_common/models/chat_item.py
26
24
  src/agi_med_common/models/enums.py
27
25
  src/agi_med_common/models/tracks.py
@@ -1,3 +1,2 @@
1
1
  pydantic<=3.0.0,>=2.9.2
2
- loguru~=0.7.2
3
2
  tqdm==4.67.*
@@ -1,3 +0,0 @@
1
- pydantic>=2.9.2,<=3.0.0
2
- loguru~=0.7.2
3
- tqdm==4.67.*
@@ -1,6 +0,0 @@
1
- from agi_med_common.models import ChatItem
2
-
3
-
4
- class CriticAPI:
5
- def evaluate(self, text: str, chat: ChatItem | None = None, request_id: str = "") -> float:
6
- raise NotImplementedError
@@ -1,6 +0,0 @@
1
- from agi_med_common.models import ChatItem
2
-
3
-
4
- class TextProcessorAPI:
5
- def process(self, text: str, chat: ChatItem | None = None, request_id: str = "") -> str:
6
- raise NotImplementedError
@@ -1,2 +0,0 @@
1
- from .log_level_enum import LogLevelEnum
2
- from .logger import logger_init, log_llm_error, logger
@@ -1,15 +0,0 @@
1
- from enum import StrEnum, auto
2
-
3
-
4
- class LogLevelEnum(StrEnum):
5
- @staticmethod
6
- def _generate_next_value_(name: str, start: int, count: int, last_values: list[str]) -> str:
7
- return name.upper()
8
-
9
- TRACE = auto() # logger.trace()
10
- DEBUG = auto() # logger.debug()
11
- INFO = auto() # logger.info()
12
- SUCCESS = auto() # logger.success()
13
- WARNING = auto() # logger.warning()
14
- ERROR = auto() # logger.error()
15
- CRITICAL = auto() # logger.critical()
@@ -1,28 +0,0 @@
1
- import sys
2
-
3
- from loguru import logger
4
-
5
- from . import LogLevelEnum
6
-
7
-
8
- def logger_init(log_level: LogLevelEnum) -> None:
9
- logger.remove()
10
- extra = {"request_id": "SYSTEM_LOG"}
11
- format_ = "{time:DD-MM-YYYY HH:mm:ss} | <level>{level: <8}</level> | {extra[request_id]}"
12
- format_ = f"{format_} | <level>{{message}}</level>"
13
- logger.add(sys.stdout, colorize=True, format=format_, level=log_level)
14
- logger.configure(extra=extra)
15
-
16
-
17
- def log_llm_error(
18
- text: str | None = None,
19
- vector: list[float] | None = None,
20
- model: str = "gigachat",
21
- ) -> None:
22
- if text is not None and not text:
23
- logger.error(f"No response from {model}!!!")
24
- return None
25
- if vector is not None and all(not item for item in vector):
26
- logger.error(f"No response from {model} encoder!!!")
27
- return None
28
- return None