waldiez 0.4.7__py3-none-any.whl → 0.4.9__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.

Potentially problematic release.


This version of waldiez might be problematic. Click here for more details.

Files changed (248) hide show
  1. waldiez/__init__.py +5 -5
  2. waldiez/_version.py +1 -1
  3. waldiez/cli.py +97 -102
  4. waldiez/exporter.py +61 -19
  5. waldiez/exporting/__init__.py +25 -6
  6. waldiez/exporting/agent/__init__.py +7 -3
  7. waldiez/exporting/agent/code_execution.py +114 -0
  8. waldiez/exporting/agent/exporter.py +354 -0
  9. waldiez/exporting/agent/extras/__init__.py +15 -0
  10. waldiez/exporting/agent/extras/captain_agent_extras.py +315 -0
  11. waldiez/exporting/agent/extras/group/target.py +178 -0
  12. waldiez/exporting/agent/extras/group_manager_agent_extas.py +500 -0
  13. waldiez/exporting/agent/extras/group_member_extras.py +181 -0
  14. waldiez/exporting/agent/extras/handoffs/__init__.py +19 -0
  15. waldiez/exporting/agent/extras/handoffs/after_work.py +78 -0
  16. waldiez/exporting/agent/extras/handoffs/available.py +74 -0
  17. waldiez/exporting/agent/extras/handoffs/condition.py +158 -0
  18. waldiez/exporting/agent/extras/handoffs/handoff.py +171 -0
  19. waldiez/exporting/agent/extras/handoffs/target.py +189 -0
  20. waldiez/exporting/agent/extras/rag/__init__.py +10 -0
  21. waldiez/exporting/agent/{utils/rag_user/chroma_utils.py → extras/rag/chroma_extras.py} +37 -24
  22. waldiez/exporting/agent/{utils/rag_user/mongo_utils.py → extras/rag/mongo_extras.py} +10 -10
  23. waldiez/exporting/agent/{utils/rag_user/pgvector_utils.py → extras/rag/pgvector_extras.py} +13 -13
  24. waldiez/exporting/agent/{utils/rag_user/qdrant_utils.py → extras/rag/qdrant_extras.py} +13 -13
  25. waldiez/exporting/agent/{utils/rag_user/vector_db.py → extras/rag/vector_db_extras.py} +59 -46
  26. waldiez/exporting/agent/extras/rag_user_proxy_agent_extras.py +245 -0
  27. waldiez/exporting/agent/extras/reasoning_agent_extras.py +88 -0
  28. waldiez/exporting/agent/factory.py +95 -0
  29. waldiez/exporting/agent/processor.py +150 -0
  30. waldiez/exporting/agent/system_message.py +36 -0
  31. waldiez/exporting/agent/termination.py +50 -0
  32. waldiez/exporting/chats/__init__.py +7 -3
  33. waldiez/exporting/chats/exporter.py +97 -0
  34. waldiez/exporting/chats/factory.py +65 -0
  35. waldiez/exporting/chats/processor.py +226 -0
  36. waldiez/exporting/chats/utils/__init__.py +6 -5
  37. waldiez/exporting/chats/utils/common.py +11 -45
  38. waldiez/exporting/chats/utils/group.py +55 -0
  39. waldiez/exporting/chats/utils/nested.py +37 -52
  40. waldiez/exporting/chats/utils/sequential.py +72 -61
  41. waldiez/exporting/chats/utils/{single_chat.py → single.py} +48 -50
  42. waldiez/exporting/core/__init__.py +196 -0
  43. waldiez/exporting/core/constants.py +17 -0
  44. waldiez/exporting/core/content.py +69 -0
  45. waldiez/exporting/core/context.py +244 -0
  46. waldiez/exporting/core/enums.py +89 -0
  47. waldiez/exporting/core/errors.py +19 -0
  48. waldiez/exporting/core/exporter.py +390 -0
  49. waldiez/exporting/core/exporters.py +67 -0
  50. waldiez/exporting/core/extras/__init__.py +39 -0
  51. waldiez/exporting/core/extras/agent_extras/__init__.py +27 -0
  52. waldiez/exporting/core/extras/agent_extras/captain_extras.py +57 -0
  53. waldiez/exporting/core/extras/agent_extras/group_manager_extras.py +102 -0
  54. waldiez/exporting/core/extras/agent_extras/rag_user_extras.py +53 -0
  55. waldiez/exporting/core/extras/agent_extras/reasoning_extras.py +68 -0
  56. waldiez/exporting/core/extras/agent_extras/standard_extras.py +263 -0
  57. waldiez/exporting/core/extras/base.py +241 -0
  58. waldiez/exporting/core/extras/chat_extras.py +118 -0
  59. waldiez/exporting/core/extras/flow_extras.py +70 -0
  60. waldiez/exporting/core/extras/model_extras.py +73 -0
  61. waldiez/exporting/core/extras/path_resolver.py +93 -0
  62. waldiez/exporting/core/extras/serializer.py +138 -0
  63. waldiez/exporting/core/extras/tool_extras.py +82 -0
  64. waldiez/exporting/core/protocols.py +259 -0
  65. waldiez/exporting/core/result.py +705 -0
  66. waldiez/exporting/core/types.py +329 -0
  67. waldiez/exporting/core/utils/__init__.py +11 -0
  68. waldiez/exporting/core/utils/comment.py +33 -0
  69. waldiez/exporting/core/utils/llm_config.py +117 -0
  70. waldiez/exporting/core/validation.py +96 -0
  71. waldiez/exporting/flow/__init__.py +6 -2
  72. waldiez/exporting/flow/execution_generator.py +193 -0
  73. waldiez/exporting/flow/exporter.py +107 -0
  74. waldiez/exporting/flow/factory.py +94 -0
  75. waldiez/exporting/flow/file_generator.py +214 -0
  76. waldiez/exporting/flow/merger.py +387 -0
  77. waldiez/exporting/flow/orchestrator.py +411 -0
  78. waldiez/exporting/flow/utils/__init__.py +9 -36
  79. waldiez/exporting/flow/utils/common.py +206 -0
  80. waldiez/exporting/flow/utils/importing.py +373 -0
  81. waldiez/exporting/flow/utils/linting.py +200 -0
  82. waldiez/exporting/flow/utils/{logging_utils.py → logging.py} +23 -9
  83. waldiez/exporting/models/__init__.py +3 -1
  84. waldiez/exporting/models/exporter.py +233 -0
  85. waldiez/exporting/models/factory.py +66 -0
  86. waldiez/exporting/models/processor.py +139 -0
  87. waldiez/exporting/tools/__init__.py +11 -0
  88. waldiez/exporting/tools/exporter.py +207 -0
  89. waldiez/exporting/tools/factory.py +57 -0
  90. waldiez/exporting/tools/processor.py +248 -0
  91. waldiez/exporting/tools/registration.py +133 -0
  92. waldiez/io/__init__.py +128 -0
  93. waldiez/io/_ws.py +199 -0
  94. waldiez/io/models/__init__.py +60 -0
  95. waldiez/io/models/base.py +66 -0
  96. waldiez/io/models/constants.py +78 -0
  97. waldiez/io/models/content/__init__.py +23 -0
  98. waldiez/io/models/content/audio.py +43 -0
  99. waldiez/io/models/content/base.py +45 -0
  100. waldiez/io/models/content/file.py +43 -0
  101. waldiez/io/models/content/image.py +96 -0
  102. waldiez/io/models/content/text.py +37 -0
  103. waldiez/io/models/content/video.py +43 -0
  104. waldiez/io/models/user_input.py +269 -0
  105. waldiez/io/models/user_response.py +215 -0
  106. waldiez/io/mqtt.py +681 -0
  107. waldiez/io/redis.py +782 -0
  108. waldiez/io/structured.py +439 -0
  109. waldiez/io/utils.py +184 -0
  110. waldiez/io/ws.py +298 -0
  111. waldiez/logger.py +481 -0
  112. waldiez/models/__init__.py +108 -51
  113. waldiez/models/agents/__init__.py +34 -70
  114. waldiez/models/agents/agent/__init__.py +10 -4
  115. waldiez/models/agents/agent/agent.py +466 -65
  116. waldiez/models/agents/agent/agent_data.py +119 -47
  117. waldiez/models/agents/agent/agent_type.py +13 -2
  118. waldiez/models/agents/agent/code_execution.py +12 -12
  119. waldiez/models/agents/agent/human_input_mode.py +8 -0
  120. waldiez/models/agents/agent/{linked_skill.py → linked_tool.py} +7 -7
  121. waldiez/models/agents/agent/nested_chat.py +35 -7
  122. waldiez/models/agents/agent/termination_message.py +30 -22
  123. waldiez/models/agents/{swarm_agent → agent}/update_system_message.py +22 -22
  124. waldiez/models/agents/agents.py +58 -63
  125. waldiez/models/agents/assistant/assistant.py +4 -4
  126. waldiez/models/agents/assistant/assistant_data.py +13 -1
  127. waldiez/models/agents/{captain_agent → captain}/captain_agent.py +5 -5
  128. waldiez/models/agents/{captain_agent → captain}/captain_agent_data.py +5 -5
  129. waldiez/models/agents/extra_requirements.py +11 -16
  130. waldiez/models/agents/group_manager/group_manager.py +103 -13
  131. waldiez/models/agents/group_manager/group_manager_data.py +36 -14
  132. waldiez/models/agents/group_manager/speakers.py +77 -24
  133. waldiez/models/agents/{rag_user → rag_user_proxy}/__init__.py +16 -16
  134. waldiez/models/agents/rag_user_proxy/rag_user_proxy.py +64 -0
  135. waldiez/models/agents/{rag_user/rag_user_data.py → rag_user_proxy/rag_user_proxy_data.py} +6 -5
  136. waldiez/models/agents/{rag_user → rag_user_proxy}/retrieve_config.py +182 -114
  137. waldiez/models/agents/{rag_user → rag_user_proxy}/vector_db_config.py +13 -13
  138. waldiez/models/agents/reasoning/reasoning_agent.py +6 -6
  139. waldiez/models/agents/reasoning/reasoning_agent_data.py +110 -63
  140. waldiez/models/agents/reasoning/reasoning_agent_reason_config.py +38 -10
  141. waldiez/models/agents/user_proxy/user_proxy.py +11 -7
  142. waldiez/models/agents/user_proxy/user_proxy_data.py +2 -2
  143. waldiez/models/chat/__init__.py +2 -1
  144. waldiez/models/chat/chat.py +166 -87
  145. waldiez/models/chat/chat_data.py +99 -136
  146. waldiez/models/chat/chat_message.py +33 -23
  147. waldiez/models/chat/chat_nested.py +31 -30
  148. waldiez/models/chat/chat_summary.py +10 -8
  149. waldiez/models/common/__init__.py +52 -2
  150. waldiez/models/common/ag2_version.py +1 -1
  151. waldiez/models/common/base.py +38 -7
  152. waldiez/models/common/dict_utils.py +42 -17
  153. waldiez/models/common/handoff.py +459 -0
  154. waldiez/models/common/id_generator.py +19 -0
  155. waldiez/models/common/method_utils.py +130 -68
  156. waldiez/{exporting/base/utils → models/common}/naming.py +38 -61
  157. waldiez/models/common/waldiez_version.py +37 -0
  158. waldiez/models/flow/__init__.py +9 -2
  159. waldiez/models/flow/connection.py +18 -0
  160. waldiez/models/flow/flow.py +311 -215
  161. waldiez/models/flow/flow_data.py +207 -40
  162. waldiez/models/flow/info.py +85 -0
  163. waldiez/models/flow/naming.py +131 -0
  164. waldiez/models/model/__init__.py +7 -1
  165. waldiez/models/model/extra_requirements.py +3 -12
  166. waldiez/models/model/model.py +76 -21
  167. waldiez/models/model/model_data.py +108 -20
  168. waldiez/models/tool/__init__.py +16 -0
  169. waldiez/models/tool/extra_requirements.py +36 -0
  170. waldiez/models/{skill/skill.py → tool/tool.py} +88 -88
  171. waldiez/models/tool/tool_data.py +51 -0
  172. waldiez/models/tool/tool_type.py +8 -0
  173. waldiez/models/waldiez.py +97 -80
  174. waldiez/runner.py +115 -61
  175. waldiez/running/__init__.py +13 -7
  176. waldiez/running/environment.py +49 -68
  177. waldiez/running/gen_seq_diagram.py +16 -14
  178. waldiez/running/post_run.py +119 -0
  179. waldiez/running/pre_run.py +149 -0
  180. waldiez/running/util.py +134 -0
  181. waldiez/utils/__init__.py +2 -4
  182. waldiez/utils/cli_extras/jupyter.py +5 -3
  183. waldiez/utils/cli_extras/runner.py +6 -4
  184. waldiez/utils/cli_extras/studio.py +6 -4
  185. waldiez/utils/conflict_checker.py +15 -9
  186. waldiez/utils/flaml_warnings.py +5 -5
  187. waldiez/utils/version.py +47 -0
  188. {waldiez-0.4.7.dist-info → waldiez-0.4.9.dist-info}/METADATA +235 -91
  189. waldiez-0.4.9.dist-info/RECORD +203 -0
  190. waldiez/exporting/agent/agent_exporter.py +0 -297
  191. waldiez/exporting/agent/utils/__init__.py +0 -23
  192. waldiez/exporting/agent/utils/captain_agent.py +0 -263
  193. waldiez/exporting/agent/utils/code_execution.py +0 -65
  194. waldiez/exporting/agent/utils/group_manager.py +0 -220
  195. waldiez/exporting/agent/utils/rag_user/__init__.py +0 -7
  196. waldiez/exporting/agent/utils/rag_user/rag_user.py +0 -209
  197. waldiez/exporting/agent/utils/reasoning.py +0 -36
  198. waldiez/exporting/agent/utils/swarm_agent.py +0 -469
  199. waldiez/exporting/agent/utils/teachability.py +0 -41
  200. waldiez/exporting/agent/utils/termination_message.py +0 -44
  201. waldiez/exporting/base/__init__.py +0 -25
  202. waldiez/exporting/base/agent_position.py +0 -75
  203. waldiez/exporting/base/base_exporter.py +0 -118
  204. waldiez/exporting/base/export_position.py +0 -48
  205. waldiez/exporting/base/import_position.py +0 -23
  206. waldiez/exporting/base/mixin.py +0 -137
  207. waldiez/exporting/base/utils/__init__.py +0 -18
  208. waldiez/exporting/base/utils/comments.py +0 -96
  209. waldiez/exporting/base/utils/path_check.py +0 -68
  210. waldiez/exporting/base/utils/to_string.py +0 -84
  211. waldiez/exporting/chats/chats_exporter.py +0 -240
  212. waldiez/exporting/chats/utils/swarm.py +0 -210
  213. waldiez/exporting/flow/flow_exporter.py +0 -528
  214. waldiez/exporting/flow/utils/agent_utils.py +0 -204
  215. waldiez/exporting/flow/utils/chat_utils.py +0 -71
  216. waldiez/exporting/flow/utils/def_main.py +0 -77
  217. waldiez/exporting/flow/utils/flow_content.py +0 -202
  218. waldiez/exporting/flow/utils/flow_names.py +0 -116
  219. waldiez/exporting/flow/utils/importing_utils.py +0 -227
  220. waldiez/exporting/models/models_exporter.py +0 -199
  221. waldiez/exporting/models/utils.py +0 -174
  222. waldiez/exporting/skills/__init__.py +0 -9
  223. waldiez/exporting/skills/skills_exporter.py +0 -176
  224. waldiez/exporting/skills/utils.py +0 -369
  225. waldiez/models/agents/agent/teachability.py +0 -70
  226. waldiez/models/agents/rag_user/rag_user.py +0 -60
  227. waldiez/models/agents/swarm_agent/__init__.py +0 -50
  228. waldiez/models/agents/swarm_agent/after_work.py +0 -179
  229. waldiez/models/agents/swarm_agent/on_condition.py +0 -105
  230. waldiez/models/agents/swarm_agent/on_condition_available.py +0 -142
  231. waldiez/models/agents/swarm_agent/on_condition_target.py +0 -40
  232. waldiez/models/agents/swarm_agent/swarm_agent.py +0 -107
  233. waldiez/models/agents/swarm_agent/swarm_agent_data.py +0 -124
  234. waldiez/models/flow/utils.py +0 -232
  235. waldiez/models/skill/__init__.py +0 -16
  236. waldiez/models/skill/extra_requirements.py +0 -36
  237. waldiez/models/skill/skill_data.py +0 -53
  238. waldiez/models/skill/skill_type.py +0 -8
  239. waldiez/running/running.py +0 -369
  240. waldiez/utils/pysqlite3_checker.py +0 -308
  241. waldiez/utils/rdps_checker.py +0 -122
  242. waldiez-0.4.7.dist-info/RECORD +0 -149
  243. /waldiez/models/agents/{captain_agent → captain}/__init__.py +0 -0
  244. /waldiez/models/agents/{captain_agent → captain}/captain_agent_lib_entry.py +0 -0
  245. {waldiez-0.4.7.dist-info → waldiez-0.4.9.dist-info}/WHEEL +0 -0
  246. {waldiez-0.4.7.dist-info → waldiez-0.4.9.dist-info}/entry_points.txt +0 -0
  247. {waldiez-0.4.7.dist-info → waldiez-0.4.9.dist-info}/licenses/LICENSE +0 -0
  248. {waldiez-0.4.7.dist-info → waldiez-0.4.9.dist-info}/licenses/NOTICE.md +0 -0
