waldiez 0.6.0__py3-none-any.whl → 0.6.1__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 +1 -1
- waldiez/_version.py +1 -1
- waldiez/cli.py +18 -7
- waldiez/cli_extras/jupyter.py +3 -0
- waldiez/cli_extras/runner.py +3 -1
- waldiez/cli_extras/studio.py +3 -1
- waldiez/exporter.py +9 -3
- waldiez/exporting/agent/exporter.py +9 -10
- waldiez/exporting/agent/extras/captain_agent_extras.py +6 -6
- waldiez/exporting/agent/extras/doc_agent_extras.py +6 -6
- waldiez/exporting/agent/extras/group_manager_agent_extas.py +34 -23
- waldiez/exporting/agent/extras/group_member_extras.py +6 -5
- waldiez/exporting/agent/extras/handoffs/after_work.py +1 -1
- waldiez/exporting/agent/extras/handoffs/available.py +1 -1
- waldiez/exporting/agent/extras/handoffs/condition.py +3 -2
- waldiez/exporting/agent/extras/handoffs/handoff.py +1 -1
- waldiez/exporting/agent/extras/handoffs/target.py +6 -4
- waldiez/exporting/agent/extras/rag/chroma_extras.py +27 -19
- waldiez/exporting/agent/extras/rag/mongo_extras.py +8 -8
- waldiez/exporting/agent/extras/rag/pgvector_extras.py +5 -5
- waldiez/exporting/agent/extras/rag/qdrant_extras.py +5 -4
- waldiez/exporting/agent/extras/rag/vector_db_extras.py +1 -1
- waldiez/exporting/agent/extras/rag_user_proxy_agent_extras.py +5 -7
- waldiez/exporting/agent/extras/reasoning_agent_extras.py +3 -5
- waldiez/exporting/chats/exporter.py +4 -4
- waldiez/exporting/chats/processor.py +1 -2
- waldiez/exporting/chats/utils/common.py +89 -48
- waldiez/exporting/chats/utils/group.py +9 -9
- waldiez/exporting/chats/utils/nested.py +7 -7
- waldiez/exporting/chats/utils/sequential.py +1 -1
- waldiez/exporting/chats/utils/single.py +2 -2
- waldiez/exporting/core/content.py +7 -7
- waldiez/exporting/core/context.py +5 -3
- waldiez/exporting/core/exporter.py +5 -3
- waldiez/exporting/core/exporters.py +2 -2
- waldiez/exporting/core/extras/agent_extras/captain_extras.py +2 -2
- waldiez/exporting/core/extras/agent_extras/group_manager_extras.py +2 -2
- waldiez/exporting/core/extras/agent_extras/rag_user_extras.py +2 -2
- waldiez/exporting/core/extras/agent_extras/standard_extras.py +3 -8
- waldiez/exporting/core/extras/base.py +7 -5
- waldiez/exporting/core/extras/flow_extras.py +4 -5
- waldiez/exporting/core/extras/model_extras.py +2 -2
- waldiez/exporting/core/extras/path_resolver.py +1 -2
- waldiez/exporting/core/extras/serializer.py +2 -2
- waldiez/exporting/core/protocols.py +6 -5
- waldiez/exporting/core/result.py +25 -28
- waldiez/exporting/core/types.py +10 -10
- waldiez/exporting/core/utils/llm_config.py +2 -2
- waldiez/exporting/core/validation.py +10 -11
- waldiez/exporting/flow/execution_generator.py +98 -10
- waldiez/exporting/flow/exporter.py +2 -2
- waldiez/exporting/flow/factory.py +2 -2
- waldiez/exporting/flow/file_generator.py +4 -2
- waldiez/exporting/flow/merger.py +5 -3
- waldiez/exporting/flow/orchestrator.py +72 -2
- waldiez/exporting/flow/utils/common.py +5 -5
- waldiez/exporting/flow/utils/importing.py +6 -7
- waldiez/exporting/flow/utils/linting.py +25 -9
- waldiez/exporting/flow/utils/logging.py +2 -2
- waldiez/exporting/models/exporter.py +8 -8
- waldiez/exporting/models/processor.py +5 -5
- waldiez/exporting/tools/exporter.py +2 -2
- waldiez/exporting/tools/processor.py +7 -4
- waldiez/io/__init__.py +8 -4
- waldiez/io/_ws.py +10 -6
- waldiez/io/models/constants.py +10 -10
- waldiez/io/models/content/audio.py +1 -0
- waldiez/io/models/content/base.py +20 -18
- waldiez/io/models/content/file.py +1 -0
- waldiez/io/models/content/image.py +1 -0
- waldiez/io/models/content/text.py +1 -0
- waldiez/io/models/content/video.py +1 -0
- waldiez/io/models/user_input.py +10 -5
- waldiez/io/models/user_response.py +17 -16
- waldiez/io/mqtt.py +18 -31
- waldiez/io/redis.py +18 -22
- waldiez/io/structured.py +52 -53
- waldiez/io/utils.py +3 -0
- waldiez/io/ws.py +5 -1
- waldiez/logger.py +16 -3
- waldiez/models/agents/__init__.py +3 -0
- waldiez/models/agents/agent/agent.py +23 -16
- waldiez/models/agents/agent/agent_data.py +25 -22
- waldiez/models/agents/agent/code_execution.py +9 -11
- waldiez/models/agents/agent/termination_message.py +10 -12
- waldiez/models/agents/agent/update_system_message.py +2 -4
- waldiez/models/agents/agents.py +8 -8
- waldiez/models/agents/assistant/assistant.py +6 -3
- waldiez/models/agents/assistant/assistant_data.py +2 -2
- waldiez/models/agents/captain/captain_agent.py +7 -4
- waldiez/models/agents/captain/captain_agent_data.py +5 -7
- waldiez/models/agents/doc_agent/doc_agent.py +7 -4
- waldiez/models/agents/doc_agent/doc_agent_data.py +9 -10
- waldiez/models/agents/doc_agent/rag_query_engine.py +10 -12
- waldiez/models/agents/extra_requirements.py +3 -3
- waldiez/models/agents/group_manager/group_manager.py +12 -7
- waldiez/models/agents/group_manager/group_manager_data.py +13 -12
- waldiez/models/agents/group_manager/speakers.py +17 -19
- waldiez/models/agents/rag_user_proxy/rag_user_proxy.py +7 -4
- waldiez/models/agents/rag_user_proxy/rag_user_proxy_data.py +4 -1
- waldiez/models/agents/rag_user_proxy/retrieve_config.py +69 -63
- waldiez/models/agents/rag_user_proxy/vector_db_config.py +19 -19
- waldiez/models/agents/reasoning/reasoning_agent.py +7 -4
- waldiez/models/agents/reasoning/reasoning_agent_data.py +3 -2
- waldiez/models/agents/reasoning/reasoning_agent_reason_config.py +8 -8
- waldiez/models/agents/user_proxy/user_proxy.py +6 -3
- waldiez/models/agents/user_proxy/user_proxy_data.py +1 -1
- waldiez/models/chat/chat.py +27 -20
- waldiez/models/chat/chat_data.py +22 -19
- waldiez/models/chat/chat_message.py +9 -9
- waldiez/models/chat/chat_nested.py +9 -9
- waldiez/models/chat/chat_summary.py +6 -6
- waldiez/models/common/__init__.py +2 -0
- waldiez/models/common/ag2_version.py +2 -0
- waldiez/models/common/dict_utils.py +8 -6
- waldiez/models/common/handoff.py +18 -17
- waldiez/models/common/method_utils.py +7 -7
- waldiez/models/common/naming.py +49 -0
- waldiez/models/flow/flow.py +11 -6
- waldiez/models/flow/flow_data.py +23 -17
- waldiez/models/flow/info.py +3 -3
- waldiez/models/flow/naming.py +2 -1
- waldiez/models/model/_aws.py +11 -13
- waldiez/models/model/_llm.py +5 -0
- waldiez/models/model/_price.py +2 -4
- waldiez/models/model/extra_requirements.py +1 -3
- waldiez/models/model/model.py +2 -2
- waldiez/models/model/model_data.py +21 -21
- waldiez/models/tool/extra_requirements.py +2 -4
- waldiez/models/tool/predefined/_duckduckgo.py +1 -0
- waldiez/models/tool/predefined/_email.py +1 -0
- waldiez/models/tool/predefined/_google.py +1 -0
- waldiez/models/tool/predefined/_perplexity.py +1 -0
- waldiez/models/tool/predefined/_searxng.py +1 -0
- waldiez/models/tool/predefined/_tavily.py +1 -0
- waldiez/models/tool/predefined/_wikipedia.py +1 -0
- waldiez/models/tool/predefined/_youtube.py +1 -0
- waldiez/models/tool/tool.py +8 -5
- waldiez/models/tool/tool_data.py +2 -2
- waldiez/models/waldiez.py +152 -4
- waldiez/runner.py +11 -5
- waldiez/running/async_utils.py +192 -0
- waldiez/running/base_runner.py +117 -264
- waldiez/running/dir_utils.py +52 -0
- waldiez/running/environment.py +10 -44
- waldiez/running/events_mixin.py +252 -0
- waldiez/running/exceptions.py +20 -0
- waldiez/running/gen_seq_diagram.py +18 -15
- waldiez/running/io_utils.py +216 -0
- waldiez/running/protocol.py +11 -5
- waldiez/running/requirements_mixin.py +65 -0
- waldiez/running/results_mixin.py +926 -0
- waldiez/running/standard_runner.py +22 -25
- waldiez/running/step_by_step/breakpoints_mixin.py +192 -60
- waldiez/running/step_by_step/command_handler.py +3 -0
- waldiez/running/step_by_step/events_processor.py +194 -14
- waldiez/running/step_by_step/step_by_step_models.py +110 -43
- waldiez/running/step_by_step/step_by_step_runner.py +107 -57
- waldiez/running/subprocess_runner/__base__.py +9 -1
- waldiez/running/subprocess_runner/_async_runner.py +5 -3
- waldiez/running/subprocess_runner/_sync_runner.py +6 -2
- waldiez/running/subprocess_runner/runner.py +39 -23
- waldiez/running/timeline_processor.py +1 -1
- waldiez/utils/__init__.py +2 -0
- waldiez/utils/conflict_checker.py +4 -4
- waldiez/utils/python_manager.py +415 -0
- waldiez/ws/_file_handler.py +18 -18
- waldiez/ws/_mock.py +2 -1
- waldiez/ws/cli.py +36 -12
- waldiez/ws/client_manager.py +35 -27
- waldiez/ws/errors.py +3 -0
- waldiez/ws/models.py +43 -52
- waldiez/ws/reloader.py +12 -4
- waldiez/ws/server.py +85 -55
- waldiez/ws/session_manager.py +8 -9
- waldiez/ws/session_stats.py +1 -1
- waldiez/ws/utils.py +4 -1
- {waldiez-0.6.0.dist-info → waldiez-0.6.1.dist-info}/METADATA +82 -93
- waldiez-0.6.1.dist-info/RECORD +254 -0
- waldiez/running/post_run.py +0 -186
- waldiez/running/pre_run.py +0 -281
- waldiez/running/run_results.py +0 -14
- waldiez/running/utils.py +0 -625
- waldiez-0.6.0.dist-info/RECORD +0 -251
- {waldiez-0.6.0.dist-info → waldiez-0.6.1.dist-info}/WHEEL +0 -0
- {waldiez-0.6.0.dist-info → waldiez-0.6.1.dist-info}/entry_points.txt +0 -0
- {waldiez-0.6.0.dist-info → waldiez-0.6.1.dist-info}/licenses/LICENSE +0 -0
- {waldiez-0.6.0.dist-info → waldiez-0.6.1.dist-info}/licenses/NOTICE.md +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# SPDX-License-Identifier: Apache-2.0.
|
|
2
2
|
# Copyright (c) 2024 - 2025 Waldiez and contributors.
|
|
3
3
|
|
|
4
|
-
# pylint: disable=line-too-long
|
|
4
|
+
# pylint: disable=duplicate-code,line-too-long
|
|
5
5
|
# pyright: reportUnknownMemberType=false, reportAttributeAccessIssue=false
|
|
6
6
|
# pyright: reportUnknownArgumentType=false, reportOptionalMemberAccess=false
|
|
7
|
-
#
|
|
7
|
+
# pyright: reportDeprecated=false, reportMissingTypeStubs=false
|
|
8
|
+
# pyright: reportUnsafeMultipleInheritance=false
|
|
8
9
|
# flake8: noqa: E501
|
|
9
10
|
|
|
10
11
|
"""Step-by-step Waldiez runner with user interaction capabilities."""
|
|
@@ -12,20 +13,23 @@
|
|
|
12
13
|
import asyncio
|
|
13
14
|
import threading
|
|
14
15
|
import traceback
|
|
16
|
+
import uuid
|
|
15
17
|
from collections import deque
|
|
18
|
+
from collections.abc import Iterable
|
|
16
19
|
from pathlib import Path
|
|
17
|
-
from typing import TYPE_CHECKING, Any,
|
|
20
|
+
from typing import TYPE_CHECKING, Any, Union
|
|
18
21
|
|
|
19
22
|
from pydantic import ValidationError
|
|
23
|
+
from typing_extensions import override
|
|
20
24
|
|
|
21
|
-
from waldiez.io.utils import DEBUG_INPUT_PROMPT, gen_id
|
|
22
25
|
from waldiez.models.waldiez import Waldiez
|
|
23
26
|
from waldiez.running.step_by_step.command_handler import CommandHandler
|
|
24
27
|
from waldiez.running.step_by_step.events_processor import EventProcessor
|
|
25
28
|
|
|
26
29
|
from ..base_runner import WaldiezBaseRunner
|
|
30
|
+
from ..events_mixin import EventsMixin
|
|
27
31
|
from ..exceptions import StopRunningException
|
|
28
|
-
from ..
|
|
32
|
+
from ..results_mixin import WaldiezRunResults
|
|
29
33
|
from .breakpoints_mixin import BreakpointsMixin
|
|
30
34
|
from .step_by_step_models import (
|
|
31
35
|
VALID_CONTROL_COMMANDS,
|
|
@@ -40,10 +44,10 @@ from .step_by_step_models import (
|
|
|
40
44
|
)
|
|
41
45
|
|
|
42
46
|
if TYPE_CHECKING:
|
|
47
|
+
from autogen.agentchat import ConversableAgent # type: ignore
|
|
43
48
|
from autogen.events import BaseEvent # type: ignore
|
|
44
49
|
from autogen.messages import BaseMessage # type: ignore
|
|
45
50
|
|
|
46
|
-
|
|
47
51
|
MESSAGES = {
|
|
48
52
|
"workflow_starting": "<Waldiez step-by-step> - Starting workflow...",
|
|
49
53
|
"workflow_finished": "<Waldiez step-by-step> - Workflow finished",
|
|
@@ -52,6 +56,21 @@ MESSAGES = {
|
|
|
52
56
|
"<Waldiez step-by-step> - Workflow execution failed: {error}"
|
|
53
57
|
),
|
|
54
58
|
}
|
|
59
|
+
DEBUG_INPUT_PROMPT = (
|
|
60
|
+
# cspell: disable-next-line
|
|
61
|
+
"[Step] (c)ontinue, (r)un, (q)uit, (i)nfo, (h)elp, (st)ats: "
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def gen_id() -> str:
|
|
66
|
+
"""Generate a new id.
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
str
|
|
71
|
+
The new id.
|
|
72
|
+
"""
|
|
73
|
+
return str(uuid.uuid4())
|
|
55
74
|
|
|
56
75
|
|
|
57
76
|
# pylint: disable=too-many-instance-attributes
|
|
@@ -67,7 +86,7 @@ class WaldiezStepByStepRunner(WaldiezBaseRunner, BreakpointsMixin):
|
|
|
67
86
|
structured_io: bool = False,
|
|
68
87
|
dot_env: str | Path | None = None,
|
|
69
88
|
auto_continue: bool = False,
|
|
70
|
-
breakpoints: Iterable[
|
|
89
|
+
breakpoints: Iterable[Any] | None = None,
|
|
71
90
|
config: WaldiezDebugConfig | None = None,
|
|
72
91
|
**kwargs: Any,
|
|
73
92
|
) -> None:
|
|
@@ -80,7 +99,8 @@ class WaldiezStepByStepRunner(WaldiezBaseRunner, BreakpointsMixin):
|
|
|
80
99
|
dot_env=dot_env,
|
|
81
100
|
**kwargs,
|
|
82
101
|
)
|
|
83
|
-
BreakpointsMixin.__init__(self)
|
|
102
|
+
BreakpointsMixin.__init__(self, config=config)
|
|
103
|
+
self.set_agent_id_to_name(waldiez.flow.unique_names["agent_names"])
|
|
84
104
|
|
|
85
105
|
# Configuration
|
|
86
106
|
self._config = config or WaldiezDebugConfig()
|
|
@@ -112,6 +132,8 @@ class WaldiezStepByStepRunner(WaldiezBaseRunner, BreakpointsMixin):
|
|
|
112
132
|
# Command handling
|
|
113
133
|
self._command_handler = CommandHandler(self)
|
|
114
134
|
self._event_processor = EventProcessor(self)
|
|
135
|
+
auto_run = self.is_auto_run()
|
|
136
|
+
self._config.auto_continue = auto_run
|
|
115
137
|
|
|
116
138
|
@property
|
|
117
139
|
def auto_continue(self) -> bool:
|
|
@@ -207,19 +229,6 @@ class WaldiezStepByStepRunner(WaldiezBaseRunner, BreakpointsMixin):
|
|
|
207
229
|
"""Get the maximum event history size."""
|
|
208
230
|
return self._config.max_event_history
|
|
209
231
|
|
|
210
|
-
@staticmethod
|
|
211
|
-
def print(*args: Any, **kwargs: Any) -> None:
|
|
212
|
-
"""Print method.
|
|
213
|
-
|
|
214
|
-
Parameters
|
|
215
|
-
----------
|
|
216
|
-
*args : Any
|
|
217
|
-
Positional arguments to print.
|
|
218
|
-
**kwargs : Any
|
|
219
|
-
Keyword arguments to print.
|
|
220
|
-
"""
|
|
221
|
-
WaldiezBaseRunner.print(*args, **kwargs)
|
|
222
|
-
|
|
223
232
|
def add_to_history(self, event_info: dict[str, Any]) -> None:
|
|
224
233
|
"""Add an event to the history.
|
|
225
234
|
|
|
@@ -259,6 +268,7 @@ class WaldiezStepByStepRunner(WaldiezBaseRunner, BreakpointsMixin):
|
|
|
259
268
|
self.emit(WaldiezDebugEventInfo(event=event_info))
|
|
260
269
|
|
|
261
270
|
# noinspection PyTypeHints
|
|
271
|
+
@override
|
|
262
272
|
def emit(self, message: WaldiezDebugMessage) -> None:
|
|
263
273
|
"""Emit a debug message.
|
|
264
274
|
|
|
@@ -463,16 +473,25 @@ class WaldiezStepByStepRunner(WaldiezBaseRunner, BreakpointsMixin):
|
|
|
463
473
|
|
|
464
474
|
return self._command_handler.handle_command(user_input or "")
|
|
465
475
|
|
|
466
|
-
def _get_user_action(self) -> WaldiezDebugStepAction:
|
|
467
|
-
"""Get user action with timeout support.
|
|
476
|
+
def _get_user_action(self, force: bool) -> WaldiezDebugStepAction:
|
|
477
|
+
"""Get user action with timeout support.
|
|
478
|
+
|
|
479
|
+
Parameters
|
|
480
|
+
----------
|
|
481
|
+
force : bool
|
|
482
|
+
Force getting the user's action, even if in auto-run mode.
|
|
483
|
+
"""
|
|
468
484
|
if self._config.auto_continue:
|
|
469
485
|
self.step_mode = True
|
|
470
|
-
|
|
471
|
-
|
|
486
|
+
if force:
|
|
487
|
+
self._config.auto_continue = False
|
|
488
|
+
else:
|
|
489
|
+
return WaldiezDebugStepAction.CONTINUE
|
|
472
490
|
while True:
|
|
473
491
|
request_id = gen_id()
|
|
474
492
|
try:
|
|
475
493
|
if not self.structured_io:
|
|
494
|
+
# if structured, we already do this (print the prompt)
|
|
476
495
|
self.emit(
|
|
477
496
|
WaldiezDebugInputRequest(
|
|
478
497
|
prompt=DEBUG_INPUT_PROMPT, request_id=request_id
|
|
@@ -481,8 +500,9 @@ class WaldiezStepByStepRunner(WaldiezBaseRunner, BreakpointsMixin):
|
|
|
481
500
|
except Exception as e: # pylint: disable=broad-exception-caught
|
|
482
501
|
self.log.warning("Failed to emit input request: %s", e)
|
|
483
502
|
try:
|
|
484
|
-
user_input =
|
|
485
|
-
DEBUG_INPUT_PROMPT
|
|
503
|
+
user_input = EventsMixin.get_user_input(
|
|
504
|
+
DEBUG_INPUT_PROMPT,
|
|
505
|
+
request_id=request_id,
|
|
486
506
|
).strip()
|
|
487
507
|
return self._parse_user_action(
|
|
488
508
|
user_input, request_id=request_id
|
|
@@ -492,11 +512,14 @@ class WaldiezStepByStepRunner(WaldiezBaseRunner, BreakpointsMixin):
|
|
|
492
512
|
self._stop_requested.set()
|
|
493
513
|
return WaldiezDebugStepAction.QUIT
|
|
494
514
|
|
|
495
|
-
async def _a_get_user_action(self) -> WaldiezDebugStepAction:
|
|
515
|
+
async def _a_get_user_action(self, force: bool) -> WaldiezDebugStepAction:
|
|
496
516
|
"""Get user action asynchronously."""
|
|
497
517
|
if self._config.auto_continue:
|
|
498
518
|
self.step_mode = True
|
|
499
|
-
|
|
519
|
+
if force:
|
|
520
|
+
self._config.auto_continue = False
|
|
521
|
+
else:
|
|
522
|
+
return WaldiezDebugStepAction.CONTINUE
|
|
500
523
|
|
|
501
524
|
while True:
|
|
502
525
|
request_id = gen_id()
|
|
@@ -508,7 +531,7 @@ class WaldiezStepByStepRunner(WaldiezBaseRunner, BreakpointsMixin):
|
|
|
508
531
|
)
|
|
509
532
|
)
|
|
510
533
|
|
|
511
|
-
user_input = await
|
|
534
|
+
user_input = await EventsMixin.a_get_user_input(
|
|
512
535
|
DEBUG_INPUT_PROMPT
|
|
513
536
|
)
|
|
514
537
|
user_input = user_input.strip()
|
|
@@ -519,36 +542,45 @@ class WaldiezStepByStepRunner(WaldiezBaseRunner, BreakpointsMixin):
|
|
|
519
542
|
except (KeyboardInterrupt, EOFError):
|
|
520
543
|
return WaldiezDebugStepAction.QUIT
|
|
521
544
|
|
|
522
|
-
def _handle_step_interaction(self) -> bool:
|
|
523
|
-
"""Handle step-by-step user interaction.
|
|
545
|
+
def _handle_step_interaction(self, force: bool) -> bool:
|
|
546
|
+
"""Handle step-by-step user interaction.
|
|
547
|
+
|
|
548
|
+
Parameters
|
|
549
|
+
----------
|
|
550
|
+
force : bool
|
|
551
|
+
Force getting the user's action, even if in auto-run mode.
|
|
552
|
+
"""
|
|
524
553
|
while True:
|
|
525
|
-
action = self._get_user_action()
|
|
554
|
+
action = self._get_user_action(force)
|
|
526
555
|
if action in (
|
|
527
556
|
WaldiezDebugStepAction.CONTINUE,
|
|
528
557
|
WaldiezDebugStepAction.STEP,
|
|
529
558
|
):
|
|
530
559
|
return True
|
|
531
560
|
if action == WaldiezDebugStepAction.RUN:
|
|
561
|
+
self._config.auto_continue = True
|
|
532
562
|
return True
|
|
533
563
|
if action == WaldiezDebugStepAction.QUIT:
|
|
534
564
|
return False
|
|
535
565
|
# For other actions (info, help, etc.), continue the loop
|
|
536
566
|
|
|
537
|
-
async def _a_handle_step_interaction(self) -> bool:
|
|
567
|
+
async def _a_handle_step_interaction(self, force: bool) -> bool:
|
|
538
568
|
"""Handle step-by-step user interaction asynchronously."""
|
|
539
569
|
while True:
|
|
540
|
-
action = await self._a_get_user_action()
|
|
570
|
+
action = await self._a_get_user_action(force)
|
|
541
571
|
if action in (
|
|
542
572
|
WaldiezDebugStepAction.CONTINUE,
|
|
543
573
|
WaldiezDebugStepAction.STEP,
|
|
544
574
|
):
|
|
545
575
|
return True
|
|
546
576
|
if action == WaldiezDebugStepAction.RUN:
|
|
577
|
+
self._config.auto_continue = True
|
|
547
578
|
return True
|
|
548
579
|
if action == WaldiezDebugStepAction.QUIT:
|
|
549
580
|
return False
|
|
550
581
|
# For other actions (info, help, etc.), continue the loop
|
|
551
582
|
|
|
583
|
+
@override
|
|
552
584
|
def _run(
|
|
553
585
|
self,
|
|
554
586
|
temp_dir: Path,
|
|
@@ -586,9 +618,9 @@ class WaldiezStepByStepRunner(WaldiezBaseRunner, BreakpointsMixin):
|
|
|
586
618
|
else:
|
|
587
619
|
stream = IOStream.get_default()
|
|
588
620
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
621
|
+
EventsMixin.set_print_function(stream.print)
|
|
622
|
+
EventsMixin.set_input_function(stream.input)
|
|
623
|
+
EventsMixin.set_send_function(stream.send)
|
|
592
624
|
|
|
593
625
|
self.print(MESSAGES["workflow_starting"])
|
|
594
626
|
self.print(self.waldiez.info.model_dump_json())
|
|
@@ -608,30 +640,44 @@ class WaldiezStepByStepRunner(WaldiezBaseRunner, BreakpointsMixin):
|
|
|
608
640
|
|
|
609
641
|
return results_container["results"]
|
|
610
642
|
|
|
611
|
-
def
|
|
643
|
+
def _re_emit_if_needed(self, event_info: dict[str, Any]) -> None:
|
|
644
|
+
# emit again if type is text, swapping the sender and without recipient
|
|
645
|
+
if event_info.get("type", "") == "text":
|
|
646
|
+
event_info["sender"] = event_info["recipient"]
|
|
647
|
+
event_info["recipient"] = None
|
|
648
|
+
event_info["agents"]["sender"] = event_info["agents"]["recipient"]
|
|
649
|
+
event_info["agents"]["recipient"] = None
|
|
650
|
+
self.emit_event(event_info)
|
|
651
|
+
|
|
652
|
+
def _on_event(
|
|
653
|
+
self,
|
|
654
|
+
event: Union["BaseEvent", "BaseMessage"],
|
|
655
|
+
agents: list["ConversableAgent"],
|
|
656
|
+
) -> bool:
|
|
612
657
|
"""Process an event with step-by-step debugging."""
|
|
613
658
|
# pylint: disable=too-many-try-statements,broad-exception-caught
|
|
614
659
|
try:
|
|
615
660
|
# Use the event processor for core logic
|
|
616
|
-
result = self._event_processor.process_event(event)
|
|
661
|
+
result = self._event_processor.process_event(event, agents)
|
|
617
662
|
|
|
618
663
|
if result["action"] == "stop":
|
|
619
664
|
self.log.debug(
|
|
620
665
|
"Step-by-step execution stopped before event processing"
|
|
621
666
|
)
|
|
622
667
|
return False
|
|
623
|
-
|
|
668
|
+
event_info = result["event_info"]
|
|
669
|
+
self.emit_event(event_info)
|
|
624
670
|
# Handle breakpoint logic
|
|
625
|
-
if result["
|
|
626
|
-
if not self._handle_step_interaction():
|
|
671
|
+
if result["action"] == "break":
|
|
672
|
+
if not self._handle_step_interaction(force=True):
|
|
627
673
|
self._stop_requested.set()
|
|
628
674
|
if hasattr(event, "type") and event.type == "input_request":
|
|
629
675
|
event.content.respond("exit")
|
|
630
676
|
return True
|
|
631
677
|
raise StopRunningException(StopRunningException.reason)
|
|
632
|
-
|
|
678
|
+
self._re_emit_if_needed(event_info)
|
|
633
679
|
# Process the actual event
|
|
634
|
-
|
|
680
|
+
EventsMixin.process_event(event, agents, skip_send=True)
|
|
635
681
|
self._processed_events += 1
|
|
636
682
|
|
|
637
683
|
except Exception as e:
|
|
@@ -644,6 +690,7 @@ class WaldiezStepByStepRunner(WaldiezBaseRunner, BreakpointsMixin):
|
|
|
644
690
|
return not self._stop_requested.is_set()
|
|
645
691
|
|
|
646
692
|
# pylint: disable=too-complex
|
|
693
|
+
@override
|
|
647
694
|
async def _a_run(
|
|
648
695
|
self,
|
|
649
696
|
temp_dir: Path,
|
|
@@ -657,7 +704,7 @@ class WaldiezStepByStepRunner(WaldiezBaseRunner, BreakpointsMixin):
|
|
|
657
704
|
|
|
658
705
|
async def _execute_workflow() -> list[dict[str, Any]]:
|
|
659
706
|
# pylint: disable=import-outside-toplevel
|
|
660
|
-
from autogen.io import IOStream
|
|
707
|
+
from autogen.io import IOStream
|
|
661
708
|
|
|
662
709
|
from waldiez.io import StructuredIOStream
|
|
663
710
|
|
|
@@ -677,9 +724,9 @@ class WaldiezStepByStepRunner(WaldiezBaseRunner, BreakpointsMixin):
|
|
|
677
724
|
else:
|
|
678
725
|
stream = IOStream.get_default()
|
|
679
726
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
727
|
+
EventsMixin.set_print_function(stream.print)
|
|
728
|
+
EventsMixin.set_input_function(stream.input)
|
|
729
|
+
EventsMixin.set_send_function(stream.send)
|
|
683
730
|
|
|
684
731
|
self.print(MESSAGES["workflow_starting"])
|
|
685
732
|
self.print(self.waldiez.info.model_dump_json())
|
|
@@ -713,31 +760,34 @@ class WaldiezStepByStepRunner(WaldiezBaseRunner, BreakpointsMixin):
|
|
|
713
760
|
return []
|
|
714
761
|
|
|
715
762
|
async def _a_on_event(
|
|
716
|
-
self,
|
|
763
|
+
self,
|
|
764
|
+
event: Union["BaseEvent", "BaseMessage"],
|
|
765
|
+
agents: list["ConversableAgent"],
|
|
717
766
|
) -> bool:
|
|
718
767
|
"""Process an event with step-by-step debugging asynchronously."""
|
|
719
768
|
# pylint: disable=too-many-try-statements,broad-exception-caught
|
|
720
769
|
try:
|
|
721
770
|
# Use the event processor for core logic
|
|
722
|
-
result = self._event_processor.process_event(event)
|
|
771
|
+
result = self._event_processor.process_event(event, agents)
|
|
723
772
|
|
|
724
773
|
if result["action"] == "stop":
|
|
725
774
|
self.log.debug(
|
|
726
775
|
"Async step-by-step execution stopped before event processing"
|
|
727
776
|
)
|
|
728
777
|
return False
|
|
729
|
-
|
|
778
|
+
event_info = result["event_info"]
|
|
779
|
+
self.emit_event(event_info)
|
|
730
780
|
# Handle breakpoint logic
|
|
731
|
-
if result["
|
|
732
|
-
if not await self._a_handle_step_interaction():
|
|
781
|
+
if result["action"] == "break":
|
|
782
|
+
if not await self._a_handle_step_interaction(force=True):
|
|
733
783
|
self._stop_requested.set()
|
|
734
784
|
if hasattr(event, "type") and event.type == "input_request":
|
|
735
|
-
event.content.respond("exit")
|
|
785
|
+
await event.content.respond("exit")
|
|
736
786
|
return True
|
|
737
787
|
raise StopRunningException(StopRunningException.reason)
|
|
738
|
-
|
|
788
|
+
self._re_emit_if_needed(event_info)
|
|
739
789
|
# Process the actual event
|
|
740
|
-
await
|
|
790
|
+
await EventsMixin.a_process_event(event, agents, skip_send=True)
|
|
741
791
|
self._processed_events += 1
|
|
742
792
|
|
|
743
793
|
except Exception as e:
|
|
@@ -50,6 +50,10 @@ class BaseSubprocessRunner:
|
|
|
50
50
|
self.dot_env = dot_env
|
|
51
51
|
self.logger = logger or logging.getLogger(self.__class__.__name__)
|
|
52
52
|
self.waiting_for_input = False
|
|
53
|
+
breakpoints = kwargs.get("breakpoints", [])
|
|
54
|
+
if not isinstance(breakpoints, list):
|
|
55
|
+
breakpoints = []
|
|
56
|
+
self.breakpoints: list[str] = breakpoints
|
|
53
57
|
|
|
54
58
|
def build_command(
|
|
55
59
|
self,
|
|
@@ -110,6 +114,10 @@ class BaseSubprocessRunner:
|
|
|
110
114
|
if self.dot_env:
|
|
111
115
|
cmd.extend(["--dot-env", str(self.dot_env)])
|
|
112
116
|
|
|
117
|
+
if self.breakpoints:
|
|
118
|
+
for entry in self.breakpoints:
|
|
119
|
+
cmd.extend(["--breakpoints", entry])
|
|
120
|
+
self.logger.debug("Runner command: %s", " ".join(cmd))
|
|
113
121
|
return cmd
|
|
114
122
|
|
|
115
123
|
def parse_output(
|
|
@@ -138,7 +146,7 @@ class BaseSubprocessRunner:
|
|
|
138
146
|
try:
|
|
139
147
|
data = json.loads(line)
|
|
140
148
|
if isinstance(data, dict):
|
|
141
|
-
return data # pyright: ignore
|
|
149
|
+
return data # pyright: ignore[reportUnknownVariableType]
|
|
142
150
|
except json.JSONDecodeError:
|
|
143
151
|
return self.create_output_message(stream=stream, content=line)
|
|
144
152
|
return self.create_output_message(
|
|
@@ -10,8 +10,9 @@ import logging
|
|
|
10
10
|
|
|
11
11
|
# noinspection PyProtectedMember
|
|
12
12
|
from asyncio.subprocess import Process as AsyncProcess
|
|
13
|
+
from collections.abc import Coroutine
|
|
13
14
|
from pathlib import Path
|
|
14
|
-
from typing import Any, Callable,
|
|
15
|
+
from typing import Any, Callable, Literal
|
|
15
16
|
|
|
16
17
|
from .__base__ import BaseSubprocessRunner
|
|
17
18
|
|
|
@@ -57,7 +58,7 @@ class AsyncSubprocessRunner(BaseSubprocessRunner):
|
|
|
57
58
|
)
|
|
58
59
|
self.on_output = on_output
|
|
59
60
|
self.on_input_request = on_input_request
|
|
60
|
-
self.process:
|
|
61
|
+
self.process: AsyncProcess | None = None
|
|
61
62
|
self.input_queue: asyncio.Queue[str] = asyncio.Queue()
|
|
62
63
|
self.output_queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
|
|
63
64
|
self._monitor_tasks: list[asyncio.Task[Any]] = []
|
|
@@ -158,7 +159,8 @@ class AsyncSubprocessRunner(BaseSubprocessRunner):
|
|
|
158
159
|
await self.process.wait()
|
|
159
160
|
|
|
160
161
|
except Exception as e:
|
|
161
|
-
|
|
162
|
+
if not isinstance(e, AttributeError):
|
|
163
|
+
self.logger.error(f"Error stopping subprocess: {e}")
|
|
162
164
|
self.process = None
|
|
163
165
|
|
|
164
166
|
async def _start_monitoring(self) -> None:
|
|
@@ -93,6 +93,7 @@ class SyncSubprocessRunner(BaseSubprocessRunner):
|
|
|
93
93
|
stdin=subprocess.PIPE,
|
|
94
94
|
stdout=subprocess.PIPE,
|
|
95
95
|
stderr=subprocess.PIPE,
|
|
96
|
+
encoding="utf-8",
|
|
96
97
|
text=True,
|
|
97
98
|
bufsize=1, # Line buffered
|
|
98
99
|
)
|
|
@@ -351,8 +352,9 @@ class SyncSubprocessRunner(BaseSubprocessRunner):
|
|
|
351
352
|
f"Thread {thread.name} did not stop gracefully"
|
|
352
353
|
)
|
|
353
354
|
|
|
355
|
+
# pylint: disable=too-complex
|
|
354
356
|
# noinspection TryExceptPass,PyBroadException
|
|
355
|
-
def _cleanup_process(self) -> None:
|
|
357
|
+
def _cleanup_process(self) -> None: # noqa: C901
|
|
356
358
|
"""Cleanup process resources."""
|
|
357
359
|
if self.process:
|
|
358
360
|
if self.process.stdin:
|
|
@@ -381,7 +383,9 @@ class SyncSubprocessRunner(BaseSubprocessRunner):
|
|
|
381
383
|
self.process.kill()
|
|
382
384
|
self.process.wait()
|
|
383
385
|
except BaseException as e:
|
|
384
|
-
|
|
386
|
+
if not isinstance(e, AttributeError):
|
|
387
|
+
# already "None"
|
|
388
|
+
self.logger.error(f"Error stopping subprocess: {e}")
|
|
385
389
|
finally:
|
|
386
390
|
self.process = None
|
|
387
391
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# SPDX-License-Identifier: Apache-2.0.
|
|
2
2
|
# Copyright (c) 2024 - 2025 Waldiez and contributors.
|
|
3
|
+
|
|
4
|
+
# pyright: reportAttributeAccessIssue=false,reportUnknownArgumentType=false
|
|
3
5
|
# flake8: noqa: G004
|
|
4
6
|
"""Waldiez subprocess runner that inherits from BaseRunner."""
|
|
5
7
|
|
|
@@ -8,15 +10,15 @@ import re
|
|
|
8
10
|
from pathlib import Path
|
|
9
11
|
from typing import Any, Callable, Literal
|
|
10
12
|
|
|
13
|
+
from typing_extensions import override
|
|
14
|
+
|
|
11
15
|
from waldiez.models import Waldiez
|
|
12
16
|
|
|
13
17
|
from ..base_runner import WaldiezBaseRunner
|
|
18
|
+
from ..step_by_step.breakpoints_mixin import BreakpointsMixin
|
|
14
19
|
from ._async_runner import AsyncSubprocessRunner
|
|
15
20
|
from ._sync_runner import SyncSubprocessRunner
|
|
16
21
|
|
|
17
|
-
# TODO: check output directory and return the results from the JSON logs
|
|
18
|
-
# in self._run and self._a_run
|
|
19
|
-
|
|
20
22
|
|
|
21
23
|
# noinspection PyUnusedLocal
|
|
22
24
|
class WaldiezSubprocessRunner(WaldiezBaseRunner):
|
|
@@ -71,6 +73,7 @@ class WaldiezSubprocessRunner(WaldiezBaseRunner):
|
|
|
71
73
|
dot_env=dot_env,
|
|
72
74
|
**kwargs,
|
|
73
75
|
)
|
|
76
|
+
self.breakpoints = self._parse_breakpoints(**kwargs)
|
|
74
77
|
|
|
75
78
|
# Store callbacks
|
|
76
79
|
self.sync_on_output = on_output or self._default_sync_output
|
|
@@ -106,17 +109,27 @@ class WaldiezSubprocessRunner(WaldiezBaseRunner):
|
|
|
106
109
|
# noinspection RegExpRedundantEscape
|
|
107
110
|
file_name = re.sub(r"[^a-zA-Z0-9_\-\.]", "_", file_name)[:30]
|
|
108
111
|
file_name = f"{file_name}.waldiez"
|
|
109
|
-
with open(file_name, "w", encoding="utf-8") as f:
|
|
112
|
+
with open(file_name, "w", encoding="utf-8", newline="\n") as f:
|
|
110
113
|
f.write(self.waldiez.model_dump_json())
|
|
111
114
|
return Path(file_name).resolve()
|
|
112
115
|
|
|
116
|
+
@staticmethod
|
|
117
|
+
def _parse_breakpoints(**kwargs: Any) -> list[str]:
|
|
118
|
+
initial_breakpoints = kwargs.get("breakpoints")
|
|
119
|
+
if isinstance(initial_breakpoints, (list, set, tuple)):
|
|
120
|
+
breakpoints = BreakpointsMixin.get_initial_breakpoints(
|
|
121
|
+
initial_breakpoints
|
|
122
|
+
)
|
|
123
|
+
return [str(item) for item in breakpoints]
|
|
124
|
+
return []
|
|
125
|
+
|
|
113
126
|
def _default_sync_output(self, data: dict[str, Any]) -> None:
|
|
114
127
|
"""Get the default sync output handler."""
|
|
115
128
|
if data.get("type") == "error":
|
|
116
129
|
self.log.error(data.get("data", ""))
|
|
117
130
|
else:
|
|
118
131
|
content = data.get("data", data)
|
|
119
|
-
self.
|
|
132
|
+
self.print(content)
|
|
120
133
|
|
|
121
134
|
def _default_sync_input_request(self, prompt: str) -> None:
|
|
122
135
|
"""Get the default sync input request handler."""
|
|
@@ -141,6 +154,7 @@ class WaldiezSubprocessRunner(WaldiezBaseRunner):
|
|
|
141
154
|
uploads_root=self.uploads_root,
|
|
142
155
|
dot_env=self.dot_env_path,
|
|
143
156
|
logger=self.log,
|
|
157
|
+
breakpoints=self.breakpoints,
|
|
144
158
|
)
|
|
145
159
|
return self.async_runner
|
|
146
160
|
|
|
@@ -153,9 +167,11 @@ class WaldiezSubprocessRunner(WaldiezBaseRunner):
|
|
|
153
167
|
uploads_root=self.uploads_root,
|
|
154
168
|
dot_env=self.dot_env_path,
|
|
155
169
|
logger=self.log,
|
|
170
|
+
breakpoints=self.breakpoints,
|
|
156
171
|
)
|
|
157
172
|
return self.sync_runner
|
|
158
173
|
|
|
174
|
+
@override
|
|
159
175
|
def run(
|
|
160
176
|
self,
|
|
161
177
|
output_path: str | Path | None = None,
|
|
@@ -220,6 +236,7 @@ class WaldiezSubprocessRunner(WaldiezBaseRunner):
|
|
|
220
236
|
return _output_path / f"{filename}.py"
|
|
221
237
|
return self._waldiez_file.with_suffix(".py")
|
|
222
238
|
|
|
239
|
+
@override
|
|
223
240
|
def _run(
|
|
224
241
|
self,
|
|
225
242
|
temp_dir: Path,
|
|
@@ -242,14 +259,8 @@ class WaldiezSubprocessRunner(WaldiezBaseRunner):
|
|
|
242
259
|
runner = self._create_sync_subprocess_runner()
|
|
243
260
|
|
|
244
261
|
# Run subprocess
|
|
245
|
-
|
|
246
|
-
return
|
|
247
|
-
{
|
|
248
|
-
"success": success,
|
|
249
|
-
"runner": "sync_subprocess",
|
|
250
|
-
"mode": self.mode,
|
|
251
|
-
}
|
|
252
|
-
]
|
|
262
|
+
runner.run_subprocess(self._waldiez_file, mode=self.mode)
|
|
263
|
+
return self.read_from_output(output_file.parent)
|
|
253
264
|
|
|
254
265
|
except Exception as e:
|
|
255
266
|
self.log.error("Error in sync subprocess execution: %s", e)
|
|
@@ -261,6 +272,7 @@ class WaldiezSubprocessRunner(WaldiezBaseRunner):
|
|
|
261
272
|
}
|
|
262
273
|
]
|
|
263
274
|
|
|
275
|
+
@override
|
|
264
276
|
async def a_run(
|
|
265
277
|
self,
|
|
266
278
|
output_path: str | Path | None = None,
|
|
@@ -311,6 +323,7 @@ class WaldiezSubprocessRunner(WaldiezBaseRunner):
|
|
|
311
323
|
**kwargs,
|
|
312
324
|
)
|
|
313
325
|
|
|
326
|
+
@override
|
|
314
327
|
async def _a_run(
|
|
315
328
|
self,
|
|
316
329
|
temp_dir: Path,
|
|
@@ -356,17 +369,11 @@ class WaldiezSubprocessRunner(WaldiezBaseRunner):
|
|
|
356
369
|
runner = self._create_async_subprocess_runner()
|
|
357
370
|
|
|
358
371
|
# Run subprocess
|
|
359
|
-
|
|
372
|
+
await runner.run_subprocess(
|
|
360
373
|
self._waldiez_file,
|
|
361
374
|
mode=self.mode,
|
|
362
375
|
)
|
|
363
|
-
return
|
|
364
|
-
{
|
|
365
|
-
"success": success,
|
|
366
|
-
"runner": "async_subprocess",
|
|
367
|
-
"mode": self.mode,
|
|
368
|
-
}
|
|
369
|
-
]
|
|
376
|
+
return await self.a_read_from_output(output_file.parent)
|
|
370
377
|
|
|
371
378
|
except Exception as e:
|
|
372
379
|
self.log.error("Error in async subprocess execution: %s", e)
|
|
@@ -408,6 +415,7 @@ class WaldiezSubprocessRunner(WaldiezBaseRunner):
|
|
|
408
415
|
self.sync_runner.provide_user_input, user_input
|
|
409
416
|
)
|
|
410
417
|
|
|
418
|
+
@override
|
|
411
419
|
def stop(self) -> None:
|
|
412
420
|
"""Stop the workflow execution."""
|
|
413
421
|
super().stop() # Set the base runner stop flag
|
|
@@ -461,9 +469,11 @@ class WaldiezSubprocessRunner(WaldiezBaseRunner):
|
|
|
461
469
|
self.sync_runner.stop()
|
|
462
470
|
self.sync_runner = None
|
|
463
471
|
|
|
472
|
+
@override
|
|
464
473
|
def _after_run(
|
|
465
474
|
self,
|
|
466
475
|
results: list[dict[str, Any]],
|
|
476
|
+
error: BaseException | None,
|
|
467
477
|
output_file: Path,
|
|
468
478
|
waldiez_file: Path,
|
|
469
479
|
uploads_root: Path | None,
|
|
@@ -476,7 +486,9 @@ class WaldiezSubprocessRunner(WaldiezBaseRunner):
|
|
|
476
486
|
Parameters
|
|
477
487
|
----------
|
|
478
488
|
results : list[dict[str, Any]]
|
|
479
|
-
Results from the workflow execution
|
|
489
|
+
Results from the workflow execution.
|
|
490
|
+
error : BaseException | None
|
|
491
|
+
Optional error during the run.
|
|
480
492
|
output_file : Path
|
|
481
493
|
Output file path
|
|
482
494
|
waldiez_file : Path
|
|
@@ -493,9 +505,11 @@ class WaldiezSubprocessRunner(WaldiezBaseRunner):
|
|
|
493
505
|
# Cleanup subprocess runners
|
|
494
506
|
self._cleanup_subprocess_runners()
|
|
495
507
|
|
|
508
|
+
@override
|
|
496
509
|
async def _a_after_run(
|
|
497
510
|
self,
|
|
498
511
|
results: list[dict[str, Any]],
|
|
512
|
+
error: BaseException | None,
|
|
499
513
|
output_file: Path,
|
|
500
514
|
waldiez_file: Path,
|
|
501
515
|
uploads_root: Path | None,
|
|
@@ -508,7 +522,9 @@ class WaldiezSubprocessRunner(WaldiezBaseRunner):
|
|
|
508
522
|
Parameters
|
|
509
523
|
----------
|
|
510
524
|
results : list[dict[str, Any]]
|
|
511
|
-
Results from the workflow execution
|
|
525
|
+
Results from the workflow execution.
|
|
526
|
+
error : BaseException | None
|
|
527
|
+
Optional error during the run.
|
|
512
528
|
output_file : Path
|
|
513
529
|
Output file path
|
|
514
530
|
waldiez_file : Path
|
|
@@ -93,7 +93,7 @@ class TimelineProcessor:
|
|
|
93
93
|
bool
|
|
94
94
|
True if the value is missing, NaN, or empty; False otherwise.
|
|
95
95
|
"""
|
|
96
|
-
if pd.isna(value):
|
|
96
|
+
if pd.isna(value):
|
|
97
97
|
return True
|
|
98
98
|
if isinstance(value, str) and (
|
|
99
99
|
value.strip() == "" or value.lower() == "nan"
|