waldiez 0.4.7__py3-none-any.whl → 0.4.8__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 (244) hide show
  1. waldiez/__init__.py +5 -5
  2. waldiez/_version.py +1 -1
  3. waldiez/cli.py +112 -73
  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} +16 -15
  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 +419 -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 +114 -49
  175. waldiez/running/__init__.py +1 -1
  176. waldiez/running/environment.py +49 -68
  177. waldiez/running/gen_seq_diagram.py +16 -14
  178. waldiez/running/running.py +53 -34
  179. waldiez/utils/__init__.py +0 -4
  180. waldiez/utils/cli_extras/jupyter.py +5 -3
  181. waldiez/utils/cli_extras/runner.py +6 -4
  182. waldiez/utils/cli_extras/studio.py +6 -4
  183. waldiez/utils/conflict_checker.py +15 -9
  184. waldiez/utils/flaml_warnings.py +5 -5
  185. {waldiez-0.4.7.dist-info → waldiez-0.4.8.dist-info}/METADATA +235 -91
  186. waldiez-0.4.8.dist-info/RECORD +200 -0
  187. waldiez/exporting/agent/agent_exporter.py +0 -297
  188. waldiez/exporting/agent/utils/__init__.py +0 -23
  189. waldiez/exporting/agent/utils/captain_agent.py +0 -263
  190. waldiez/exporting/agent/utils/code_execution.py +0 -65
  191. waldiez/exporting/agent/utils/group_manager.py +0 -220
  192. waldiez/exporting/agent/utils/rag_user/__init__.py +0 -7
  193. waldiez/exporting/agent/utils/rag_user/rag_user.py +0 -209
  194. waldiez/exporting/agent/utils/reasoning.py +0 -36
  195. waldiez/exporting/agent/utils/swarm_agent.py +0 -469
  196. waldiez/exporting/agent/utils/teachability.py +0 -41
  197. waldiez/exporting/agent/utils/termination_message.py +0 -44
  198. waldiez/exporting/base/__init__.py +0 -25
  199. waldiez/exporting/base/agent_position.py +0 -75
  200. waldiez/exporting/base/base_exporter.py +0 -118
  201. waldiez/exporting/base/export_position.py +0 -48
  202. waldiez/exporting/base/import_position.py +0 -23
  203. waldiez/exporting/base/mixin.py +0 -137
  204. waldiez/exporting/base/utils/__init__.py +0 -18
  205. waldiez/exporting/base/utils/comments.py +0 -96
  206. waldiez/exporting/base/utils/path_check.py +0 -68
  207. waldiez/exporting/base/utils/to_string.py +0 -84
  208. waldiez/exporting/chats/chats_exporter.py +0 -240
  209. waldiez/exporting/chats/utils/swarm.py +0 -210
  210. waldiez/exporting/flow/flow_exporter.py +0 -528
  211. waldiez/exporting/flow/utils/agent_utils.py +0 -204
  212. waldiez/exporting/flow/utils/chat_utils.py +0 -71
  213. waldiez/exporting/flow/utils/def_main.py +0 -77
  214. waldiez/exporting/flow/utils/flow_content.py +0 -202
  215. waldiez/exporting/flow/utils/flow_names.py +0 -116
  216. waldiez/exporting/flow/utils/importing_utils.py +0 -227
  217. waldiez/exporting/models/models_exporter.py +0 -199
  218. waldiez/exporting/models/utils.py +0 -174
  219. waldiez/exporting/skills/__init__.py +0 -9
  220. waldiez/exporting/skills/skills_exporter.py +0 -176
  221. waldiez/exporting/skills/utils.py +0 -369
  222. waldiez/models/agents/agent/teachability.py +0 -70
  223. waldiez/models/agents/rag_user/rag_user.py +0 -60
  224. waldiez/models/agents/swarm_agent/__init__.py +0 -50
  225. waldiez/models/agents/swarm_agent/after_work.py +0 -179
  226. waldiez/models/agents/swarm_agent/on_condition.py +0 -105
  227. waldiez/models/agents/swarm_agent/on_condition_available.py +0 -142
  228. waldiez/models/agents/swarm_agent/on_condition_target.py +0 -40
  229. waldiez/models/agents/swarm_agent/swarm_agent.py +0 -107
  230. waldiez/models/agents/swarm_agent/swarm_agent_data.py +0 -124
  231. waldiez/models/flow/utils.py +0 -232
  232. waldiez/models/skill/__init__.py +0 -16
  233. waldiez/models/skill/extra_requirements.py +0 -36
  234. waldiez/models/skill/skill_data.py +0 -53
  235. waldiez/models/skill/skill_type.py +0 -8
  236. waldiez/utils/pysqlite3_checker.py +0 -308
  237. waldiez/utils/rdps_checker.py +0 -122
  238. waldiez-0.4.7.dist-info/RECORD +0 -149
  239. /waldiez/models/agents/{captain_agent → captain}/__init__.py +0 -0
  240. /waldiez/models/agents/{captain_agent → captain}/captain_agent_lib_entry.py +0 -0
  241. {waldiez-0.4.7.dist-info → waldiez-0.4.8.dist-info}/WHEEL +0 -0
  242. {waldiez-0.4.7.dist-info → waldiez-0.4.8.dist-info}/entry_points.txt +0 -0
  243. {waldiez-0.4.7.dist-info → waldiez-0.4.8.dist-info}/licenses/LICENSE +0 -0
  244. {waldiez-0.4.7.dist-info → waldiez-0.4.8.dist-info}/licenses/NOTICE.md +0 -0