@@ -0,0 +1,43 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+ # pylint: disable=unused-argument
4
+ """Video and audio media content models."""
5
+
6
+ from pathlib import Path
7
+
8
+ from pydantic import BaseModel
9
+ from typing_extensions import Literal
10
+
11
+ from .base import VideoContent
12
+
13
+
14
+ class VideoMediaContent(BaseModel):
15
+ """Video media content."""
16
+
17
+ type: Literal["video"] = "video"
18
+ video: VideoContent
19
+
20
+ def to_string(
21
+ self,
22
+ uploads_root: Path | None,
23
+ base_name: str | None = None,
24
+ ) -> str:
25
+ """Convert the content to a string.
26
+
27
+ Parameters
28
+ ----------
29
+ uploads_root : Path | None
30
+ The root directory for storing images, optional.
31
+ base_name : str | None
32
+ The base name for the image file, optional.
33
+
34
+ Returns
35
+ -------
36
+ str
37
+ The string representation of the content.
38
+ """
39
+ if self.video.url:
40
+ return f"<video src='{self.video.url}'></video>"
41
+ if self.video.file:
42
+ return f"<video src='{self.video.file}'></video>"
43
+ return str(self.video.file)
@@ -0,0 +1,269 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+
4
+ """User input data models and validation."""
5
+
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ from typing import Any, Union
10
+ from urllib.parse import urlparse
11
+
12
+ from pydantic import BaseModel, Field, field_validator
13
+ from typing_extensions import Annotated
14
+
15
+ from ..utils import detect_media_type
16
+ from .constants import CONTENT_MAPPING, ContentMappingEntry, MediaContent
17
+ from .content.audio import AudioMediaContent
18
+ from .content.base import (
19
+ AudioContent,
20
+ FileContent,
21
+ ImageContent,
22
+ VideoContent,
23
+ )
24
+ from .content.file import FileMediaContent
25
+ from .content.image import ImageMediaContent, ImageUrlMediaContent
26
+ from .content.text import TextMediaContent
27
+ from .content.video import VideoMediaContent
28
+
29
+
30
+ class UserInputData(BaseModel):
31
+ """User's input data model."""
32
+
33
+ content: Annotated[
34
+ Union[MediaContent, list[MediaContent]],
35
+ Field(
36
+ description="The content of the input data.",
37
+ title="Content",
38
+ ),
39
+ ]
40
+
41
+ def __str__(self) -> str:
42
+ """Get the string representation of the content."""
43
+ return self.to_string()
44
+
45
+ def __repr__(self) -> str:
46
+ """Get the string representation of the UserInputData."""
47
+ return f"UserInputData(content={self.to_string()})"
48
+
49
+ def to_string(
50
+ self,
51
+ uploads_root: Path | None = None,
52
+ base_name: str | None = None,
53
+ ) -> str:
54
+ """Convert the content to a string.
55
+
56
+ Parameters
57
+ ----------
58
+ uploads_root : Path | None
59
+ The root directory for storing images, optional.
60
+ base_name : str | None
61
+ The base name for the image file, optional.
62
+
63
+ Returns
64
+ -------
65
+ str
66
+ The string representation of the content.
67
+ """
68
+ if isinstance(self.content, list):
69
+ return " ".join(
70
+ [
71
+ item.to_string(uploads_root, base_name) # pyright: ignore
72
+ for item in self.content
73
+ ]
74
+ )
75
+ return self.content.to_string(uploads_root, base_name)
76
+
77
+ @classmethod
78
+ def content_from_string(cls, value: str) -> MediaContent:
79
+ """Convert a string to the appropriate MediaContent type.
80
+
81
+ Parameters
82
+ ----------
83
+ value : str
84
+ The input string
85
+
86
+ Returns
87
+ -------
88
+ MediaContent
89
+ the appropriate MediaContent.
90
+ """
91
+ try:
92
+ parsed = json.loads(value)
93
+ except json.JSONDecodeError:
94
+ return TextMediaContent(type="text", text=value)
95
+ if isinstance(parsed, str):
96
+ try:
97
+ parsed = json.loads(parsed)
98
+ except json.JSONDecodeError:
99
+ return TextMediaContent(type="text", text=parsed)
100
+ if isinstance(parsed, str):
101
+ return TextMediaContent(type="text", text=parsed)
102
+ return cls.validate_content(parsed)
103
+
104
+ @classmethod
105
+ def content_from_dict(cls, value: dict[str, Any]) -> MediaContent:
106
+ """Convert a dictionary to the appropriate MediaContent type.
107
+
108
+ Parameters
109
+ ----------
110
+ value : dict[str, Any]
111
+ The input dictionary
112
+
113
+ Returns
114
+ -------
115
+ MediaContent
116
+ the appropriate MediaContent.
117
+
118
+ Raises
119
+ ------
120
+ ValueError
121
+ If the content type is not supported or
122
+ if any required field is missing.
123
+ """
124
+ content_type = detect_media_type(value)
125
+
126
+ # Get the mapping for the detected content type
127
+ if content_type not in CONTENT_MAPPING:
128
+ raise ValueError(f"Unsupported content type: {content_type}")
129
+
130
+ # Get the mapping for the content type
131
+ mapping = CONTENT_MAPPING[content_type]
132
+ return cls._build_media_content(value, mapping, content_type)
133
+
134
+ @classmethod
135
+ def _build_media_content(
136
+ cls,
137
+ value: dict[str, Any],
138
+ mapping: ContentMappingEntry,
139
+ content_type: str,
140
+ ) -> MediaContent:
141
+ """Try to construct the appropriate MediaContent from the mapping."""
142
+ for field in mapping["fields"]:
143
+ if field not in value:
144
+ continue
145
+
146
+ raw_val = value[field]
147
+
148
+ if field == mapping["required_field"]:
149
+ # if we have direct mapping to the required field,
150
+ # let's try to instantiate the class directly
151
+ try:
152
+ return mapping["cls"](**{field: raw_val})
153
+ except ValueError:
154
+ pass # let's try to convert it
155
+
156
+ converted = cls._convert_simple_content(
157
+ raw_val, mapping["required_field"]
158
+ )
159
+ if converted is not None: # pragma: no branch
160
+ return mapping["cls"](
161
+ **{mapping["required_field"]: converted} # type: ignore
162
+ )
163
+
164
+ raise ValueError(
165
+ "Missing required field for content type "
166
+ f"'{content_type}' in: {value}"
167
+ )
168
+
169
+ @staticmethod
170
+ def _convert_simple_content(
171
+ raw_val: Any, target_field: str
172
+ ) -> ImageContent | VideoContent | AudioContent | FileContent | None:
173
+ """Convert a simple string to the appropriate content, if applicable."""
174
+ if not isinstance(raw_val, str): # pragma: no cover
175
+ return None
176
+
177
+ if target_field in ("image_url", "image"):
178
+ return ImageContent(url=raw_val)
179
+ if target_field == "video":
180
+ return VideoContent(url=raw_val)
181
+ if target_field == "audio":
182
+ return AudioContent(url=raw_val)
183
+ if target_field == "file":
184
+ filename = extract_filename_from_path(raw_val)
185
+ return FileContent(name=filename, url=raw_val)
186
+
187
+ return None # pragma: no cover
188
+
189
+ @field_validator("content", mode="before")
190
+ @classmethod
191
+ def validate_content(cls, v: Any) -> MediaContent: # noqa: C901,D102
192
+ """Validate the input data content.
193
+
194
+ Parameters
195
+ ----------
196
+ v: Any
197
+ The input data content
198
+
199
+ Returns
200
+ -------
201
+ MediaContent
202
+ The validated content
203
+
204
+ Raises
205
+ ------
206
+ ValueError
207
+ If the content is not valid.
208
+ """
209
+ if isinstance(
210
+ v,
211
+ (
212
+ TextMediaContent,
213
+ ImageMediaContent,
214
+ ImageUrlMediaContent,
215
+ VideoMediaContent,
216
+ AudioMediaContent,
217
+ FileMediaContent,
218
+ ),
219
+ ):
220
+ return v
221
+
222
+ # If it's a string, check if it is a dumped one
223
+ if isinstance(v, str):
224
+ return cls.content_from_string(v)
225
+
226
+ # If it's a dictionary, check if it has a 'type' field
227
+ if isinstance(v, dict):
228
+ return cls.content_from_dict(v) # pyright: ignore
229
+
230
+ # If it's a list
231
+ if isinstance(v, list):
232
+ return [cls.validate_content(item) for item in v] # type: ignore
233
+
234
+ # Default fallback
235
+ return TextMediaContent(type="text", text=str(v))
236
+
237
+
238
+ def extract_filename_from_path(path_or_url: str) -> str:
239
+ """Extract the filename from a given path or URL.
240
+
241
+ Parameters
242
+ ----------
243
+ path_or_url: str
244
+ The path or URL from which to extract the filename.
245
+
246
+ Returns
247
+ -------
248
+ str
249
+ The extracted filename.
250
+ """
251
+ if "://" in path_or_url:
252
+ # It's a URL - parse it properly
253
+ parsed = urlparse(path_or_url)
254
+ # Extract filename from the path component
255
+ filename = os.path.basename(parsed.path)
256
+
257
+ # Handle edge cases where path might be empty or end with /
258
+ if not filename and parsed.path.endswith("/"):
259
+ # Try to get the last directory name
260
+ path_parts = [p for p in parsed.path.split("/") if p]
261
+ filename = path_parts[-1] if path_parts else parsed.netloc
262
+ elif not filename:
263
+ # Fallback to using netloc (domain) if no path
264
+ filename = parsed.netloc
265
+
266
+ return filename
267
+ # else:
268
+ # Local file path
269
+ return os.path.basename(path_or_url)
@@ -0,0 +1,215 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+ """User response model and validation."""
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any, Callable, Optional, Union
8
+
9
+ from pydantic import ValidationError, field_validator
10
+
11
+ from ..utils import MessageType
12
+ from .base import StructuredBase
13
+ from .content.text import TextMediaContent
14
+ from .user_input import UserInputData
15
+
16
+
17
+ class UserResponse(StructuredBase):
18
+ """User response model."""
19
+
20
+ request_id: str
21
+ type: MessageType = "input_response"
22
+ data: UserInputData | list[UserInputData] | str
23
+
24
+ @field_validator("data", mode="before")
25
+ @classmethod
26
+ def validate_data(
27
+ cls, value: Any
28
+ ) -> Optional[Union[str, UserInputData, list[UserInputData]]]:
29
+ """Validate the data field in UserResponse.
30
+
31
+ Parameters
32
+ ----------
33
+ value : Any
34
+ The value to validate.
35
+
36
+ Returns
37
+ -------
38
+ Union[str, UserInputData, list[UserInputData]]
39
+ The validated value.
40
+
41
+ Raises
42
+ ------
43
+ ValueError
44
+ If the value is not valid.
45
+ """
46
+ if value is None:
47
+ return ""
48
+ if cls._is_valid_type(value):
49
+ return value
50
+
51
+ handlers: dict[
52
+ type,
53
+ Callable[[Any], Union[str, UserInputData, list[UserInputData]]],
54
+ ] = {
55
+ str: cls._handle_string,
56
+ dict: cls._handle_dict,
57
+ list: cls._handle_list,
58
+ }
59
+
60
+ value_type = type(value) # pyright: ignore
61
+ handler = handlers.get(
62
+ value_type, # pyright: ignore
63
+ cls._handle_default,
64
+ )
65
+ result = handler(value)
66
+ return result
67
+
68
+ @classmethod
69
+ def _is_valid_type(cls, value: Any) -> bool:
70
+ """Check if value is already a valid type."""
71
+ return isinstance(value, UserInputData) or (
72
+ isinstance(value, list)
73
+ and all(
74
+ isinstance(item, UserInputData)
75
+ for item in value # pyright: ignore
76
+ )
77
+ )
78
+
79
+ @classmethod
80
+ def _handle_string(
81
+ cls, value: str
82
+ ) -> Union[str, UserInputData, list[UserInputData]]:
83
+ """Handle string input.
84
+
85
+ Parameters
86
+ ----------
87
+ value : str
88
+ The string value to handle.
89
+
90
+ Returns
91
+ -------
92
+ Union[str, UserInputData, list[UserInputData]]
93
+ The handled value.
94
+ """
95
+ # pylint: disable=too-many-try-statements
96
+ try:
97
+ parsed_value = json.loads(value)
98
+ if isinstance(parsed_value, dict):
99
+ return cls._handle_dict(parsed_value) # pyright: ignore
100
+ if isinstance(parsed_value, list):
101
+ return cls._handle_list(parsed_value) # pyright: ignore
102
+ return cls._create_text_input(str(parsed_value))
103
+ except json.JSONDecodeError:
104
+ return cls._create_text_input(value)
105
+
106
+ @classmethod
107
+ def _handle_dict(cls, value: dict[str, Any]) -> UserInputData:
108
+ if "content" in value:
109
+ return cls._try_parse_user_input(value)
110
+
111
+ try:
112
+ return UserInputData(content=value) # type: ignore
113
+ except (ValueError, TypeError, ValidationError):
114
+ return cls._create_invalid_input(value, label="Invalid data")
115
+
116
+ @classmethod
117
+ def _try_parse_user_input(cls, value: dict[str, Any]) -> UserInputData:
118
+ try:
119
+ return UserInputData(content=value["content"])
120
+ except (KeyError, ValueError, TypeError, ValidationError):
121
+ try:
122
+ return UserInputData(content=value) # type: ignore
123
+ except (ValueError, TypeError, ValidationError):
124
+ return cls._create_invalid_input(value, label="Invalid content")
125
+
126
+ @classmethod
127
+ def _handle_list(
128
+ cls, value: list[Any]
129
+ ) -> Union[UserInputData, list[UserInputData]]:
130
+ result: list[UserInputData] = []
131
+
132
+ for item in value:
133
+ if isinstance(item, UserInputData):
134
+ result.append(item)
135
+ elif isinstance(item, dict):
136
+ result.append(cls._handle_dict(item)) # pyright: ignore
137
+ elif isinstance(item, str):
138
+ result.append(cls._create_text_input(item))
139
+ elif isinstance(item, list):
140
+ nested_result = cls._handle_list(item) # pyright: ignore
141
+ if isinstance(nested_result, list):
142
+ result.extend(nested_result)
143
+ else:
144
+ result.append(nested_result)
145
+ else:
146
+ result.append(cls._create_text_input(str(item)))
147
+
148
+ return result[0] if len(result) == 1 else result
149
+
150
+ @classmethod
151
+ def _handle_default(cls, value: Any) -> UserInputData:
152
+ return cls._create_text_input(str(value))
153
+
154
+ @classmethod
155
+ def _create_text_input(cls, text: str) -> UserInputData:
156
+ return UserInputData(content=TextMediaContent(text=text))
157
+
158
+ @classmethod
159
+ def _create_invalid_input(cls, raw: Any, label: str) -> UserInputData:
160
+ try:
161
+ preview = json.dumps(raw)[:100]
162
+ except (TypeError, ValueError): # pragma: no cover
163
+ preview = str(raw)[:100]
164
+ return UserInputData(
165
+ content=TextMediaContent(text=f"{label}: {preview}...")
166
+ )
167
+
168
+ def to_string(
169
+ self,
170
+ uploads_root: Path | None = None,
171
+ base_name: str | None = None,
172
+ ) -> str:
173
+ """Convert the UserResponse to a string.
174
+
175
+ Parameters
176
+ ----------
177
+ uploads_root : Path | None
178
+ The root directory for storing images, optional.
179
+ base_name : str | None
180
+ The base name for the image file, optional.
181
+
182
+ Returns
183
+ -------
184
+ str
185
+ The string representation of the UserResponse.
186
+ """
187
+ if isinstance(self.data, list):
188
+ return " ".join(
189
+ [
190
+ (
191
+ item.to_string(
192
+ uploads_root=uploads_root, base_name=base_name
193
+ )
194
+ if hasattr(item, "to_string")
195
+ else (
196
+ item
197
+ if isinstance(item, str) # pragma: no cover
198
+ else str(item)
199
+ ) # pragma: no cover
200
+ )
201
+ for item in self.data
202
+ ]
203
+ ).strip()
204
+ if isinstance(self.data, UserInputData):
205
+ return self.data.to_string(
206
+ uploads_root=uploads_root, base_name=base_name
207
+ ).strip()
208
+ # we have probably returned sth till here
209
+ if isinstance(self.data, str): # pyright: ignore # pragma: no cover
210
+ return self.data
211
+ return ( # pragma: no cover
212
+ json.dumps(self.data)
213
+ if hasattr(self.data, "__dict__")
214
+ else str(self.data)
215
+ )