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,439 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+
4
+ """Structured I/O stream for JSON-based communication over stdin/stdout."""
5
+
6
+ import json
7
+ import queue
8
+ import sys
9
+ import threading
10
+ from getpass import getpass
11
+ from pathlib import Path
12
+ from typing import Any
13
+ from uuid import uuid4
14
+
15
+ from autogen.events import BaseEvent # type: ignore
16
+ from autogen.io import IOStream # type: ignore
17
+
18
+ from .models import (
19
+ PrintMessage,
20
+ UserInputData,
21
+ UserInputRequest,
22
+ UserResponse,
23
+ )
24
+ from .utils import (
25
+ gen_id,
26
+ get_image,
27
+ is_json_dumped,
28
+ now,
29
+ try_parse_maybe_serialized,
30
+ )
31
+
32
+
33
+ class StructuredIOStream(IOStream):
34
+ """Structured I/O stream using stdin and stdout."""
35
+
36
+ uploads_root: Path | None = None
37
+
38
+ def __init__(
39
+ self,
40
+ timeout: float = 120,
41
+ uploads_root: Path | str | None = None,
42
+ is_async: bool = False,
43
+ ) -> None:
44
+ self.timeout = timeout
45
+ self.is_async = is_async
46
+ if uploads_root is not None:
47
+ self.uploads_root = Path(uploads_root).resolve()
48
+ if not self.uploads_root.exists():
49
+ self.uploads_root.mkdir(parents=True, exist_ok=True)
50
+
51
+ # noinspection PyMethodMayBeStatic
52
+ # pylint: disable=no-self-use
53
+ def print(self, *args: Any, **kwargs: Any) -> None:
54
+ """Structured print to stdout.
55
+
56
+ Parameters
57
+ ----------
58
+ args : Any
59
+ The data to print.
60
+ kwargs : Any
61
+ """
62
+ sep = kwargs.get("sep", " ")
63
+ end = kwargs.get("end", "\n")
64
+ message = sep.join(map(str, args))
65
+ is_dumped = is_json_dumped(message)
66
+ if is_dumped and end.endswith("\n"):
67
+ # If the message is already JSON-dumped,
68
+ # let's try not to double dump it
69
+ message = json.loads(message)
70
+ payload: dict[str, Any] = {
71
+ "type": "print",
72
+ "id": uuid4().hex,
73
+ "timestamp": now(),
74
+ "data": message,
75
+ }
76
+ else:
77
+ message += end
78
+ payload = PrintMessage(
79
+ id=uuid4().hex,
80
+ timestamp=now(),
81
+ data=message,
82
+ ).model_dump(mode="json")
83
+ flush = kwargs.get("flush", True)
84
+ payload_type = kwargs.get("type", "print")
85
+ payload["type"] = payload_type
86
+ print(json.dumps(payload), flush=flush)
87
+
88
+ def input(self, prompt: str = "", *, password: bool = False) -> str:
89
+ """Structured input from stdin.
90
+
91
+ Parameters
92
+ ----------
93
+ prompt : str, optional
94
+ The prompt to display. Defaults to "".
95
+ password : bool, optional
96
+ Whether to read a password. Defaults to False.
97
+
98
+ Returns
99
+ -------
100
+ str
101
+ The line read from the input stream.
102
+ """
103
+ request_id = uuid4().hex
104
+ prompt = prompt or "> "
105
+
106
+ self._send_input_request(prompt, request_id, password)
107
+ user_input_raw = self._read_user_input(prompt, password, request_id)
108
+ response = self._handle_user_input(user_input_raw, request_id)
109
+ user_response = response.to_string(
110
+ uploads_root=self.uploads_root,
111
+ base_name=request_id,
112
+ )
113
+ # let's keep an eye here:
114
+ # https://github.com/ag2ai/ag2/blob/main/autogen/agentchat/conversable_agent.py#L2973
115
+ # reply = await iostream.input(prompt) ???? (await???)
116
+ if self.is_async:
117
+ # let's make a coroutine to just return the user response
118
+ async def async_input() -> str:
119
+ """Asynchronous input method.
120
+
121
+ Returns
122
+ -------
123
+ str
124
+ The line read from the input stream.
125
+ """
126
+ return user_response
127
+
128
+ return async_input() # type: ignore
129
+ return user_response
130
+
131
+ # noinspection PyMethodMayBeStatic
132
+ # pylint: disable=no-self-use
133
+ def send(self, message: BaseEvent) -> None:
134
+ """Structured sending of a BaseEvent.
135
+
136
+ Parameters
137
+ ----------
138
+ message : BaseEvent
139
+ The message to send.
140
+ """
141
+ message_dump = message.model_dump(mode="json")
142
+ if message_dump.get("type") == "text":
143
+ content_block = message_dump.get("content")
144
+ if (
145
+ isinstance(content_block, dict)
146
+ and "content" in content_block
147
+ and isinstance(content_block["content"], str)
148
+ ):
149
+ inner_content = content_block["content"]
150
+ content_block["content"] = try_parse_maybe_serialized(
151
+ inner_content
152
+ )
153
+ print(json.dumps(message_dump), flush=True, file=sys.stdout)
154
+
155
+ # noinspection PyMethodMayBeStatic
156
+ # pylint: disable=no-self-use
157
+ def _send_input_request(
158
+ self,
159
+ prompt: str,
160
+ request_id: str,
161
+ password: bool,
162
+ ) -> None:
163
+ payload = UserInputRequest(
164
+ request_id=request_id,
165
+ prompt=prompt,
166
+ password=password,
167
+ ).model_dump(mode="json")
168
+ print(json.dumps(payload), flush=True)
169
+
170
+ def _read_user_input(
171
+ self,
172
+ prompt: str,
173
+ password: bool,
174
+ request_id: str,
175
+ ) -> str:
176
+ input_queue: queue.Queue[str] = queue.Queue()
177
+
178
+ def read_input() -> None:
179
+ """Read user input from stdin."""
180
+ try:
181
+ user_input = (
182
+ getpass(prompt).strip()
183
+ if password
184
+ else input(prompt).strip()
185
+ )
186
+ input_queue.put(user_input)
187
+ except EOFError:
188
+ input_queue.put("")
189
+
190
+ input_thread = threading.Thread(target=read_input, daemon=True)
191
+ input_thread.start()
192
+
193
+ try:
194
+ return input_queue.get(timeout=self.timeout)
195
+ except queue.Empty:
196
+ self._send_timeout_message(request_id)
197
+ return ""
198
+
199
+ def _send_timeout_message(self, request_id: str) -> None:
200
+ timeout_payload = {
201
+ "id": gen_id(),
202
+ "type": "timeout",
203
+ "request_id": request_id,
204
+ "timestamp": now(),
205
+ "data": f"No input received after {self.timeout} seconds.",
206
+ }
207
+ print(json.dumps(timeout_payload), flush=True, file=sys.stderr)
208
+
209
+ def _handle_user_input(
210
+ self, user_input_raw: str, request_id: str
211
+ ) -> UserResponse:
212
+ """Handle user input and return the appropriate response.
213
+
214
+ Parameters
215
+ ----------
216
+ user_input_raw : str
217
+ The raw user input string.
218
+ request_id : str
219
+ The request ID to match against.
220
+
221
+ Returns
222
+ -------
223
+ UserResponse
224
+ The structured user response.
225
+ """
226
+ user_input = self._load_user_input(user_input_raw)
227
+ if isinstance(user_input, str):
228
+ return UserResponse(data=user_input, request_id=request_id)
229
+ return self._parse_user_input(user_input, request_id)
230
+
231
+ @staticmethod
232
+ def _load_user_input(user_input_raw: str) -> str | dict[str, Any]:
233
+ """Load user input from a raw string.
234
+
235
+ Parameters
236
+ ----------
237
+ user_input_raw : str
238
+ The raw user input string.
239
+
240
+ Returns
241
+ -------
242
+ str | dict[str, Any]
243
+ The loaded user input, either as a string or a dictionary.
244
+ """
245
+ response: str | dict[str, Any] = user_input_raw
246
+ try:
247
+ # Attempt to parse the input as JSON
248
+ response = json.loads(user_input_raw)
249
+ except json.JSONDecodeError:
250
+ # If it's not valid JSON, return as is
251
+ # This allows for backwards compatibility with raw text input
252
+ return user_input_raw
253
+ if isinstance(response, str):
254
+ # double inner dumped?
255
+ try:
256
+ response = json.loads(response)
257
+ except json.JSONDecodeError:
258
+ # If it's not valid JSON, return as is
259
+ return response
260
+ if not isinstance(response, dict):
261
+ return str(response)
262
+ if "data" in response and isinstance(response["data"], str):
263
+ # double inner dumped?
264
+ try:
265
+ response["data"] = json.loads(response["data"])
266
+ except json.JSONDecodeError:
267
+ pass
268
+ return response
269
+
270
+ def _parse_user_input(
271
+ self, user_input: dict[str, Any], request_id: str
272
+ ) -> UserResponse:
273
+ """Parse user input and return the appropriate response.
274
+
275
+ Parameters
276
+ ----------
277
+ user_input : str
278
+ The raw user input string.
279
+ request_id : str
280
+ The request ID to match against.
281
+
282
+ Returns
283
+ -------
284
+ UserResponse
285
+ The structured user response.
286
+ """
287
+ # Load the user input
288
+ if user_input.get("request_id") == request_id:
289
+ # We have a valid response to our request
290
+ data = user_input.get("data")
291
+ if not data:
292
+ # let's check if text|image keys are sent (outside data)
293
+ if "image" in user_input or "text" in user_input:
294
+ return UserResponse(
295
+ request_id=request_id,
296
+ data=self._format_multimedia_response(
297
+ request_id=request_id, data=user_input
298
+ ),
299
+ )
300
+ if isinstance(data, list):
301
+ return self._handle_list_response(
302
+ data, # pyright: ignore
303
+ request_id=request_id,
304
+ )
305
+ if not data or not isinstance(data, (str, dict)):
306
+ # No / invalid data provided in the response
307
+ return UserResponse(
308
+ request_id=request_id,
309
+ data="",
310
+ )
311
+ # Process different data types
312
+ if isinstance(data, str):
313
+ # double inner dumped?
314
+ data = self._load_user_input(data)
315
+ if isinstance(data, dict):
316
+ return UserResponse(
317
+ data=self._format_multimedia_response(
318
+ request_id=request_id,
319
+ data=data, # pyright: ignore
320
+ ),
321
+ request_id=request_id,
322
+ )
323
+ # For other types (numbers, bools ,...),
324
+ # let's just convert to string
325
+ return UserResponse(
326
+ data=str(data), request_id=request_id
327
+ ) # pragma: no cover
328
+ # This response doesn't match our request_id, log and return empty
329
+ self._log_mismatched_response(request_id, user_input)
330
+ return UserResponse(
331
+ request_id=request_id,
332
+ data="",
333
+ )
334
+
335
+ def _handle_list_response(
336
+ self,
337
+ data: list[dict[str, Any]],
338
+ request_id: str,
339
+ ) -> UserResponse:
340
+ if len(data) == 0: # pyright: ignore
341
+ # Empty list, return empty response
342
+ return UserResponse(
343
+ request_id=request_id,
344
+ data="",
345
+ )
346
+
347
+ input_data: list[UserInputData] = []
348
+ for entry in data: # pyright: ignore
349
+ # pylint: disable=broad-exception-caught
350
+ try:
351
+ content = UserInputData.model_validate(entry)
352
+ input_data.append(content)
353
+ except Exception as error: # pragma: no cover
354
+ print({"type": "error", "message": str(error)}, file=sys.stderr)
355
+ continue
356
+ if not input_data: # pragma: no cover
357
+ # No valid data in the list, return empty response
358
+ return UserResponse(
359
+ request_id=request_id,
360
+ data="",
361
+ )
362
+ return UserResponse(
363
+ request_id=request_id,
364
+ data=input_data,
365
+ )
366
+
367
+ @staticmethod
368
+ def _log_mismatched_response(expected_id: str, response: Any) -> None:
369
+ """Log information about mismatched response IDs.
370
+
371
+ Parameters
372
+ ----------
373
+ expected_id : str
374
+ The request ID we were expecting
375
+ response : Any
376
+ The response received
377
+ """
378
+ # Create a log message
379
+ log_payload: dict[str, Any] = {
380
+ "type": "warning",
381
+ "id": uuid4().hex,
382
+ "timestamp": now(),
383
+ "data": {
384
+ "message": (
385
+ "Received response with mismatched request_id. "
386
+ f"Expected: {expected_id}"
387
+ ),
388
+ "details": {
389
+ "expected_id": expected_id,
390
+ "received": str(response)[:100]
391
+ + ("..." if len(str(response)) > 100 else ""),
392
+ },
393
+ },
394
+ }
395
+ # Print to stderr to avoid interfering with stdout communication
396
+ print(json.dumps(log_payload), file=sys.stderr)
397
+
398
+ def _format_multimedia_response(
399
+ self,
400
+ data: dict[str, Any],
401
+ request_id: str | None = None,
402
+ ) -> str:
403
+ """Format a multimedia response dict into a string with image tags.
404
+
405
+ Parameters
406
+ ----------
407
+ data : dict[str, Any]
408
+ The data dictionary containing "image" and "text" keys.
409
+ request_id : str | None
410
+ The input request ID, if available.
411
+
412
+ Returns
413
+ -------
414
+ str
415
+ The formatted string with image tags and text.
416
+ """
417
+ result: list[str] = []
418
+ if "content" in data and isinstance(data["content"], dict):
419
+ return self._format_multimedia_response(
420
+ data=data["content"], # pyright: ignore
421
+ request_id=request_id,
422
+ )
423
+
424
+ # Handle image if present
425
+ if "image" in data and data["image"]:
426
+ image_data = data["image"]
427
+ image = get_image(self.uploads_root, image_data, request_id)
428
+ img_tag = f"<img {image}>"
429
+ result.append(img_tag)
430
+
431
+ # Handle text if present
432
+ if "text" in data and data["text"]:
433
+ result.append(str(data["text"]))
434
+
435
+ # If neither image nor text, return empty string
436
+ if not result:
437
+ return ""
438
+ # Join with a space
439
+ return " ".join(result)
waldiez/io/utils.py ADDED
@@ -0,0 +1,184 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+
4
+ """Utility functions for the waldiez.io package."""
5
+
6
+ import ast
7
+ import json
8
+ import uuid
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import Any, Literal, Union
12
+
13
+ from autogen.agentchat.contrib.img_utils import get_pil_image # type: ignore
14
+
15
+ MessageType = Literal[
16
+ "input_request",
17
+ "input_response",
18
+ "print",
19
+ "input",
20
+ ]
21
+ """Possible message types for the structured I/O stream."""
22
+
23
+ MediaType = Union[
24
+ Literal["text"],
25
+ Literal["image"],
26
+ Literal["image_url"],
27
+ Literal["video"],
28
+ Literal["audio"],
29
+ Literal["file"],
30
+ ]
31
+ """Possible media types for the structured I/O stream."""
32
+
33
+
34
+ def gen_id() -> str:
35
+ """Generate a unique identifier.
36
+
37
+ Returns
38
+ -------
39
+ str
40
+ A unique id
41
+ """
42
+ return str(uuid.uuid4())
43
+
44
+
45
+ def now() -> str:
46
+ """Get the current time as an ISO string.
47
+
48
+ Returns
49
+ -------
50
+ str
51
+ The current time as an ISO string.
52
+ """
53
+ return datetime.now().isoformat()
54
+
55
+
56
+ def detect_media_type(value: dict[str, Any]) -> MediaType:
57
+ """Detect mediatype from dict.
58
+
59
+ Either using the 'type' field or by checking the keys.
60
+
61
+ Parameters
62
+ ----------
63
+ value : dict[str, Any]
64
+ The input dictionary
65
+
66
+ Returns
67
+ -------
68
+ MediaType
69
+ The detected media type
70
+
71
+ Raises
72
+ ------
73
+ ValueError
74
+ If the media type is not valid or not found.
75
+ """
76
+ valid_types = (
77
+ "text",
78
+ "image",
79
+ "image_url",
80
+ "video",
81
+ "audio",
82
+ "file",
83
+ )
84
+ if "type" in value:
85
+ content_type = value["type"]
86
+ if content_type not in valid_types:
87
+ raise ValueError(f"Invalid media type: {content_type}")
88
+ return content_type
89
+ for valid_type in valid_types:
90
+ if valid_type in value:
91
+ return valid_type # type: ignore
92
+ raise ValueError(f"No type in value: {value}.")
93
+
94
+
95
+ def get_image(
96
+ uploads_root: Path | None,
97
+ image_data: str,
98
+ base_name: str | None = None,
99
+ ) -> str:
100
+ """Store the image data in a file and return the file path.
101
+
102
+ Parameters
103
+ ----------
104
+ uploads_root : Path | None
105
+ The root directory for storing images, optional.
106
+ image_data : str
107
+ The base64-encoded image data.
108
+ base_name : str | None
109
+ The base name for the image file, optional.
110
+
111
+ Returns
112
+ -------
113
+ str
114
+ The file path of the stored image.
115
+ """
116
+ if uploads_root:
117
+ # noinspection PyBroadException
118
+ # pylint: disable=broad-exception-caught
119
+ try:
120
+ pil_image = get_pil_image(image_data)
121
+ except BaseException:
122
+ return image_data
123
+ if not base_name:
124
+ base_name = uuid.uuid4().hex
125
+ file_name = f"{base_name}.png"
126
+ file_path = uploads_root / file_name
127
+ pil_image.save(file_path, format="PNG")
128
+ return str(file_path)
129
+ return image_data
130
+
131
+
132
+ def is_json_dumped(value: str) -> bool:
133
+ """Check if a string is JSON-dumped.
134
+
135
+ Parameters
136
+ ----------
137
+ value : str
138
+ The string to check.
139
+
140
+ Returns
141
+ -------
142
+ bool
143
+ True if the string is JSON-dumped, False otherwise.
144
+ """
145
+ try:
146
+ parsed = json.loads(value)
147
+ # If we can parse it as JSON and it's not a string,
148
+ # we consider it JSON-dumped
149
+ return not isinstance(parsed, str)
150
+ except json.JSONDecodeError:
151
+ return False
152
+
153
+
154
+ def try_parse_maybe_serialized(value: str) -> Any:
155
+ """Parse a string that may be JSON or Python serialized.
156
+
157
+ Returns the parsed object if successful, or the original string otherwise.
158
+
159
+ Parameters
160
+ ----------
161
+ value : str
162
+ The string to parse.
163
+
164
+ Returns
165
+ -------
166
+ Any
167
+ The parsed object or the original string if parsing fails.
168
+ """
169
+ for parser in (json.loads, ast.literal_eval):
170
+ # pylint: disable=broad-exception-caught, too-many-try-statements
171
+ try:
172
+ parsed: dict[str, Any] | list[Any] | str = parser(value)
173
+ # Normalize: if it's a single-item list of a string
174
+ # return the string
175
+ if (
176
+ isinstance(parsed, list)
177
+ and len(parsed) == 1
178
+ and isinstance(parsed[0], str)
179
+ ):
180
+ return parsed[0]
181
+ return parsed
182
+ except Exception:
183
+ pass # Try next parser
184
+ return value # Return original if all parsing fails