@@ -0,0 +1,419 @@
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, timeout: float = 120, uploads_root: Path | str | None = None
40
+ ) -> None:
41
+ self.timeout = timeout
42
+ if uploads_root is not None:
43
+ self.uploads_root = Path(uploads_root).resolve()
44
+ if not self.uploads_root.exists():
45
+ self.uploads_root.mkdir(parents=True, exist_ok=True)
46
+
47
+ # noinspection PyMethodMayBeStatic
48
+ # pylint: disable=no-self-use
49
+ def print(self, *args: Any, **kwargs: Any) -> None:
50
+ """Structured print to stdout.
51
+
52
+ Parameters
53
+ ----------
54
+ args : Any
55
+ The data to print.
56
+ kwargs : Any
57
+ """
58
+ sep = kwargs.get("sep", " ")
59
+ end = kwargs.get("end", "\n")
60
+ message = sep.join(map(str, args))
61
+ is_dumped = is_json_dumped(message)
62
+ if is_dumped and end.endswith("\n"):
63
+ # If the message is already JSON-dumped,
64
+ # let's try not to double dump it
65
+ message = json.loads(message)
66
+ payload: dict[str, Any] = {
67
+ "type": "print",
68
+ "id": uuid4().hex,
69
+ "timestamp": now(),
70
+ "data": message,
71
+ }
72
+ else:
73
+ message += end
74
+ payload = PrintMessage(
75
+ id=uuid4().hex,
76
+ timestamp=now(),
77
+ data=message,
78
+ ).model_dump(mode="json")
79
+ flush = kwargs.get("flush", True)
80
+ payload_type = kwargs.get("type", "print")
81
+ payload["type"] = payload_type
82
+ print(json.dumps(payload), flush=flush)
83
+
84
+ def input(self, prompt: str = "", *, password: bool = False) -> str:
85
+ """Structured input from stdin.
86
+
87
+ Parameters
88
+ ----------
89
+ prompt : str, optional
90
+ The prompt to display. Defaults to "".
91
+ password : bool, optional
92
+ Whether to read a password. Defaults to False.
93
+
94
+ Returns
95
+ -------
96
+ str
97
+ The line read from the input stream.
98
+ """
99
+ request_id = uuid4().hex
100
+ prompt = prompt or "> "
101
+
102
+ self._send_input_request(prompt, request_id, password)
103
+ user_input_raw = self._read_user_input(prompt, password, request_id)
104
+ response = self._handle_user_input(user_input_raw, request_id)
105
+ user_response = response.to_string(
106
+ uploads_root=self.uploads_root,
107
+ base_name=request_id,
108
+ )
109
+ return user_response
110
+
111
+ # noinspection PyMethodMayBeStatic
112
+ # pylint: disable=no-self-use
113
+ def send(self, message: BaseEvent) -> None:
114
+ """Structured sending of a BaseEvent.
115
+
116
+ Parameters
117
+ ----------
118
+ message : BaseEvent
119
+ The message to send.
120
+ """
121
+ message_dump = message.model_dump(mode="json")
122
+ if message_dump.get("type") == "text":
123
+ content_block = message_dump.get("content")
124
+ if (
125
+ isinstance(content_block, dict)
126
+ and "content" in content_block
127
+ and isinstance(content_block["content"], str)
128
+ ):
129
+ inner_content = content_block["content"]
130
+ content_block["content"] = try_parse_maybe_serialized(
131
+ inner_content
132
+ )
133
+ print(json.dumps(message_dump), flush=True, file=sys.stdout)
134
+
135
+ # noinspection PyMethodMayBeStatic
136
+ # pylint: disable=no-self-use
137
+ def _send_input_request(
138
+ self,
139
+ prompt: str,
140
+ request_id: str,
141
+ password: bool,
142
+ ) -> None:
143
+ payload = UserInputRequest(
144
+ request_id=request_id,
145
+ prompt=prompt,
146
+ password=password,
147
+ ).model_dump(mode="json")
148
+ print(json.dumps(payload), flush=True)
149
+
150
+ def _read_user_input(
151
+ self,
152
+ prompt: str,
153
+ password: bool,
154
+ request_id: str,
155
+ ) -> str:
156
+ input_queue: queue.Queue[str] = queue.Queue()
157
+
158
+ def read_input() -> None:
159
+ """Read user input from stdin."""
160
+ try:
161
+ user_input = (
162
+ getpass(prompt).strip()
163
+ if password
164
+ else input(prompt).strip()
165
+ )
166
+ input_queue.put(user_input)
167
+ except EOFError:
168
+ input_queue.put("")
169
+
170
+ input_thread = threading.Thread(target=read_input, daemon=True)
171
+ input_thread.start()
172
+
173
+ try:
174
+ return input_queue.get(timeout=self.timeout)
175
+ except queue.Empty:
176
+ self._send_timeout_message(request_id)
177
+ return ""
178
+
179
+ def _send_timeout_message(self, request_id: str) -> None:
180
+ timeout_payload = {
181
+ "id": gen_id(),
182
+ "type": "timeout",
183
+ "request_id": request_id,
184
+ "timestamp": now(),
185
+ "data": f"No input received after {self.timeout} seconds.",
186
+ }
187
+ print(json.dumps(timeout_payload), flush=True, file=sys.stderr)
188
+
189
+ def _handle_user_input(
190
+ self, user_input_raw: str, request_id: str
191
+ ) -> UserResponse:
192
+ """Handle user input and return the appropriate response.
193
+
194
+ Parameters
195
+ ----------
196
+ user_input_raw : str
197
+ The raw user input string.
198
+ request_id : str
199
+ The request ID to match against.
200
+
201
+ Returns
202
+ -------
203
+ UserResponse
204
+ The structured user response.
205
+ """
206
+ user_input = self._load_user_input(user_input_raw)
207
+ if isinstance(user_input, str):
208
+ return UserResponse(data=user_input, request_id=request_id)
209
+ return self._parse_user_input(user_input, request_id)
210
+
211
+ @staticmethod
212
+ def _load_user_input(user_input_raw: str) -> str | dict[str, Any]:
213
+ """Load user input from a raw string.
214
+
215
+ Parameters
216
+ ----------
217
+ user_input_raw : str
218
+ The raw user input string.
219
+
220
+ Returns
221
+ -------
222
+ str | dict[str, Any]
223
+ The loaded user input, either as a string or a dictionary.
224
+ """
225
+ response: str | dict[str, Any] = user_input_raw
226
+ try:
227
+ # Attempt to parse the input as JSON
228
+ response = json.loads(user_input_raw)
229
+ except json.JSONDecodeError:
230
+ # If it's not valid JSON, return as is
231
+ # This allows for backwards compatibility with raw text input
232
+ return user_input_raw
233
+ if isinstance(response, str):
234
+ # double inner dumped?
235
+ try:
236
+ response = json.loads(response)
237
+ except json.JSONDecodeError:
238
+ # If it's not valid JSON, return as is
239
+ return response
240
+ if not isinstance(response, dict):
241
+ return str(response)
242
+ if "data" in response and isinstance(response["data"], str):
243
+ # double inner dumped?
244
+ try:
245
+ response["data"] = json.loads(response["data"])
246
+ except json.JSONDecodeError:
247
+ pass
248
+ return response
249
+
250
+ def _parse_user_input(
251
+ self, user_input: dict[str, Any], request_id: str
252
+ ) -> UserResponse:
253
+ """Parse user input and return the appropriate response.
254
+
255
+ Parameters
256
+ ----------
257
+ user_input : str
258
+ The raw user input string.
259
+ request_id : str
260
+ The request ID to match against.
261
+
262
+ Returns
263
+ -------
264
+ UserResponse
265
+ The structured user response.
266
+ """
267
+ # Load the user input
268
+ if user_input.get("request_id") == request_id:
269
+ # We have a valid response to our request
270
+ data = user_input.get("data")
271
+ if not data:
272
+ # let's check if text|image keys are sent (outside data)
273
+ if "image" in user_input or "text" in user_input:
274
+ return UserResponse(
275
+ request_id=request_id,
276
+ data=self._format_multimedia_response(
277
+ request_id=request_id, data=user_input
278
+ ),
279
+ )
280
+ if isinstance(data, list):
281
+ return self._handle_list_response(
282
+ data, # pyright: ignore
283
+ request_id=request_id,
284
+ )
285
+ if not data or not isinstance(data, (str, dict)):
286
+ # No / invalid data provided in the response
287
+ return UserResponse(
288
+ request_id=request_id,
289
+ data="",
290
+ )
291
+ # Process different data types
292
+ if isinstance(data, str):
293
+ # double inner dumped?
294
+ data = self._load_user_input(data)
295
+ if isinstance(data, dict):
296
+ return UserResponse(
297
+ data=self._format_multimedia_response(
298
+ request_id=request_id,
299
+ data=data, # pyright: ignore
300
+ ),
301
+ request_id=request_id,
302
+ )
303
+ # For other types (numbers, bools ,...),
304
+ # let's just convert to string
305
+ return UserResponse(
306
+ data=str(data), request_id=request_id
307
+ ) # pragma: no cover
308
+ # This response doesn't match our request_id, log and return empty
309
+ self._log_mismatched_response(request_id, user_input)
310
+ return UserResponse(
311
+ request_id=request_id,
312
+ data="",
313
+ )
314
+
315
+ def _handle_list_response(
316
+ self,
317
+ data: list[dict[str, Any]],
318
+ request_id: str,
319
+ ) -> UserResponse:
320
+ if len(data) == 0: # pyright: ignore
321
+ # Empty list, return empty response
322
+ return UserResponse(
323
+ request_id=request_id,
324
+ data="",
325
+ )
326
+
327
+ input_data: list[UserInputData] = []
328
+ for entry in data: # pyright: ignore
329
+ # pylint: disable=broad-exception-caught
330
+ try:
331
+ content = UserInputData.model_validate(entry)
332
+ input_data.append(content)
333
+ except Exception as error: # pragma: no cover
334
+ print({"type": "error", "message": str(error)}, file=sys.stderr)
335
+ continue
336
+ if not input_data: # pragma: no cover
337
+ # No valid data in the list, return empty response
338
+ return UserResponse(
339
+ request_id=request_id,
340
+ data="",
341
+ )
342
+ return UserResponse(
343
+ request_id=request_id,
344
+ data=input_data,
345
+ )
346
+
347
+ @staticmethod
348
+ def _log_mismatched_response(expected_id: str, response: Any) -> None:
349
+ """Log information about mismatched response IDs.
350
+
351
+ Parameters
352
+ ----------
353
+ expected_id : str
354
+ The request ID we were expecting
355
+ response : Any
356
+ The response received
357
+ """
358
+ # Create a log message
359
+ log_payload: dict[str, Any] = {
360
+ "type": "warning",
361
+ "id": uuid4().hex,
362
+ "timestamp": now(),
363
+ "data": {
364
+ "message": (
365
+ "Received response with mismatched request_id. "
366
+ f"Expected: {expected_id}"
367
+ ),
368
+ "details": {
369
+ "expected_id": expected_id,
370
+ "received": str(response)[:100]
371
+ + ("..." if len(str(response)) > 100 else ""),
372
+ },
373
+ },
374
+ }
375
+ # Print to stderr to avoid interfering with stdout communication
376
+ print(json.dumps(log_payload), file=sys.stderr)
377
+
378
+ def _format_multimedia_response(
379
+ self,
380
+ data: dict[str, Any],
381
+ request_id: str | None = None,
382
+ ) -> str:
383
+ """Format a multimedia response dict into a string with image tags.
384
+
385
+ Parameters
386
+ ----------
387
+ data : dict[str, Any]
388
+ The data dictionary containing "image" and "text" keys.
389
+ request_id : str | None
390
+ The input request ID, if available.
391
+
392
+ Returns
393
+ -------
394
+ str
395
+ The formatted string with image tags and text.
396
+ """
397
+ result: list[str] = []
398
+ if "content" in data and isinstance(data["content"], dict):
399
+ return self._format_multimedia_response(
400
+ data=data["content"], # pyright: ignore
401
+ request_id=request_id,
402
+ )
403
+
404
+ # Handle image if present
405
+ if "image" in data and data["image"]:
406
+ image_data = data["image"]
407
+ image = get_image(self.uploads_root, image_data, request_id)
408
+ img_tag = f"<img {image}>"
409
+ result.append(img_tag)
410
+
411
+ # Handle text if present
412
+ if "text" in data and data["text"]:
413
+ result.append(str(data["text"]))
414
+
415
+ # If neither image nor text, return empty string
416
+ if not result:
417
+ return ""
418
+ # Join with a space
419
+ 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