agi-med-common 5.0.15__tar.gz → 5.0.17__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.17}/PKG-INFO +1 -2
  2. {agi_med_common-5.0.15 → agi_med_common-5.0.17}/pyproject.toml +5 -5
  3. {agi_med_common-5.0.15 → agi_med_common-5.0.17}/src/agi_med_common/__init__.py +9 -4
  4. agi_med_common-5.0.17/src/agi_med_common/api/classifier_api.py +14 -0
  5. agi_med_common-5.0.17/src/agi_med_common/api/text_generator_api.py +6 -0
  6. {agi_med_common-5.0.15 → agi_med_common-5.0.17}/src/agi_med_common/models/__init__.py +1 -0
  7. agi_med_common-5.0.17/src/agi_med_common/models/chat.py +267 -0
  8. {agi_med_common-5.0.15 → agi_med_common-5.0.17}/src/agi_med_common/models/chat_item.py +29 -37
  9. {agi_med_common-5.0.15 → agi_med_common-5.0.17}/src/agi_med_common/models/widget.py +3 -1
  10. agi_med_common-5.0.17/src/agi_med_common/type_union.py +63 -0
  11. {agi_med_common-5.0.15 → agi_med_common-5.0.17}/src/agi_med_common/utils.py +11 -0
  12. {agi_med_common-5.0.15 → agi_med_common-5.0.17}/src/agi_med_common.egg-info/PKG-INFO +1 -2
  13. {agi_med_common-5.0.15 → agi_med_common-5.0.17}/src/agi_med_common.egg-info/SOURCES.txt +4 -6
  14. {agi_med_common-5.0.15 → agi_med_common-5.0.17}/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.17}/README.md +0 -0
  22. {agi_med_common-5.0.15 → agi_med_common-5.0.17}/setup.cfg +0 -0
  23. {agi_med_common-5.0.15 → agi_med_common-5.0.17}/src/agi_med_common/api/chat_manager_api.py +0 -0
  24. {agi_med_common-5.0.15 → agi_med_common-5.0.17}/src/agi_med_common/api/content_interpreter_api.py +0 -0
  25. {agi_med_common-5.0.15 → agi_med_common-5.0.17}/src/agi_med_common/api/content_interpreter_remote_api.py +0 -0
  26. {agi_med_common-5.0.15 → agi_med_common-5.0.17}/src/agi_med_common/file_storage.py +0 -0
  27. {agi_med_common-5.0.15 → agi_med_common-5.0.17}/src/agi_med_common/models/_base.py +0 -0
  28. {agi_med_common-5.0.15 → agi_med_common-5.0.17}/src/agi_med_common/models/base_config_models/__init__.py +0 -0
  29. {agi_med_common-5.0.15 → agi_med_common-5.0.17}/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.17}/src/agi_med_common/models/enums.py +0 -0
  31. {agi_med_common-5.0.15 → agi_med_common-5.0.17}/src/agi_med_common/models/tracks.py +0 -0
  32. {agi_med_common-5.0.15 → agi_med_common-5.0.17}/src/agi_med_common/parallel_map.py +0 -0
  33. {agi_med_common-5.0.15 → agi_med_common-5.0.17}/src/agi_med_common/validators.py +0 -0
  34. {agi_med_common-5.0.15 → agi_med_common-5.0.17}/src/agi_med_common/xml_parser.py +0 -0
  35. {agi_med_common-5.0.15 → agi_med_common-5.0.17}/src/agi_med_common.egg-info/dependency_links.txt +0 -0
  36. {agi_med_common-5.0.15 → agi_med_common-5.0.17}/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.17
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.17"
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,267 @@
1
+ from datetime import datetime
2
+ from typing import Any, List, Dict, Literal
3
+
4
+ from agi_med_common.models.chat_item import ChatItem, ReplicaItem, OuterContextItem
5
+ from agi_med_common.models.widget import Widget
6
+ from agi_med_common.type_union import TypeUnion
7
+ from agi_med_common.utils import first_nonnull
8
+ from pydantic import Field
9
+
10
+ from ._base import _Base
11
+
12
+
13
+ _DT_FORMAT: str = "%Y-%m-%d-%H-%M-%S"
14
+ _EXAMPLE_DT: str = datetime(year=1970, month=1, day=1).strftime(_DT_FORMAT)
15
+ StrDict = Dict[str, Any]
16
+ ContentBase = str | Widget | StrDict
17
+ Content = ContentBase | List[ContentBase]
18
+
19
+
20
+ def now_pretty() -> str:
21
+ return datetime.now().strftime(_DT_FORMAT)
22
+
23
+
24
+ class Context(_Base):
25
+ client_id: str = Field("", examples=["543216789"])
26
+ user_id: str = Field("", examples=["123456789"])
27
+ session_id: str = Field("", examples=["987654321"])
28
+ track_id: str = Field(examples=["Hello"])
29
+ extra: StrDict | None = Field(None, examples=[None])
30
+
31
+ def create_id(self, short: bool = False) -> str:
32
+ uid, sid, cid = self.user_id, self.session_id, self.client_id
33
+ if short:
34
+ return f"{cid}_{uid}_{sid}"
35
+ return f"client_{cid}_user_{uid}_session_{sid}"
36
+
37
+
38
+ def _get_str_field(obj: dict, field) -> str | None:
39
+ if not isinstance(obj, dict):
40
+ return None
41
+ text = obj.get(field)
42
+ if text is not None and isinstance(text, str):
43
+ return text
44
+ return None
45
+
46
+
47
+ def _get_text(obj: Content) -> str:
48
+ if isinstance(obj, str):
49
+ return obj
50
+ if isinstance(obj, list):
51
+ return "".join(map(_get_text, obj))
52
+ if isinstance(obj, dict) and obj.get("type") == "text":
53
+ return _get_str_field(obj, "text") or ""
54
+ return ""
55
+
56
+
57
+ def _get_resource_id(obj: Content) -> str | None:
58
+ if isinstance(obj, list):
59
+ return first_nonnull(map(_get_resource_id, obj))
60
+ if isinstance(obj, dict) and obj.get("type") == "resource_id":
61
+ return _get_str_field(obj, "resource_id")
62
+ return None
63
+
64
+
65
+ def _get_command(obj: Content) -> dict | None:
66
+ if isinstance(obj, list):
67
+ return first_nonnull(map(_get_command, obj))
68
+ if isinstance(obj, dict) and obj.get("type") == "command":
69
+ return _get_str_field(obj, "command")
70
+ return None
71
+
72
+
73
+ def _get_widget(obj: Content) -> Widget | None:
74
+ if isinstance(obj, list):
75
+ return first_nonnull(map(_get_widget, obj))
76
+ if isinstance(obj, Widget):
77
+ return obj
78
+ return None
79
+
80
+
81
+ # todo fix: generalize functions above
82
+
83
+
84
+ class BaseMessage(_Base):
85
+ type: str
86
+ content: Content = Field("", examples=["Привет"])
87
+ date_time: str = Field(default_factory=now_pretty, examples=[_EXAMPLE_DT])
88
+ extra: StrDict | None = Field(None, examples=[None])
89
+
90
+ @property
91
+ def text(self) -> str:
92
+ return _get_text(self.content)
93
+
94
+ @property
95
+ def resource_id(self) -> str | None:
96
+ return _get_resource_id(self.content)
97
+
98
+ @property
99
+ def command(self) -> dict | None:
100
+ return _get_command(self.content)
101
+
102
+ @property
103
+ def widget(self) -> Widget | None:
104
+ return _get_widget(self.content)
105
+
106
+ @staticmethod
107
+ def DATETIME_FORMAT() -> str:
108
+ return _DT_FORMAT
109
+
110
+ def with_now_datetime(self):
111
+ return self.model_copy(update=dict(date_time=now_pretty()))
112
+
113
+
114
+ class HumanMessage(BaseMessage):
115
+ type: Literal["human"] = "human"
116
+
117
+
118
+ class AIMessage(BaseMessage):
119
+ type: Literal["ai"] = "ai"
120
+ state: str = Field("", examples=["COLLECTION"])
121
+
122
+
123
+ class MiscMessage(BaseMessage):
124
+ type: Literal["misc"] = "misc"
125
+
126
+
127
+ ChatMessage = TypeUnion[HumanMessage, AIMessage, MiscMessage]
128
+
129
+
130
+ class Chat(_Base):
131
+ context: Context
132
+ messages: List[ChatMessage] = []
133
+
134
+ def create_id(self, short: bool = False) -> str:
135
+ return self.context.create_id(short)
136
+
137
+ @classmethod
138
+ def parse(self, chat_obj: str | dict) -> "Chat":
139
+ return _parse_chat_compat(chat_obj)
140
+
141
+
142
+ def convert_replica_item_to_message(replica: ReplicaItem) -> ChatMessage:
143
+ resource_id = (replica.resource_id or None) and {"type": "resource_id", "resource_id": replica.resource_id}
144
+ body = replica.body
145
+ command = replica.command
146
+ widget = replica.widget
147
+
148
+ content = list(filter(None, [body, resource_id, command, widget]))
149
+ if len(content) == 0:
150
+ content = ""
151
+ elif len(content) == 1:
152
+ content = content[0]
153
+
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
+ )
169
+
170
+
171
+ def convert_outer_context_to_context(octx: OuterContextItem) -> Context:
172
+ # legacy: eliminate
173
+ context = Context(
174
+ client_id=octx.client_id,
175
+ user_id=octx.user_id,
176
+ session_id=octx.session_id,
177
+ track_id=octx.track_id,
178
+ extra=dict(
179
+ sex=octx.sex,
180
+ age=octx.age,
181
+ parent_session_id=octx.parent_session_id,
182
+ entrypoint_key=octx.entrypoint_key,
183
+ language_code=octx.language_code,
184
+ ),
185
+ )
186
+ return context
187
+
188
+
189
+ def convert_chat_item_to_chat(chat_item: ChatItem) -> Chat:
190
+ # legacy: eliminate
191
+ 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]
193
+ res = Chat(context=context, messages=messages)
194
+ return res
195
+
196
+
197
+ def convert_context_to_outer_context(context: Context) -> OuterContextItem:
198
+ # legacy: eliminate
199
+ extra = context.extra or {}
200
+ return OuterContextItem(
201
+ client_id=context.client_id,
202
+ user_id=context.user_id,
203
+ session_id=context.session_id,
204
+ track_id=context.track_id,
205
+ sex=extra.get("sex"),
206
+ age=extra.get("age"),
207
+ parent_session_id=extra.get("parent_session_id"),
208
+ entrypoint_key=extra.get("entrypoint_key"),
209
+ language_code=extra.get("language_code"),
210
+ )
211
+
212
+
213
+ def convert_message_to_replica_item(message: ChatMessage) -> ReplicaItem | None:
214
+ # legacy: eliminate
215
+ m_type = message.type
216
+ if m_type in {"ai", "human"}:
217
+ role = m_type == "ai"
218
+ else:
219
+ return None
220
+
221
+ extra = message.extra or {}
222
+ action = extra.pop("action")
223
+ moderation = extra.pop("moderation")
224
+
225
+ kwargs = dict(
226
+ role=role,
227
+ body=message.body,
228
+ resource_id=message.resource_id,
229
+ command=message.command,
230
+ widget=message.widget,
231
+ date_time=message.date_time,
232
+ extra=extra or None,
233
+ state=getattr(message, "state", ""),
234
+ action=action,
235
+ moderation=moderation,
236
+ )
237
+ return ReplicaItem(**kwargs)
238
+
239
+
240
+ def convert_chat_to_chat_item(chat: Chat) -> ChatItem:
241
+ # legacy: eliminate
242
+ return ChatItem(
243
+ outer_context=convert_context_to_outer_context(chat.context),
244
+ inner_context=dict(replicas=list(map(convert_message_to_replica_item, chat.messages))),
245
+ )
246
+
247
+
248
+ def parse_chat_item_as_chat(chat_obj: str | dict) -> Chat:
249
+ # legacy: eliminate
250
+ chat_item = ChatItem.parse(chat_obj)
251
+ res = convert_chat_item_to_chat(chat_item)
252
+ return res
253
+
254
+
255
+ def _parse_chat(chat_obj: str | dict) -> Chat:
256
+ if isinstance(chat_obj, dict):
257
+ return Chat.model_validate(chat_obj)
258
+
259
+ return Chat.model_validate_json(chat_obj)
260
+
261
+
262
+ def _parse_chat_compat(chat_obj: str | dict) -> Chat:
263
+ # legacy: eliminate
264
+ try:
265
+ return _parse_chat(chat_obj)
266
+ except Exception:
267
+ return parse_chat_item_as_chat(chat_obj)
@@ -1,10 +1,8 @@
1
- import json
2
1
  from datetime import datetime
