dtlpy 1.113.10__py3-none-any.whl → 1.114.13__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.
Files changed (243) hide show
  1. dtlpy/__init__.py +488 -488
  2. dtlpy/__version__.py +1 -1
  3. dtlpy/assets/__init__.py +26 -26
  4. dtlpy/assets/__pycache__/__init__.cpython-38.pyc +0 -0
  5. dtlpy/assets/code_server/config.yaml +2 -2
  6. dtlpy/assets/code_server/installation.sh +24 -24
  7. dtlpy/assets/code_server/launch.json +13 -13
  8. dtlpy/assets/code_server/settings.json +2 -2
  9. dtlpy/assets/main.py +53 -53
  10. dtlpy/assets/main_partial.py +18 -18
  11. dtlpy/assets/mock.json +11 -11
  12. dtlpy/assets/model_adapter.py +83 -83
  13. dtlpy/assets/package.json +61 -61
  14. dtlpy/assets/package_catalog.json +29 -29
  15. dtlpy/assets/package_gitignore +307 -307
  16. dtlpy/assets/service_runners/__init__.py +33 -33
  17. dtlpy/assets/service_runners/converter.py +96 -96
  18. dtlpy/assets/service_runners/multi_method.py +49 -49
  19. dtlpy/assets/service_runners/multi_method_annotation.py +54 -54
  20. dtlpy/assets/service_runners/multi_method_dataset.py +55 -55
  21. dtlpy/assets/service_runners/multi_method_item.py +52 -52
  22. dtlpy/assets/service_runners/multi_method_json.py +52 -52
  23. dtlpy/assets/service_runners/single_method.py +37 -37
  24. dtlpy/assets/service_runners/single_method_annotation.py +43 -43
  25. dtlpy/assets/service_runners/single_method_dataset.py +43 -43
  26. dtlpy/assets/service_runners/single_method_item.py +41 -41
  27. dtlpy/assets/service_runners/single_method_json.py +42 -42
  28. dtlpy/assets/service_runners/single_method_multi_input.py +45 -45
  29. dtlpy/assets/voc_annotation_template.xml +23 -23
  30. dtlpy/caches/base_cache.py +32 -32
  31. dtlpy/caches/cache.py +473 -473
  32. dtlpy/caches/dl_cache.py +201 -201
  33. dtlpy/caches/filesystem_cache.py +89 -89
  34. dtlpy/caches/redis_cache.py +84 -84
  35. dtlpy/dlp/__init__.py +20 -20
  36. dtlpy/dlp/cli_utilities.py +367 -367
  37. dtlpy/dlp/command_executor.py +764 -764
  38. dtlpy/dlp/dlp +1 -1
  39. dtlpy/dlp/dlp.bat +1 -1
  40. dtlpy/dlp/dlp.py +128 -128
  41. dtlpy/dlp/parser.py +651 -651
  42. dtlpy/entities/__init__.py +83 -83
  43. dtlpy/entities/analytic.py +311 -311
  44. dtlpy/entities/annotation.py +1879 -1879
  45. dtlpy/entities/annotation_collection.py +699 -699
  46. dtlpy/entities/annotation_definitions/__init__.py +20 -20
  47. dtlpy/entities/annotation_definitions/base_annotation_definition.py +100 -100
  48. dtlpy/entities/annotation_definitions/box.py +195 -195
  49. dtlpy/entities/annotation_definitions/classification.py +67 -67
  50. dtlpy/entities/annotation_definitions/comparison.py +72 -72
  51. dtlpy/entities/annotation_definitions/cube.py +204 -204
  52. dtlpy/entities/annotation_definitions/cube_3d.py +149 -149
  53. dtlpy/entities/annotation_definitions/description.py +32 -32
  54. dtlpy/entities/annotation_definitions/ellipse.py +124 -124
  55. dtlpy/entities/annotation_definitions/free_text.py +62 -62
  56. dtlpy/entities/annotation_definitions/gis.py +69 -69
  57. dtlpy/entities/annotation_definitions/note.py +139 -139
  58. dtlpy/entities/annotation_definitions/point.py +117 -117
  59. dtlpy/entities/annotation_definitions/polygon.py +182 -182
  60. dtlpy/entities/annotation_definitions/polyline.py +111 -111
  61. dtlpy/entities/annotation_definitions/pose.py +92 -92
  62. dtlpy/entities/annotation_definitions/ref_image.py +86 -86
  63. dtlpy/entities/annotation_definitions/segmentation.py +240 -240
  64. dtlpy/entities/annotation_definitions/subtitle.py +34 -34
  65. dtlpy/entities/annotation_definitions/text.py +85 -85
  66. dtlpy/entities/annotation_definitions/undefined_annotation.py +74 -74
  67. dtlpy/entities/app.py +220 -220
  68. dtlpy/entities/app_module.py +107 -107
  69. dtlpy/entities/artifact.py +174 -174
  70. dtlpy/entities/assignment.py +399 -399
  71. dtlpy/entities/base_entity.py +214 -214
  72. dtlpy/entities/bot.py +113 -113
  73. dtlpy/entities/codebase.py +296 -296
  74. dtlpy/entities/collection.py +38 -38
  75. dtlpy/entities/command.py +169 -169
  76. dtlpy/entities/compute.py +442 -442
  77. dtlpy/entities/dataset.py +1285 -1285
  78. dtlpy/entities/directory_tree.py +44 -44
  79. dtlpy/entities/dpk.py +470 -470
  80. dtlpy/entities/driver.py +222 -222
  81. dtlpy/entities/execution.py +397 -397
  82. dtlpy/entities/feature.py +124 -124
  83. dtlpy/entities/feature_set.py +145 -145
  84. dtlpy/entities/filters.py +641 -641
  85. dtlpy/entities/gis_item.py +107 -107
  86. dtlpy/entities/integration.py +184 -184
  87. dtlpy/entities/item.py +953 -953
  88. dtlpy/entities/label.py +123 -123
  89. dtlpy/entities/links.py +85 -85
  90. dtlpy/entities/message.py +175 -175
  91. dtlpy/entities/model.py +694 -691
  92. dtlpy/entities/node.py +1005 -1005
  93. dtlpy/entities/ontology.py +803 -803
  94. dtlpy/entities/organization.py +287 -287
  95. dtlpy/entities/package.py +657 -657
  96. dtlpy/entities/package_defaults.py +5 -5
  97. dtlpy/entities/package_function.py +185 -185
  98. dtlpy/entities/package_module.py +113 -113
  99. dtlpy/entities/package_slot.py +118 -118
  100. dtlpy/entities/paged_entities.py +290 -267
  101. dtlpy/entities/pipeline.py +593 -593
  102. dtlpy/entities/pipeline_execution.py +279 -279
  103. dtlpy/entities/project.py +394 -394
  104. dtlpy/entities/prompt_item.py +499 -499
  105. dtlpy/entities/recipe.py +301 -301
  106. dtlpy/entities/reflect_dict.py +102 -102
  107. dtlpy/entities/resource_execution.py +138 -138
  108. dtlpy/entities/service.py +958 -958
  109. dtlpy/entities/service_driver.py +117 -117
  110. dtlpy/entities/setting.py +294 -294
  111. dtlpy/entities/task.py +491 -491
  112. dtlpy/entities/time_series.py +143 -143
  113. dtlpy/entities/trigger.py +426 -426
  114. dtlpy/entities/user.py +118 -118
  115. dtlpy/entities/webhook.py +124 -124
  116. dtlpy/examples/__init__.py +19 -19
  117. dtlpy/examples/add_labels.py +135 -135
  118. dtlpy/examples/add_metadata_to_item.py +21 -21
  119. dtlpy/examples/annotate_items_using_model.py +65 -65
  120. dtlpy/examples/annotate_video_using_model_and_tracker.py +75 -75
  121. dtlpy/examples/annotations_convert_to_voc.py +9 -9
  122. dtlpy/examples/annotations_convert_to_yolo.py +9 -9
  123. dtlpy/examples/convert_annotation_types.py +51 -51
  124. dtlpy/examples/converter.py +143 -143
  125. dtlpy/examples/copy_annotations.py +22 -22
  126. dtlpy/examples/copy_folder.py +31 -31
  127. dtlpy/examples/create_annotations.py +51 -51
  128. dtlpy/examples/create_video_annotations.py +83 -83
  129. dtlpy/examples/delete_annotations.py +26 -26
  130. dtlpy/examples/filters.py +113 -113
  131. dtlpy/examples/move_item.py +23 -23
  132. dtlpy/examples/play_video_annotation.py +13 -13
  133. dtlpy/examples/show_item_and_mask.py +53 -53
  134. dtlpy/examples/triggers.py +49 -49
  135. dtlpy/examples/upload_batch_of_items.py +20 -20
  136. dtlpy/examples/upload_items_and_custom_format_annotations.py +55 -55
  137. dtlpy/examples/upload_items_with_modalities.py +43 -43
  138. dtlpy/examples/upload_segmentation_annotations_from_mask_image.py +44 -44
  139. dtlpy/examples/upload_yolo_format_annotations.py +70 -70
  140. dtlpy/exceptions.py +125 -125
  141. dtlpy/miscellaneous/__init__.py +20 -20
  142. dtlpy/miscellaneous/dict_differ.py +95 -95
  143. dtlpy/miscellaneous/git_utils.py +217 -217
  144. dtlpy/miscellaneous/json_utils.py +14 -14
  145. dtlpy/miscellaneous/list_print.py +105 -105
  146. dtlpy/miscellaneous/zipping.py +130 -130
  147. dtlpy/ml/__init__.py +20 -20
  148. dtlpy/ml/base_feature_extractor_adapter.py +27 -27
  149. dtlpy/ml/base_model_adapter.py +945 -940
  150. dtlpy/ml/metrics.py +461 -461
  151. dtlpy/ml/predictions_utils.py +274 -274
  152. dtlpy/ml/summary_writer.py +57 -57
  153. dtlpy/ml/train_utils.py +60 -60
  154. dtlpy/new_instance.py +252 -252
  155. dtlpy/repositories/__init__.py +56 -56
  156. dtlpy/repositories/analytics.py +85 -85
  157. dtlpy/repositories/annotations.py +916 -916
  158. dtlpy/repositories/apps.py +383 -383
  159. dtlpy/repositories/artifacts.py +452 -452
  160. dtlpy/repositories/assignments.py +599 -599
  161. dtlpy/repositories/bots.py +213 -213
  162. dtlpy/repositories/codebases.py +559 -559
  163. dtlpy/repositories/collections.py +332 -348
  164. dtlpy/repositories/commands.py +158 -158
  165. dtlpy/repositories/compositions.py +61 -61
  166. dtlpy/repositories/computes.py +434 -406
  167. dtlpy/repositories/datasets.py +1291 -1291
  168. dtlpy/repositories/downloader.py +895 -895
  169. dtlpy/repositories/dpks.py +433 -433
  170. dtlpy/repositories/drivers.py +266 -266
  171. dtlpy/repositories/executions.py +817 -817
  172. dtlpy/repositories/feature_sets.py +226 -226
  173. dtlpy/repositories/features.py +238 -238
  174. dtlpy/repositories/integrations.py +484 -484
  175. dtlpy/repositories/items.py +909 -915
  176. dtlpy/repositories/messages.py +94 -94
  177. dtlpy/repositories/models.py +877 -867
  178. dtlpy/repositories/nodes.py +80 -80
  179. dtlpy/repositories/ontologies.py +511 -511
  180. dtlpy/repositories/organizations.py +525 -525
  181. dtlpy/repositories/packages.py +1941 -1941
  182. dtlpy/repositories/pipeline_executions.py +448 -448
  183. dtlpy/repositories/pipelines.py +642 -642
  184. dtlpy/repositories/projects.py +539 -539
  185. dtlpy/repositories/recipes.py +399 -399
  186. dtlpy/repositories/resource_executions.py +137 -137
  187. dtlpy/repositories/schema.py +120 -120
  188. dtlpy/repositories/service_drivers.py +213 -213
  189. dtlpy/repositories/services.py +1704 -1704
  190. dtlpy/repositories/settings.py +339 -339
  191. dtlpy/repositories/tasks.py +1124 -1124
  192. dtlpy/repositories/times_series.py +278 -278
  193. dtlpy/repositories/triggers.py +536 -536
  194. dtlpy/repositories/upload_element.py +257 -257
  195. dtlpy/repositories/uploader.py +651 -651
  196. dtlpy/repositories/webhooks.py +249 -249
  197. dtlpy/services/__init__.py +22 -22
  198. dtlpy/services/aihttp_retry.py +131 -131
  199. dtlpy/services/api_client.py +1782 -1782
  200. dtlpy/services/api_reference.py +40 -40
  201. dtlpy/services/async_utils.py +133 -133
  202. dtlpy/services/calls_counter.py +44 -44
  203. dtlpy/services/check_sdk.py +68 -68
  204. dtlpy/services/cookie.py +115 -115
  205. dtlpy/services/create_logger.py +156 -156
  206. dtlpy/services/events.py +84 -84
  207. dtlpy/services/logins.py +235 -235
  208. dtlpy/services/reporter.py +256 -256
  209. dtlpy/services/service_defaults.py +91 -91
  210. dtlpy/utilities/__init__.py +20 -20
  211. dtlpy/utilities/annotations/__init__.py +16 -16
  212. dtlpy/utilities/annotations/annotation_converters.py +269 -269
  213. dtlpy/utilities/base_package_runner.py +264 -264
  214. dtlpy/utilities/converter.py +1650 -1650
  215. dtlpy/utilities/dataset_generators/__init__.py +1 -1
  216. dtlpy/utilities/dataset_generators/dataset_generator.py +670 -670
  217. dtlpy/utilities/dataset_generators/dataset_generator_tensorflow.py +23 -23
  218. dtlpy/utilities/dataset_generators/dataset_generator_torch.py +21 -21
  219. dtlpy/utilities/local_development/__init__.py +1 -1
  220. dtlpy/utilities/local_development/local_session.py +179 -179
  221. dtlpy/utilities/reports/__init__.py +2 -2
  222. dtlpy/utilities/reports/figures.py +343 -343
  223. dtlpy/utilities/reports/report.py +71 -71
  224. dtlpy/utilities/videos/__init__.py +17 -17
  225. dtlpy/utilities/videos/video_player.py +598 -598
  226. dtlpy/utilities/videos/videos.py +470 -470
  227. {dtlpy-1.113.10.data → dtlpy-1.114.13.data}/scripts/dlp +1 -1
  228. dtlpy-1.114.13.data/scripts/dlp.bat +2 -0
  229. {dtlpy-1.113.10.data → dtlpy-1.114.13.data}/scripts/dlp.py +128 -128
  230. {dtlpy-1.113.10.dist-info → dtlpy-1.114.13.dist-info}/LICENSE +200 -200
  231. {dtlpy-1.113.10.dist-info → dtlpy-1.114.13.dist-info}/METADATA +172 -172
  232. dtlpy-1.114.13.dist-info/RECORD +240 -0
  233. {dtlpy-1.113.10.dist-info → dtlpy-1.114.13.dist-info}/WHEEL +1 -1
  234. tests/features/environment.py +551 -550
  235. dtlpy-1.113.10.data/scripts/dlp.bat +0 -2
  236. dtlpy-1.113.10.dist-info/RECORD +0 -244
  237. tests/assets/__init__.py +0 -0
  238. tests/assets/models_flow/__init__.py +0 -0
  239. tests/assets/models_flow/failedmain.py +0 -52
  240. tests/assets/models_flow/main.py +0 -62
  241. tests/assets/models_flow/main_model.py +0 -54
  242. {dtlpy-1.113.10.dist-info → dtlpy-1.114.13.dist-info}/entry_points.txt +0 -0
  243. {dtlpy-1.113.10.dist-info → dtlpy-1.114.13.dist-info}/top_level.txt +0 -0
