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.
- waldiez/__init__.py +5 -5
- waldiez/_version.py +1 -1
- waldiez/cli.py +97 -102
- waldiez/exporter.py +61 -19
- waldiez/exporting/__init__.py +25 -6
- waldiez/exporting/agent/__init__.py +7 -3
- waldiez/exporting/agent/code_execution.py +114 -0
- waldiez/exporting/agent/exporter.py +354 -0
- waldiez/exporting/agent/extras/__init__.py +15 -0
- waldiez/exporting/agent/extras/captain_agent_extras.py +315 -0
- waldiez/exporting/agent/extras/group/target.py +178 -0
- waldiez/exporting/agent/extras/group_manager_agent_extas.py +500 -0
- waldiez/exporting/agent/extras/group_member_extras.py +181 -0
- waldiez/exporting/agent/extras/handoffs/__init__.py +19 -0
- waldiez/exporting/agent/extras/handoffs/after_work.py +78 -0
- waldiez/exporting/agent/extras/handoffs/available.py +74 -0
- waldiez/exporting/agent/extras/handoffs/condition.py +158 -0
- waldiez/exporting/agent/extras/handoffs/handoff.py +171 -0
- waldiez/exporting/agent/extras/handoffs/target.py +189 -0
- waldiez/exporting/agent/extras/rag/__init__.py +10 -0
- waldiez/exporting/agent/{utils/rag_user/chroma_utils.py → extras/rag/chroma_extras.py} +37 -24
- waldiez/exporting/agent/{utils/rag_user/mongo_utils.py → extras/rag/mongo_extras.py} +10 -10
- waldiez/exporting/agent/{utils/rag_user/pgvector_utils.py → extras/rag/pgvector_extras.py} +13 -13
- waldiez/exporting/agent/{utils/rag_user/qdrant_utils.py → extras/rag/qdrant_extras.py} +13 -13
- waldiez/exporting/agent/{utils/rag_user/vector_db.py → extras/rag/vector_db_extras.py} +59 -46
- waldiez/exporting/agent/extras/rag_user_proxy_agent_extras.py +245 -0
- waldiez/exporting/agent/extras/reasoning_agent_extras.py +88 -0
- waldiez/exporting/agent/factory.py +95 -0
- waldiez/exporting/agent/processor.py +150 -0
- waldiez/exporting/agent/system_message.py +36 -0
- waldiez/exporting/agent/termination.py +50 -0
- waldiez/exporting/chats/__init__.py +7 -3
- waldiez/exporting/chats/exporter.py +97 -0
- waldiez/exporting/chats/factory.py +65 -0
- waldiez/exporting/chats/processor.py +226 -0
- waldiez/exporting/chats/utils/__init__.py +6 -5
- waldiez/exporting/chats/utils/common.py +11 -45
- waldiez/exporting/chats/utils/group.py +55 -0
- waldiez/exporting/chats/utils/nested.py +37 -52
- waldiez/exporting/chats/utils/sequential.py +72 -61
- waldiez/exporting/chats/utils/{single_chat.py → single.py} +48 -50
- waldiez/exporting/core/__init__.py +196 -0
- waldiez/exporting/core/constants.py +17 -0
- waldiez/exporting/core/content.py +69 -0
- waldiez/exporting/core/context.py +244 -0
- waldiez/exporting/core/enums.py +89 -0
- waldiez/exporting/core/errors.py +19 -0
- waldiez/exporting/core/exporter.py +390 -0
- waldiez/exporting/core/exporters.py +67 -0
- waldiez/exporting/core/extras/__init__.py +39 -0
- waldiez/exporting/core/extras/agent_extras/__init__.py +27 -0
- waldiez/exporting/core/extras/agent_extras/captain_extras.py +57 -0
- waldiez/exporting/core/extras/agent_extras/group_manager_extras.py +102 -0
- waldiez/exporting/core/extras/agent_extras/rag_user_extras.py +53 -0
- waldiez/exporting/core/extras/agent_extras/reasoning_extras.py +68 -0
- waldiez/exporting/core/extras/agent_extras/standard_extras.py +263 -0
- waldiez/exporting/core/extras/base.py +241 -0
- waldiez/exporting/core/extras/chat_extras.py +118 -0
- waldiez/exporting/core/extras/flow_extras.py +70 -0
- waldiez/exporting/core/extras/model_extras.py +73 -0
- waldiez/exporting/core/extras/path_resolver.py +93 -0
- waldiez/exporting/core/extras/serializer.py +138 -0
- waldiez/exporting/core/extras/tool_extras.py +82 -0
- waldiez/exporting/core/protocols.py +259 -0
- waldiez/exporting/core/result.py +705 -0
- waldiez/exporting/core/types.py +329 -0
- waldiez/exporting/core/utils/__init__.py +11 -0
- waldiez/exporting/core/utils/comment.py +33 -0
- waldiez/exporting/core/utils/llm_config.py +117 -0
- waldiez/exporting/core/validation.py +96 -0
- waldiez/exporting/flow/__init__.py +6 -2
- waldiez/exporting/flow/execution_generator.py +193 -0
- waldiez/exporting/flow/exporter.py +107 -0
- waldiez/exporting/flow/factory.py +94 -0
- waldiez/exporting/flow/file_generator.py +214 -0
- waldiez/exporting/flow/merger.py +387 -0
- waldiez/exporting/flow/orchestrator.py +411 -0
- waldiez/exporting/flow/utils/__init__.py +9 -36
- waldiez/exporting/flow/utils/common.py +206 -0
- waldiez/exporting/flow/utils/importing.py +373 -0
- waldiez/exporting/flow/utils/linting.py +200 -0
- waldiez/exporting/flow/utils/{logging_utils.py → logging.py} +23 -9
- waldiez/exporting/models/__init__.py +3 -1
- waldiez/exporting/models/exporter.py +233 -0
- waldiez/exporting/models/factory.py +66 -0
- waldiez/exporting/models/processor.py +139 -0
- waldiez/exporting/tools/__init__.py +11 -0
- waldiez/exporting/tools/exporter.py +207 -0
- waldiez/exporting/tools/factory.py +57 -0
- waldiez/exporting/tools/processor.py +248 -0
- waldiez/exporting/tools/registration.py +133 -0
- waldiez/io/__init__.py +128 -0
- waldiez/io/_ws.py +199 -0
- waldiez/io/models/__init__.py +60 -0
- waldiez/io/models/base.py +66 -0
- waldiez/io/models/constants.py +78 -0
- waldiez/io/models/content/__init__.py +23 -0
- waldiez/io/models/content/audio.py +43 -0
- waldiez/io/models/content/base.py +45 -0
- waldiez/io/models/content/file.py +43 -0
- waldiez/io/models/content/image.py +96 -0
- waldiez/io/models/content/text.py +37 -0
- waldiez/io/models/content/video.py +43 -0
- waldiez/io/models/user_input.py +269 -0
- waldiez/io/models/user_response.py +215 -0
- waldiez/io/mqtt.py +681 -0
- waldiez/io/redis.py +782 -0
- waldiez/io/structured.py +439 -0
- waldiez/io/utils.py +184 -0
- waldiez/io/ws.py +298 -0
- waldiez/logger.py +481 -0
- waldiez/models/__init__.py +108 -51
- waldiez/models/agents/__init__.py +34 -70
- waldiez/models/agents/agent/__init__.py +10 -4
- waldiez/models/agents/agent/agent.py +466 -65
- waldiez/models/agents/agent/agent_data.py +119 -47
- waldiez/models/agents/agent/agent_type.py +13 -2
- waldiez/models/agents/agent/code_execution.py +12 -12
- waldiez/models/agents/agent/human_input_mode.py +8 -0
- waldiez/models/agents/agent/{linked_skill.py → linked_tool.py} +7 -7
- waldiez/models/agents/agent/nested_chat.py +35 -7
- waldiez/models/agents/agent/termination_message.py +30 -22
- waldiez/models/agents/{swarm_agent → agent}/update_system_message.py +22 -22
- waldiez/models/agents/agents.py +58 -63
- waldiez/models/agents/assistant/assistant.py +4 -4
- waldiez/models/agents/assistant/assistant_data.py +13 -1
- waldiez/models/agents/{captain_agent → captain}/captain_agent.py +5 -5
- waldiez/models/agents/{captain_agent → captain}/captain_agent_data.py +5 -5
- waldiez/models/agents/extra_requirements.py +11 -16
- waldiez/models/agents/group_manager/group_manager.py +103 -13
- waldiez/models/agents/group_manager/group_manager_data.py +36 -14
- waldiez/models/agents/group_manager/speakers.py +77 -24
- waldiez/models/agents/{rag_user → rag_user_proxy}/__init__.py +16 -16
- waldiez/models/agents/rag_user_proxy/rag_user_proxy.py +64 -0
- waldiez/models/agents/{rag_user/rag_user_data.py → rag_user_proxy/rag_user_proxy_data.py} +6 -5
- waldiez/models/agents/{rag_user → rag_user_proxy}/retrieve_config.py +182 -114
- waldiez/models/agents/{rag_user → rag_user_proxy}/vector_db_config.py +13 -13
- waldiez/models/agents/reasoning/reasoning_agent.py +6 -6
- waldiez/models/agents/reasoning/reasoning_agent_data.py +110 -63
- waldiez/models/agents/reasoning/reasoning_agent_reason_config.py +38 -10
- waldiez/models/agents/user_proxy/user_proxy.py +11 -7
- waldiez/models/agents/user_proxy/user_proxy_data.py +2 -2
- waldiez/models/chat/__init__.py +2 -1
- waldiez/models/chat/chat.py +166 -87
- waldiez/models/chat/chat_data.py +99 -136
- waldiez/models/chat/chat_message.py +33 -23
- waldiez/models/chat/chat_nested.py +31 -30
- waldiez/models/chat/chat_summary.py +10 -8
- waldiez/models/common/__init__.py +52 -2
- waldiez/models/common/ag2_version.py +1 -1
- waldiez/models/common/base.py +38 -7
- waldiez/models/common/dict_utils.py +42 -17
- waldiez/models/common/handoff.py +459 -0
- waldiez/models/common/id_generator.py +19 -0
- waldiez/models/common/method_utils.py +130 -68
- waldiez/{exporting/base/utils → models/common}/naming.py +38 -61
- waldiez/models/common/waldiez_version.py +37 -0
- waldiez/models/flow/__init__.py +9 -2
- waldiez/models/flow/connection.py +18 -0
- waldiez/models/flow/flow.py +311 -215
- waldiez/models/flow/flow_data.py +207 -40
- waldiez/models/flow/info.py +85 -0
- waldiez/models/flow/naming.py +131 -0
- waldiez/models/model/__init__.py +7 -1
- waldiez/models/model/extra_requirements.py +3 -12
- waldiez/models/model/model.py +76 -21
- waldiez/models/model/model_data.py +108 -20
- waldiez/models/tool/__init__.py +16 -0
- waldiez/models/tool/extra_requirements.py +36 -0
- waldiez/models/{skill/skill.py → tool/tool.py} +88 -88
- waldiez/models/tool/tool_data.py +51 -0
- waldiez/models/tool/tool_type.py +8 -0
- waldiez/models/waldiez.py +97 -80
- waldiez/runner.py +115 -61
- waldiez/running/__init__.py +13 -7
- waldiez/running/environment.py +49 -68
- waldiez/running/gen_seq_diagram.py +16 -14
- waldiez/running/post_run.py +119 -0
- waldiez/running/pre_run.py +149 -0
- waldiez/running/util.py +134 -0
- waldiez/utils/__init__.py +2 -4
- waldiez/utils/cli_extras/jupyter.py +5 -3
- waldiez/utils/cli_extras/runner.py +6 -4
- waldiez/utils/cli_extras/studio.py +6 -4
- waldiez/utils/conflict_checker.py +15 -9
- waldiez/utils/flaml_warnings.py +5 -5
- waldiez/utils/version.py +47 -0
- {waldiez-0.4.7.dist-info → waldiez-0.4.9.dist-info}/METADATA +235 -91
- waldiez-0.4.9.dist-info/RECORD +203 -0
- waldiez/exporting/agent/agent_exporter.py +0 -297
- waldiez/exporting/agent/utils/__init__.py +0 -23
- waldiez/exporting/agent/utils/captain_agent.py +0 -263
- waldiez/exporting/agent/utils/code_execution.py +0 -65
- waldiez/exporting/agent/utils/group_manager.py +0 -220
- waldiez/exporting/agent/utils/rag_user/__init__.py +0 -7
- waldiez/exporting/agent/utils/rag_user/rag_user.py +0 -209
- waldiez/exporting/agent/utils/reasoning.py +0 -36
- waldiez/exporting/agent/utils/swarm_agent.py +0 -469
- waldiez/exporting/agent/utils/teachability.py +0 -41
- waldiez/exporting/agent/utils/termination_message.py +0 -44
- waldiez/exporting/base/__init__.py +0 -25
- waldiez/exporting/base/agent_position.py +0 -75
- waldiez/exporting/base/base_exporter.py +0 -118
- waldiez/exporting/base/export_position.py +0 -48
- waldiez/exporting/base/import_position.py +0 -23
- waldiez/exporting/base/mixin.py +0 -137
- waldiez/exporting/base/utils/__init__.py +0 -18
- waldiez/exporting/base/utils/comments.py +0 -96
- waldiez/exporting/base/utils/path_check.py +0 -68
- waldiez/exporting/base/utils/to_string.py +0 -84
- waldiez/exporting/chats/chats_exporter.py +0 -240
- waldiez/exporting/chats/utils/swarm.py +0 -210
- waldiez/exporting/flow/flow_exporter.py +0 -528
- waldiez/exporting/flow/utils/agent_utils.py +0 -204
- waldiez/exporting/flow/utils/chat_utils.py +0 -71
- waldiez/exporting/flow/utils/def_main.py +0 -77
- waldiez/exporting/flow/utils/flow_content.py +0 -202
- waldiez/exporting/flow/utils/flow_names.py +0 -116
- waldiez/exporting/flow/utils/importing_utils.py +0 -227
- waldiez/exporting/models/models_exporter.py +0 -199
- waldiez/exporting/models/utils.py +0 -174
- waldiez/exporting/skills/__init__.py +0 -9
- waldiez/exporting/skills/skills_exporter.py +0 -176
- waldiez/exporting/skills/utils.py +0 -369
- waldiez/models/agents/agent/teachability.py +0 -70
- waldiez/models/agents/rag_user/rag_user.py +0 -60
- waldiez/models/agents/swarm_agent/__init__.py +0 -50
- waldiez/models/agents/swarm_agent/after_work.py +0 -179
- waldiez/models/agents/swarm_agent/on_condition.py +0 -105
- waldiez/models/agents/swarm_agent/on_condition_available.py +0 -142
- waldiez/models/agents/swarm_agent/on_condition_target.py +0 -40
- waldiez/models/agents/swarm_agent/swarm_agent.py +0 -107
- waldiez/models/agents/swarm_agent/swarm_agent_data.py +0 -124
- waldiez/models/flow/utils.py +0 -232
- waldiez/models/skill/__init__.py +0 -16
- waldiez/models/skill/extra_requirements.py +0 -36
- waldiez/models/skill/skill_data.py +0 -53
- waldiez/models/skill/skill_type.py +0 -8
- waldiez/running/running.py +0 -369
- waldiez/utils/pysqlite3_checker.py +0 -308
- waldiez/utils/rdps_checker.py +0 -122
- waldiez-0.4.7.dist-info/RECORD +0 -149
- /waldiez/models/agents/{captain_agent → captain}/__init__.py +0 -0
- /waldiez/models/agents/{captain_agent → captain}/captain_agent_lib_entry.py +0 -0
- {waldiez-0.4.7.dist-info → waldiez-0.4.9.dist-info}/WHEEL +0 -0
- {waldiez-0.4.7.dist-info → waldiez-0.4.9.dist-info}/entry_points.txt +0 -0
- {waldiez-0.4.7.dist-info → waldiez-0.4.9.dist-info}/licenses/LICENSE +0 -0
- {waldiez-0.4.7.dist-info → waldiez-0.4.9.dist-info}/licenses/NOTICE.md +0 -0
waldiez/io/structured.py
ADDED
|
@@ -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
|