3
- from typing import Any, List
2
+ from typing import Annotated, Any, List
4
3
 
5
4
  from agi_med_common.models.widget import Widget
6
- from loguru import logger
7
- from pydantic import Field, ConfigDict, ValidationError
5
+ from pydantic import Field, ConfigDict, BeforeValidator
8
6
 
9
7
  from ._base import _Base
10
8
 
@@ -41,6 +39,24 @@ class OuterContextItem(_Base):
41
39
  def to_dict(self) -> dict[str, Any]:
42
40
  return self.model_dump(by_alias=True)
43
41
 
42
+ LABELS = {
43
+ 0: "OK",
44
+ 1: "NON_MED",
45
+ 2: "CHILD",
46
+ 3: "ABSURD",
47
+ 4: "GREETING",
48
+ 5: "RECEIPT",
49
+ }
50
+
51
+
52
+ def fix_deprecated_moderation(moderation):
53
+ if isinstance(moderation, int):
54
+ return LABELS.get(moderation, "OK")
55
+ elif isinstance(moderation, str):
56
+ return moderation
57
+ else:
58
+ raise ValueError(f"Unsupported moderation: {moderation} :: {type(moderation)}")
59
+
44
60
 
45
61
  class ReplicaItem(_Base):
46
62
  body: str = Field("", alias="Body", examples=["Привет"])