@@ -1,500 +1,500 @@
1
- import requests
2
- import logging
3
- import base64
4
- import enum
5
- import json
6
- import io
7
- import os
8
- from typing import List, Optional
9
-
10
- from concurrent.futures import ThreadPoolExecutor
11
- from .. import entities, repositories
12
- from dtlpy.services.api_client import client as client_api
13
-
14
- logger = logging.getLogger(name='dtlpy')
15
-
16
-
17
- class PromptType(str, enum.Enum):
18
- TEXT = 'application/text'
19
- IMAGE = 'image/*'
20
- AUDIO = 'audio/*'
21
- VIDEO = 'video/*'
22
- METADATA = 'metadata'
23
-
24
-
25
- class Prompt:
26
- def __init__(self, key, role='user'):
27
- """
28
- Create a single Prompt. Prompt can contain multiple mimetype elements, e.g. text sentence and an image.
29
- :param key: unique identifier of the prompt in the item
30
- """
31
- self.key = key
32
- self.elements = list()
33
- # to avoid broken stream of json files - DAT-75653
34
- client_api.default_headers['x-dl-sanitize'] = '0'
35
- self._items = repositories.Items(client_api=client_api)
36
- self.metadata = {'role': role}
37
-
38
- def add_element(self, value, mimetype='application/text'):
39
- """
40
-
41
- :param value: url or string of the input
42
- :param mimetype: mimetype of the input. options: `text`, `image/*`, `video/*`, `audio/*`
43
- :return:
44
- """
45
- allowed_prompt_types = [prompt_type for prompt_type in PromptType]
46
- if mimetype not in allowed_prompt_types:
47
- raise ValueError(f'Invalid mimetype: {mimetype}. Allowed values: {allowed_prompt_types}')
48
- if mimetype == PromptType.METADATA and isinstance(value, dict):
49
- self.metadata.update(value)
50
- else:
51
- self.elements.append({'mimetype': mimetype,
52
- 'value': value})
53
-
54
- def to_json(self):
55
- """
56
- Convert Prompt entity to the item json
57
-
58
- :return:
59
- """
60
- elements_json = [
61
- {
62
- "mimetype": e['mimetype'],
63
- "value": e['value'],
64
- } for e in self.elements if not e['mimetype'] == PromptType.METADATA
65
- ]
66
- elements_json.append({
67
- "mimetype": PromptType.METADATA,
68
- "value": self.metadata
69
- })
70
- return {
71
- self.key: elements_json
72
- }
73
-
74
- def _convert_stream_to_binary(self, image_url: str):
75
- """
76
- Convert a stream to binary
77
- :param image_url: dataloop image stream url
78
- :return: binary object
79
- """
80
- image_buffer = None
81
- if '.' in image_url and 'dataloop.ai' not in image_url:
82
- # URL and not DL item stream
83
- try:
84
- response = requests.get(image_url, stream=True)
85
- response.raise_for_status() # Raise an exception for bad status codes
86
-
87
- # Check for valid image content type
88
- if response.headers["Content-Type"].startswith("image/"):
89
- # Read the image data in chunks to avoid loading large images in memory
90
- image_buffer = b"".join(chunk for chunk in response.iter_content(1024))
91
- except requests.exceptions.RequestException as e:
92
- logger.error(f"Failed to download image from URL: {image_url}, error: {e}")
93
-
94
- elif '.' in image_url and 'stream' in image_url:
95
- # DL Stream URL
96
- item_id = image_url.split("/stream")[0].split("/items/")[-1]
97
- image_buffer = self._items.get(item_id=item_id).download(save_locally=False).getvalue()
98
- else:
99
- # DL item ID
100
- image_buffer = self._items.get(item_id=image_url).download(save_locally=False).getvalue()
101
-
102
- if image_buffer is not None:
103
- encoded_image = base64.b64encode(image_buffer).decode()
104
- else:
105
- logger.error(f'Invalid image url: {image_url}')
106
- return None
107
-
108
- return f'data:image/jpeg;base64,{encoded_image}'
109
-
110
- def messages(self):
111
- """
112
- return a list of messages in the prompt item,
113
- messages are returned following the openai SDK format https://platform.openai.com/docs/guides/vision
114
- """
115
- messages = []
116
- for element in self.elements:
117
- if element['mimetype'] == PromptType.TEXT:
118
- data = {
119
- "type": "text",
120
- "text": element['value']
121
- }
122
- messages.append(data)
123
- elif element['mimetype'] == PromptType.IMAGE:
124
- image_url = self._convert_stream_to_binary(element['value'])
125
- data = {
126
- "type": "image_url",
127
- "image_url": {
128
- "url": image_url
129
- }
130
- }
131
- messages.append(data)
132
- elif element['mimetype'] == PromptType.AUDIO:
133
- raise NotImplementedError('Audio prompt is not supported yet')
134
- elif element['mimetype'] == PromptType.VIDEO:
135
- raise NotImplementedError('Video prompt is not supported yet')
136
- else:
137
- raise ValueError(f'Invalid mimetype: {element["mimetype"]}')
138
- return messages, self.key
139
-
140
-
141
- class PromptItem:
142
- def __init__(self, name, item: entities.Item = None, role_mapping=None):
143
- if role_mapping is None:
144
- role_mapping = {'user': 'item',
145
- 'assistant': 'annotation'}
146
- if not isinstance(role_mapping, dict):
147
- raise ValueError(f'input role_mapping must be dict. type: {type(role_mapping)}')
148
- self.role_mapping = role_mapping
149
- # prompt item name
150
- self.name = name
151
- # list of user prompts in the prompt item
152
- self.prompts = list()
153
- self.assistant_prompts = list()
154
- # list of assistant (annotations) prompts in the prompt item
155
- # Dataloop Item
156
- self._item: entities.Item = item
157
- self._messages = []
158
- self._annotations: entities.AnnotationCollection = None
159
- if item is not None:
160
- if 'json' not in item.mimetype or item.system.get('shebang', dict()).get('dltype') != 'prompt':
161
- raise ValueError('Expecting a json item with system.shebang.dltype = prompt')
162
- self._items = item.items
163
- self.fetch()
164
- else:
165
- self._items = repositories.Items(client_api=client_api)
166
-
167
- # to avoid broken stream of json files - DAT-75653
168
- self._items._client_api.default_headers['x-dl-sanitize'] = '0'
169
-
170
- @classmethod
171
- def from_messages(cls, messages: list):
172
- ...
173
-
174
- @classmethod
175
- def from_item(cls, item: entities.Item):
176
- """
177
- Load a prompt item from the platform
178
- :param item : Item object
179
- :return: PromptItem object
180
- """
181
- if 'json' not in item.mimetype or item.system.get('shebang', dict()).get('dltype') != 'prompt':
182
- raise ValueError('Expecting a json item with system.shebang.dltype = prompt')
183
- return cls(name=item.name, item=item)
184
-
185
- @classmethod
186
- def from_local_file(cls, filepath):
187
- """
188
- Create a new prompt item from a file
189
- :param filepath: path to the file
190
- :return: PromptItem object
191
- """
192
- if os.path.exists(filepath) is False:
193
- raise FileNotFoundError(f'File does not exists: {filepath}')
194
- if 'json' not in os.path.splitext(filepath)[-1]:
195
- raise ValueError(f'Expected path to json item, got {os.path.splitext(filepath)[-1]}')
196
- prompt_item = cls(name=filepath)
197
- with open(filepath, 'r', encoding='utf-8') as f:
198
- data = json.load(f)
199
- prompt_item.prompts = prompt_item._load_item_prompts(data=data)
200
- return prompt_item
201
-
202
- @staticmethod
203
- def _load_item_prompts(data):
204
- prompts = list()
205
- for prompt_key, prompt_elements in data.get('prompts', dict()).items():
206
- content = list()
207
- for element in prompt_elements:
208
- content.append({'value': element.get('value', dict()),
209
- 'mimetype': element['mimetype']})
210
- prompt = Prompt(key=prompt_key, role="user")
211
- for element in content:
212
- prompt.add_element(value=element.get('value', ''),
213
- mimetype=element.get('mimetype', PromptType.TEXT))
214
- prompts.append(prompt)
215
- return prompts
216
-
217
- @staticmethod
218
- def _load_annotations_prompts(annotations: entities.AnnotationCollection):
219
- """
220
- Get all the annotations in the item for the assistant messages
221
- """
222
- # clearing the assistant prompts from previous annotations that might not belong
223
- assistant_prompts = list()
224
- for annotation in annotations:
225
- prompt_id = annotation.metadata.get('system', dict()).get('promptId', None)
226
- model_info = annotation.metadata.get('user', dict()).get('model', dict())
227
- annotation_id = annotation.id
228
- if annotation.type == 'ref_image':
229
- prompt = Prompt(key=prompt_id, role='assistant')
230
- prompt.add_element(value=annotation.annotation_definition.coordinates.get('ref'),
231
- mimetype=PromptType.IMAGE)
232
- elif annotation.type == 'text':
233
- prompt = Prompt(key=prompt_id, role='assistant')
234
- prompt.add_element(value=annotation.annotation_definition.coordinates,
235
- mimetype=PromptType.TEXT)
236
- else:
237
- raise ValueError(f"Unsupported annotation type: {annotation.type}")
238
-
239
- prompt.add_element(value={'id': annotation_id,
240
- 'model_info': model_info},
241
- mimetype=PromptType.METADATA)
242
- assistant_prompts.append(prompt)
243
- return assistant_prompts
244
-
245
- def to_json(self):
246
- """
247
- Convert the entity to a platform item.
248
-
249
- :return:
250
- """
251
- prompts_json = {
252
- "shebang": "dataloop",
253
- "metadata": {
254
- "dltype": 'prompt'
255
- },
256
- "prompts": {}
257
- }
258
- for prompt in self.prompts:
259
- for prompt_key, prompt_values in prompt.to_json().items():
260
- prompts_json["prompts"][prompt_key] = prompt_values
261
- return prompts_json
262
-
263
- def to_messages(self, model_name=None, include_assistant=True):
264
- all_prompts_messages = dict()
265
- for prompt in self.prompts:
266
- if prompt.key not in all_prompts_messages:
267
- all_prompts_messages[prompt.key] = list()
268
- prompt_messages, prompt_key = prompt.messages()
269
- messages = {
270
- 'role': prompt.metadata.get('role', 'user'),
271
- 'content': prompt_messages
272
- }
273
- all_prompts_messages[prompt.key].append(messages)
274
- if include_assistant is True:
275
- # reload to filer model annotations
276
- for prompt in self.assistant_prompts:
277
- prompt_model_name = prompt.metadata.get('model_info', dict()).get('name')
278
- if model_name is not None and prompt_model_name != model_name:
279
- continue
280
- if prompt.key not in all_prompts_messages:
281
- logger.warning(
282
- f'Prompt key {prompt.key} is not found in the user prompts, skipping Assistant prompt')
283
- continue
284
- prompt_messages, prompt_key = prompt.messages()
285
- assistant_messages = {
286
- 'role': 'assistant',
287
- 'content': prompt_messages
288
- }
289
- all_prompts_messages[prompt.key].append(assistant_messages)
290
- res = list()
291
- for prompts in all_prompts_messages.values():
292
- for prompt in prompts:
293
- res.append(prompt)
294
- self._messages = res
295
- return self._messages
296
-
297
- def to_bytes_io(self):
298
- # Used for item upload, do not delete
299
- byte_io = io.BytesIO()
300
- byte_io.name = self.name
301
- byte_io.write(json.dumps(self.to_json()).encode())
302
- byte_io.seek(0)
303
- return byte_io
304
-
305
- def fetch(self):
306
- if self._item is None:
307
- raise ValueError('Missing item, nothing to fetch..')
308
- self._item = self._items.get(item_id=self._item.id)
309
- self._annotations = self._item.annotations.list()
310
- self.prompts = self._load_item_prompts(data=json.load(self._item.download(save_locally=False)))
311
- self.assistant_prompts = self._load_annotations_prompts(self._annotations)
312
-
313
- def build_context(self, nearest_items, add_metadata=None) -> str:
314
- """
315
- Create a context stream from nearest items list.
316
- add_metadata is a list of location in the item.metadata to add to the context, for instance ['system.document.source']
317
- :param nearest_items: list of item ids
318
- :param add_metadata: list of metadata location to add metadata to context
319
- :return:
320
- """
321
- if add_metadata is None:
322
- add_metadata = list()
323
-
324
- def stream_single(w_id):
325
- context_item = self._items.get(item_id=w_id)
326
- buf = context_item.download(save_locally=False)
327
- text = buf.read().decode(encoding='utf-8')
328
- m = ""
329
- for path in add_metadata:
330
- parts = path.split('.')
331
- value = context_item.metadata
332
- part = ""
333
- for part in parts:
334
- if isinstance(value, dict):
335
- value = value.get(part)
336
- else:
337
- value = ""
338
-
339
- m += f"{part}:{value}\n"
340
- return text, m
341
-
342
- pool = ThreadPoolExecutor(max_workers=32)
343
- context = ""
344
- if len(nearest_items) > 0:
345
- # build context
346
- results = pool.map(stream_single, nearest_items)
347
- for res in results:
348
- context += f"\n<source>\n{res[1]}\n</source>\n<text>\n{res[0]}\n</text>"
349
- return context
350
-
351
- def add(self,
352
- message: dict,
353
- prompt_key: str = None,
354
- model_info: dict = None):
355
- """
356
- add a prompt to the prompt item
357
- prompt: a dictionary. keys are prompt message id, values are prompt messages
358
- responses: a list of annotations representing responses to the prompt
359
-
360
- :param message:
361
- :param prompt_key:
362
- :param model_info:
363
- :return:
364
- """
365
- role = message.get('role', 'user')
366
- content = message.get('content', list())
367
-
368
- if self.role_mapping.get(role, 'item') == 'item':
369
- if prompt_key is None:
370
- prompt_key = str(len(self.prompts) + 1)
371
- # for new prompt we need a new key
372
- prompt = Prompt(key=prompt_key, role=role)
373
- for element in content:
374
- prompt.add_element(value=element.get('value', ''),
375
- mimetype=element.get('mimetype', PromptType.TEXT))
376
-
377
- # create new prompt and add to prompts
378
- self.prompts.append(prompt)
379
- if self._item is not None:
380
- self._item._Item__update_item_binary(_json=self.to_json())
381
- else:
382
- if prompt_key is None:
383
- prompt_key = str(len(self.prompts))
384
- assistant_message = content[0]
385
- assistant_mimetype = assistant_message.get('mimetype', PromptType.TEXT)
386
- uploaded_annotation = None
387
-
388
- # find if prompt
389
- if model_info is None:
390
- # dont search for existing if there's no model information
391
- existing_prompt = None
392
- else:
393
- existing_prompts = list()
394
- for prompt in self.assistant_prompts:
395
- prompt_id = prompt.key
396
- model_name = prompt.metadata.get('model_info', dict()).get('name')
397
- if prompt_id == prompt_key and model_name == model_info.get('name'):
398
- # TODO how to handle multiple annotations
399
- existing_prompts.append(prompt)
400
- if len(existing_prompts) > 1:
401
- assert False, "shouldn't be here! more than 1 annotation for a single model"
402
- elif len(existing_prompts) == 1:
403
- # found model annotation to upload
404
- existing_prompt = existing_prompts[0]
405
- else:
406
- # no annotation found
407
- existing_prompt = None
408
-
409
- if existing_prompt is None:
410
- prompt = Prompt(key=prompt_key)
411
- if assistant_mimetype == PromptType.TEXT:
412
- annotation_definition = entities.FreeText(text=assistant_message.get('value'))
413
- prompt.add_element(value=annotation_definition.to_coordinates(None),
414
- mimetype=PromptType.TEXT)
415
- elif assistant_mimetype == PromptType.IMAGE:
416
- annotation_definition = entities.RefImage(ref=assistant_message.get('value'))
417
- prompt.add_element(value=annotation_definition.to_coordinates(None).get('ref'),
418
- mimetype=PromptType.IMAGE)
419
- else:
420
- raise NotImplementedError('Only images of mimetype image and text are supported')
421
- metadata = {'system': {'promptId': prompt_key},
422
- 'user': {'model': model_info}}
423
- prompt.add_element(mimetype=PromptType.METADATA,
424
- value={"model_info": model_info})
425
-
426
- existing_annotation = entities.Annotation.new(item=self._item,
427
- metadata=metadata,
428
- annotation_definition=annotation_definition)
429
- uploaded_annotation = existing_annotation.upload()
430
- prompt.add_element(mimetype=PromptType.METADATA,
431
- value={"id": uploaded_annotation.id})
432
- existing_prompt = prompt
433
- self.assistant_prompts.append(prompt)
434
-
435
- existing_prompt_element = [element for element in existing_prompt.elements if
436
- element['mimetype'] != PromptType.METADATA][-1]
437
- existing_prompt_element['value'] = assistant_message.get('value')
438
- if uploaded_annotation is None:
439
- # Creating annotation with old dict to match platform dict
440
- annotation_definition = entities.FreeText(text='')
441
- metadata = {'system': {'promptId': prompt_key},
442
- 'user': {'model': existing_prompt.metadata.get('model_info')}}
443
- annotation = entities.Annotation.new(item=self._item,
444
- metadata=metadata,
445
- annotation_definition=annotation_definition
446
- )
447
- annotation.id = existing_prompt.metadata['id']
448
- # set the platform dict to match the old annotation for the dict difference check, otherwise it won't
449
- # update
450
- annotation._platform_dict = annotation.to_json()
451
- # update the annotation with the new text
452
- annotation.annotation_definition.text = existing_prompt_element['value']
453
- self._item.annotations.update(annotation)
454
-
455
- def update(self):
456
- """
457
- Update the prompt item in the platform.
458
- """
459
- if self._item is not None:
460
- self._item._Item__update_item_binary(_json=self.to_json())
461
- self._item = self._item.update()
462
- else:
463
- raise ValueError('Cannot update PromptItem without an item.')
464
-
465
- # Properties
466
- @property
467
- def item(self) -> Optional['entities.Item']:
468
- """
469
- Get the underlying Item object.
470
-
471
- :return: The Item object associated with this PromptItem, or None.
472
- :rtype: Optional[dtlpy.entities.Item]
473
- """
474
- return self._item
475
-
476
- @item.setter
477
- def item(self, item: Optional['entities.Item']):
478
- """
479
- Set the underlying Item object.
480
-
481
- :param item: The Item object to associate with this PromptItem, or None.
482
- :type item: Optional[dtlpy.entities.Item]
483
- """
484
- if item is not None and not isinstance(item, entities.Item):
485
- raise ValueError(f"Expected dtlpy.entities.Item or None, got {type(item)}")
486
- self._item = item
487
-
488
-
489
- @property
490
- def metadata(self) -> dict:
491
- """
492
- Get the metadata from the underlying Item object.
493
-
494
- :return: Metadata dictionary from the item, or empty dict if no item exists.
495
- :rtype: dict
496
- """
497
- if self._item is not None:
498
- return self._item.metadata
499
- else:
1
+ import requests
2
+ import logging
3
+ import base64
4
+ import enum
5
+ import json
6
+ import io
7
+ import os
8
+ from typing import List, Optional
9
+
10
+ from concurrent.futures import ThreadPoolExecutor
11
+ from .. import entities, repositories
12
+ from dtlpy.services.api_client import client as client_api
13
+
14
+ logger = logging.getLogger(name='dtlpy')
15
+
16
+
17
+ class PromptType(str, enum.Enum):
18
+ TEXT = 'application/text'
19
+ IMAGE = 'image/*'
20
+ AUDIO = 'audio/*'
21
+ VIDEO = 'video/*'
22
+ METADATA = 'metadata'
23
+
24
+
25
+ class Prompt:
26
+ def __init__(self, key, role='user'):
27
+ """
28
+ Create a single Prompt. Prompt can contain multiple mimetype elements, e.g. text sentence and an image.
29
+ :param key: unique identifier of the prompt in the item
30
+ """
31
+ self.key = key
32
+ self.elements = list()
33
+ # to avoid broken stream of json files - DAT-75653
34
+ client_api.default_headers['x-dl-sanitize'] = '0'
35
+ self._items = repositories.Items(client_api=client_api)
36
+ self.metadata = {'role': role}
37
+
38
+ def add_element(self, value, mimetype='application/text'):
39
+ """
40
+
41
+ :param value: url or string of the input
42
+ :param mimetype: mimetype of the input. options: `text`, `image/*`, `video/*`, `audio/*`
43
+ :return:
44
+ """
45
+ allowed_prompt_types = [prompt_type for prompt_type in PromptType]
46
+ if mimetype not in allowed_prompt_types:
47
+ raise ValueError(f'Invalid mimetype: {mimetype}. Allowed values: {allowed_prompt_types}')
48
+ if mimetype == PromptType.METADATA and isinstance(value, dict):
49
+ self.metadata.update(value)
50
+ else:
51
+ self.elements.append({'mimetype': mimetype,
52
+ 'value': value})
53
+
54
+ def to_json(self):
55
+ """
56
+ Convert Prompt entity to the item json
57
+
58
+ :return:
59
+ """
60
+ elements_json = [
61
+ {
62
+ "mimetype": e['mimetype'],
63
+ "value": e['value'],
64
+ } for e in self.elements if not e['mimetype'] == PromptType.METADATA
65
+ ]
66
+ elements_json.append({
67
+ "mimetype": PromptType.METADATA,
68
+ "value": self.metadata
69
+ })
70
+ return {
71
+ self.key: elements_json
72
+ }
73
+
74
+ def _convert_stream_to_binary(self, image_url: str):
75
+ """
76
+ Convert a stream to binary
77
+ :param image_url: dataloop image stream url
78
+ :return: binary object
79
+ """
80
+ image_buffer = None
81
+ if '.' in image_url and 'dataloop.ai' not in image_url:
82
+ # URL and not DL item stream
83
+ try:
84
+ response = requests.get(image_url, stream=True)
85
+ response.raise_for_status() # Raise an exception for bad status codes
86
+
87
+ # Check for valid image content type
88
+ if response.headers["Content-Type"].startswith("image/"):
89
+ # Read the image data in chunks to avoid loading large images in memory
90
+ image_buffer = b"".join(chunk for chunk in response.iter_content(1024))
91
+ except requests.exceptions.RequestException as e:
92
+ logger.error(f"Failed to download image from URL: {image_url}, error: {e}")
93
+
94
+ elif '.' in image_url and 'stream' in image_url:
95
+ # DL Stream URL
96
+ item_id = image_url.split("/stream")[0].split("/items/")[-1]
97
+ image_buffer = self._items.get(item_id=item_id).download(save_locally=False).getvalue()
98
+ else:
99
+ # DL item ID
100
+ image_buffer = self._items.get(item_id=image_url).download(save_locally=False).getvalue()
101
+
102
+ if image_buffer is not None:
103
+ encoded_image = base64.b64encode(image_buffer).decode()
104
+ else:
105
+ logger.error(f'Invalid image url: {image_url}')
106
+ return None
107
+
108
+ return f'data:image/jpeg;base64,{encoded_image}'
109
+
110
+ def messages(self):
111
+ """
112
+ return a list of messages in the prompt item,
113
+ messages are returned following the openai SDK format https://platform.openai.com/docs/guides/vision
114
+ """
115
+ messages = []
116
+ for element in self.elements:
117
+ if element['mimetype'] == PromptType.TEXT:
118
+ data = {
119
+ "type": "text",
120
+ "text": element['value']
121
+ }
122
+ messages.append(data)
123
+ elif element['mimetype'] == PromptType.IMAGE:
124
+ image_url = self._convert_stream_to_binary(element['value'])
125
+ data = {
126
+ "type": "image_url",
127
+ "image_url": {
128
+ "url": image_url
129
+ }
130
+ }
131
+ messages.append(data)
132
+ elif element['mimetype'] == PromptType.AUDIO:
133
+ raise NotImplementedError('Audio prompt is not supported yet')
134
+ elif element['mimetype'] == PromptType.VIDEO:
135
+ raise NotImplementedError('Video prompt is not supported yet')
136
+ else:
137
+ raise ValueError(f'Invalid mimetype: {element["mimetype"]}')
138
+ return messages, self.key
139
+
140
+
141
+ class PromptItem:
142
+ def __init__(self, name, item: entities.Item = None, role_mapping=None):
143
+ if role_mapping is None:
144
+ role_mapping = {'user': 'item',
145
+ 'assistant': 'annotation'}
146
+ if not isinstance(role_mapping, dict):
147
+ raise ValueError(f'input role_mapping must be dict. type: {type(role_mapping)}')
148
+ self.role_mapping = role_mapping
149
+ # prompt item name
150
+ self.name = name
151
+ # list of user prompts in the prompt item
152
+ self.prompts = list()
153
+ self.assistant_prompts = list()
154
+ # list of assistant (annotations) prompts in the prompt item
155
+ # Dataloop Item
156
+ self._item: entities.Item = item
157
+ self._messages = []
158
+ self._annotations: entities.AnnotationCollection = None
159
+ if item is not None:
160
+ if 'json' not in item.mimetype or item.system.get('shebang', dict()).get('dltype') != 'prompt':
161
+ raise ValueError('Expecting a json item with system.shebang.dltype = prompt')
162
+ self._items = item.items
163
+ self.fetch()
164
+ else:
165
+ self._items = repositories.Items(client_api=client_api)
166
+
167
+ # to avoid broken stream of json files - DAT-75653
168
+ self._items._client_api.default_headers['x-dl-sanitize'] = '0'
169
+
170
+ @classmethod
171
+ def from_messages(cls, messages: list):
172
+ ...
173
+
174
+ @classmethod
175
+ def from_item(cls, item: entities.Item):
176
+ """
177
+ Load a prompt item from the platform
178
+ :param item : Item object
179
+ :return: PromptItem object
180
+ """
181
+ if 'json' not in item.mimetype or item.system.get('shebang', dict()).get('dltype') != 'prompt':
182
+ raise ValueError('Expecting a json item with system.shebang.dltype = prompt')
183
+ return cls(name=item.name, item=item)
184
+
185
+ @classmethod
186
+ def from_local_file(cls, filepath):
187
+ """
188
+ Create a new prompt item from a file
189
+ :param filepath: path to the file
190
+ :return: PromptItem object
191
+ """
192
+ if os.path.exists(filepath) is False:
193
+ raise FileNotFoundError(f'File does not exists: {filepath}')
194
+ if 'json' not in os.path.splitext(filepath)[-1]:
195
+ raise ValueError(f'Expected path to json item, got {os.path.splitext(filepath)[-1]}')
196
+ prompt_item = cls(name=filepath)
197
+ with open(filepath, 'r', encoding='utf-8') as f:
198
+ data = json.load(f)
199
+ prompt_item.prompts = prompt_item._load_item_prompts(data=data)
200
+ return prompt_item
201
+
202
+ @staticmethod
203
+ def _load_item_prompts(data):
204
+ prompts = list()
205
+ for prompt_key, prompt_elements in data.get('prompts', dict()).items():
206
+ content = list()
207
+ for element in prompt_elements:
208
+ content.append({'value': element.get('value', dict()),
209
+ 'mimetype': element['mimetype']})
210
+ prompt = Prompt(key=prompt_key, role="user")
211
+ for element in content:
212
+ prompt.add_element(value=element.get('value', ''),
213
+ mimetype=element.get('mimetype', PromptType.TEXT))
214
+ prompts.append(prompt)
215
+ return prompts
216
+
217
+ @staticmethod
218
+ def _load_annotations_prompts(annotations: entities.AnnotationCollection):
219
+ """
220
+ Get all the annotations in the item for the assistant messages
221
+ """
222
+ # clearing the assistant prompts from previous annotations that might not belong
223
+ assistant_prompts = list()
224
+ for annotation in annotations:
225
+ prompt_id = annotation.metadata.get('system', dict()).get('promptId', None)
226
+ model_info = annotation.metadata.get('user', dict()).get('model', dict())
227
+ annotation_id = annotation.id
228
+ if annotation.type == 'ref_image':
229
+ prompt = Prompt(key=prompt_id, role='assistant')
230
+ prompt.add_element(value=annotation.annotation_definition.coordinates.get('ref'),
231
+ mimetype=PromptType.IMAGE)
232
+ elif annotation.type == 'text':
233
+ prompt = Prompt(key=prompt_id, role='assistant')
234
+ prompt.add_element(value=annotation.annotation_definition.coordinates,
235
+ mimetype=PromptType.TEXT)
236
+ else:
237
+ raise ValueError(f"Unsupported annotation type: {annotation.type}")
238
+
239
+ prompt.add_element(value={'id': annotation_id,
240
+ 'model_info': model_info},
241
+ mimetype=PromptType.METADATA)
242
+ assistant_prompts.append(prompt)
243
+ return assistant_prompts
244
+
245
+ def to_json(self):
246
+ """
247
+ Convert the entity to a platform item.
248
+
249
+ :return:
250
+ """
251
+ prompts_json = {
252
+ "shebang": "dataloop",
253
+ "metadata": {
254
+ "dltype": 'prompt'
255
+ },
256
+ "prompts": {}
257
+ }
258
+ for prompt in self.prompts:
259
+ for prompt_key, prompt_values in prompt.to_json().items():
260
+ prompts_json["prompts"][prompt_key] = prompt_values
261
+ return prompts_json
262
+
263
+ def to_messages(self, model_name=None, include_assistant=True):
264
+ all_prompts_messages = dict()
265
+ for prompt in self.prompts:
266
+ if prompt.key not in all_prompts_messages:
267
+ all_prompts_messages[prompt.key] = list()
268
+ prompt_messages, prompt_key = prompt.messages()
269
+ messages = {
270
+ 'role': prompt.metadata.get('role', 'user'),
271
+ 'content': prompt_messages
272
+ }
273
+ all_prompts_messages[prompt.key].append(messages)
274
+ if include_assistant is True:
275
+ # reload to filer model annotations
276
+ for prompt in self.assistant_prompts:
277
+ prompt_model_name = prompt.metadata.get('model_info', dict()).get('name')
278
+ if model_name is not None and prompt_model_name != model_name:
279
+ continue
280
+ if prompt.key not in all_prompts_messages:
281
+ logger.warning(
282
+ f'Prompt key {prompt.key} is not found in the user prompts, skipping Assistant prompt')
283
+ continue
284
+ prompt_messages, prompt_key = prompt.messages()
285
+ assistant_messages = {
286
+ 'role': 'assistant',
287
+ 'content': prompt_messages
288
+ }
289
+ all_prompts_messages[prompt.key].append(assistant_messages)
290
+ res = list()
291
+ for prompts in all_prompts_messages.values():
292
+ for prompt in prompts:
293
+ res.append(prompt)
294
+ self._messages = res
295
+ return self._messages
296
+
297
+ def to_bytes_io(self):
298
+ # Used for item upload, do not delete
299
+ byte_io = io.BytesIO()
300
+ byte_io.name = self.name
301
+ byte_io.write(json.dumps(self.to_json()).encode())
302
+ byte_io.seek(0)
303
+ return byte_io
304
+
305
+ def fetch(self):
306
+ if self._item is None:
307
+ raise ValueError('Missing item, nothing to fetch..')
308
+ self._item = self._items.get(item_id=self._item.id)
309
+ self._annotations = self._item.annotations.list()
310
+ self.prompts = self._load_item_prompts(data=json.load(self._item.download(save_locally=False)))
311
+ self.assistant_prompts = self._load_annotations_prompts(self._annotations)
312
+
313
+ def build_context(self, nearest_items, add_metadata=None) -> str:
314
+ """
315
+ Create a context stream from nearest items list.
316
+ add_metadata is a list of location in the item.metadata to add to the context, for instance ['system.document.source']
317
+ :param nearest_items: list of item ids
318
+ :param add_metadata: list of metadata location to add metadata to context
319
+ :return:
320
+ """
321
+ if add_metadata is None:
322
+ add_metadata = list()
323
+
324
+ def stream_single(w_id):
325
+ context_item = self._items.get(item_id=w_id)
326
+ buf = context_item.download(save_locally=False)
327
+ text = buf.read().decode(encoding='utf-8')
328
+ m = ""
329
+ for path in add_metadata:
330
+ parts = path.split('.')
331
+ value = context_item.metadata
332
+ part = ""
333
+ for part in parts:
334
+ if isinstance(value, dict):
335
+ value = value.get(part)
336
+ else:
337
+ value = ""
338
+
339
+ m += f"{part}:{value}\n"
340
+ return text, m
341
+
342
+ pool = ThreadPoolExecutor(max_workers=32)
343
+ context = ""
344
+ if len(nearest_items) > 0:
345
+ # build context
346
+ results = pool.map(stream_single, nearest_items)
347
+ for res in results:
348
+ context += f"\n<source>\n{res[1]}\n</source>\n<text>\n{res[0]}\n</text>"
349
+ return context
350
+
351
+ def add(self,
352
+ message: dict,
353
+ prompt_key: str = None,
354
+ model_info: dict = None):
355
+ """
356
+ add a prompt to the prompt item
357
+ prompt: a dictionary. keys are prompt message id, values are prompt messages
358
+ responses: a list of annotations representing responses to the prompt
359
+
360
+ :param message:
361
+ :param prompt_key:
362
+ :param model_info:
363
+ :return:
364
+ """
365
+ role = message.get('role', 'user')
366
+ content = message.get('content', list())
367
+
368
+ if self.role_mapping.get(role, 'item') == 'item':
369
+ if prompt_key is None:
370
+ prompt_key = str(len(self.prompts) + 1)
371
+ # for new prompt we need a new key
372
+ prompt = Prompt(key=prompt_key, role=role)
373
+ for element in content:
374
+ prompt.add_element(value=element.get('value', ''),
375
+ mimetype=element.get('mimetype', PromptType.TEXT))
376
+
377
+ # create new prompt and add to prompts
378
+ self.prompts.append(prompt)
379
+ if self._item is not None:
380
+ self._item._Item__update_item_binary(_json=self.to_json())
381
+ else:
382
+ if prompt_key is None:
383
+ prompt_key = str(len(self.prompts))
384
+ assistant_message = content[0]
385
+ assistant_mimetype = assistant_message.get('mimetype', PromptType.TEXT)
386
+ uploaded_annotation = None
387
+
388
+ # find if prompt
389
+ if model_info is None:
390
+ # dont search for existing if there's no model information
391
+ existing_prompt = None
392
+ else:
393
+ existing_prompts = list()
394
+ for prompt in self.assistant_prompts:
395
+ prompt_id = prompt.key
396
+ model_name = prompt.metadata.get('model_info', dict()).get('name')
397
+ if prompt_id == prompt_key and model_name == model_info.get('name'):
398
+ # TODO how to handle multiple annotations
399
+ existing_prompts.append(prompt)
400
+ if len(existing_prompts) > 1:
401
+ assert False, "shouldn't be here! more than 1 annotation for a single model"
402
+ elif len(existing_prompts) == 1:
403
+ # found model annotation to upload
404
+ existing_prompt = existing_prompts[0]
405
+ else:
406
+ # no annotation found
407
+ existing_prompt = None
408
+
409
+ if existing_prompt is None:
410
+ prompt = Prompt(key=prompt_key)
411
+ if assistant_mimetype == PromptType.TEXT:
412
+ annotation_definition = entities.FreeText(text=assistant_message.get('value'))
413
+ prompt.add_element(value=annotation_definition.to_coordinates(None),
414
+ mimetype=PromptType.TEXT)
415
+ elif assistant_mimetype == PromptType.IMAGE:
416
+ annotation_definition = entities.RefImage(ref=assistant_message.get('value'))
417
+ prompt.add_element(value=annotation_definition.to_coordinates(None).get('ref'),
418
+ mimetype=PromptType.IMAGE)
419
+ else:
420
+ raise NotImplementedError('Only images of mimetype image and text are supported')
421
+ metadata = {'system': {'promptId': prompt_key},
422
+ 'user': {'model': model_info}}
423
+ prompt.add_element(mimetype=PromptType.METADATA,
424
+ value={"model_info": model_info})
425
+
426
+ existing_annotation = entities.Annotation.new(item=self._item,
427
+ metadata=metadata,
428
+ annotation_definition=annotation_definition)
429
+ uploaded_annotation = existing_annotation.upload()
430
+ prompt.add_element(mimetype=PromptType.METADATA,
431
+ value={"id": uploaded_annotation.id})
432
+ existing_prompt = prompt
433
+ self.assistant_prompts.append(prompt)
434
+
435
+ existing_prompt_element = [element for element in existing_prompt.elements if
436
+ element['mimetype'] != PromptType.METADATA][-1]
437
+ existing_prompt_element['value'] = assistant_message.get('value')
438
+ if uploaded_annotation is None:
439
+ # Creating annotation with old dict to match platform dict
440
+ annotation_definition = entities.FreeText(text='')
441
+ metadata = {'system': {'promptId': prompt_key},
442
+ 'user': {'model': existing_prompt.metadata.get('model_info')}}
443
+ annotation = entities.Annotation.new(item=self._item,
444
+ metadata=metadata,
445
+ annotation_definition=annotation_definition
446
+ )
447
+ annotation.id = existing_prompt.metadata['id']
448
+ # set the platform dict to match the old annotation for the dict difference check, otherwise it won't
449
+ # update
450
+ annotation._platform_dict = annotation.to_json()
451
+ # update the annotation with the new text
452
+ annotation.annotation_definition.text = existing_prompt_element['value']
453
+ self._item.annotations.update(annotation)
454
+
455
+ def update(self):
456
+ """
457
+ Update the prompt item in the platform.
458
+ """
459
+ if self._item is not None:
460
+ self._item._Item__update_item_binary(_json=self.to_json())
461
+ self._item = self._item.update()
462
+ else:
463
+ raise ValueError('Cannot update PromptItem without an item.')
464
+
465
+ # Properties
466
+ @property
467
+ def item(self) -> Optional['entities.Item']:
468
+ """
469
+ Get the underlying Item object.
470
+
471
+ :return: The Item object associated with this PromptItem, or None.
472
+ :rtype: Optional[dtlpy.entities.Item]
473
+ """
474
+ return self._item
475
+
476
+ @item.setter
477
+ def item(self, item: Optional['entities.Item']):
478
+ """
479
+ Set the underlying Item object.
480
+
481
+ :param item: The Item object to associate with this PromptItem, or None.
482
+ :type item: Optional[dtlpy.entities.Item]
483
+ """
484
+ if item is not None and not isinstance(item, entities.Item):
485
+ raise ValueError(f"Expected dtlpy.entities.Item or None, got {type(item)}")
486
+ self._item = item
487
+
488
+
489
+ @property
490
+ def metadata(self) -> dict:
491
+ """
492
+ Get the metadata from the underlying Item object.
493
+
494
+ :return: Metadata dictionary from the item, or empty dict if no item exists.
495
+ :rtype: dict
496
+ """
497
+ if self._item is not None:
498
+ return self._item.metadata
499
+ else:
500
500
  raise ValueError('No item found, cannot get metadata, to set item use prompt_item.item = item')