@@ -54,7 +70,9 @@ class ReplicaItem(_Base):
54
70
  state: str = Field("", alias="State", description="chat manager fsm state", examples=["COLLECTION"])
55
71
  action: str = Field("", alias="Action", description="chat manager fsm action", examples=["DIAGNOSIS"])
56
72
  # todo fix: support loading from `moderation: int`
57
- moderation: str = Field("OK", alias="Moderation", description="moderation outcome", examples=["OK"])
73
+ moderation: Annotated[str, BeforeValidator(str)] = Field(
74
+ "OK", alias="Moderation", description="moderation outcome", examples=["OK"]
75
+ )
58
76
  extra: dict | None = Field(None, alias="Extra", examples=[None])
59
77
 
60
78
  def to_dict(self) -> dict[str, Any]:
@@ -97,38 +115,12 @@ class ChatItem(_Base):
97
115
  return [replica.to_dict().get(field, None) for replica in self.inner_context.replicas]
98
116
 
99
117
  @classmethod
100
- def parse(cls, chat_text: str) -> "ChatItem":
101
- return _parse_chat(chat_text)
102
-
103
-
104
- def _is_moderation_int_error(err):
105
- # fmt: off
106
- if err['type'] != 'string_type': return False
107
- if err['loc'][0] != 'InnerContext': return False
108
- if err['loc'][1] != 'Replicas': return False
109
- if err['loc'][3] != 'Moderation': return False
110
- if err['msg'] != 'Input should be a valid string': return False
111
- # fmt: on
112
- return True
113
-
114
-
115
- def _is_moderation_int_errors(ex):
116
- errs = ex.errors()
117
- return all(map(_is_moderation_int_error, errs))
118
-
118
+ def parse(cls, chat_obj: str | dict) -> "ChatItem":
119
+ return _parse_chat_item(chat_obj)
119
120
 
120
- def _parse_chat(chat_text: str) -> ChatItem:
121
- try:
122
- return ChatItem.model_validate_json(chat_text)
123
- except ValidationError as ex:
124
- if _is_moderation_int_errors(ex):
125
- logger.warning("Failed to parse ChatItem, fallback to old version with `Moderation:int`")
126
- else:
127
- logger.error(f"Failed to parse: {ex}")
128
121
 
129
- # old version
130
- chat_dict = json.loads(chat_text)
131
- for rep in chat_dict["InnerContext"]["Replicas"]:
132
- rep["Moderation"] = "OK"
122
+ def _parse_chat_item(chat_obj: str | dict) -> ChatItem:
123
+ if isinstance(chat_obj, dict):
124
+ return ChatItem.model_validate(chat_obj)
133
125
 
134
- return ChatItem.model_validate(chat_dict)
126
+ return ChatItem.model_validate_json(chat_obj)
@@ -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.17
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