waldiez 0.5.8__py3-none-any.whl → 0.5.10__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 (88) hide show
  1. waldiez/_version.py +1 -1
  2. waldiez/cli.py +112 -24
  3. waldiez/exporting/agent/exporter.py +3 -0
  4. waldiez/exporting/agent/extras/captain_agent_extras.py +44 -7
  5. waldiez/exporting/agent/extras/handoffs/condition.py +3 -1
  6. waldiez/exporting/chats/utils/common.py +25 -23
  7. waldiez/exporting/core/__init__.py +0 -2
  8. waldiez/exporting/core/context.py +13 -13
  9. waldiez/exporting/core/protocols.py +0 -141
  10. waldiez/exporting/core/result.py +5 -5
  11. waldiez/exporting/flow/merger.py +2 -2
  12. waldiez/exporting/flow/orchestrator.py +1 -0
  13. waldiez/exporting/flow/utils/common.py +2 -2
  14. waldiez/exporting/flow/utils/importing.py +1 -0
  15. waldiez/exporting/flow/utils/logging.py +6 -7
  16. waldiez/exporting/tools/exporter.py +5 -0
  17. waldiez/exporting/tools/factory.py +4 -0
  18. waldiez/exporting/tools/processor.py +5 -1
  19. waldiez/io/_ws.py +13 -5
  20. waldiez/io/models/content/image.py +1 -0
  21. waldiez/io/models/user_input.py +4 -4
  22. waldiez/io/models/user_response.py +1 -0
  23. waldiez/io/mqtt.py +1 -1
  24. waldiez/io/structured.py +17 -17
  25. waldiez/io/utils.py +1 -1
  26. waldiez/io/ws.py +9 -11
  27. waldiez/logger.py +180 -63
  28. waldiez/models/agents/agent/update_system_message.py +0 -2
  29. waldiez/models/agents/doc_agent/doc_agent.py +8 -1
  30. waldiez/models/common/dict_utils.py +169 -40
  31. waldiez/models/flow/flow.py +6 -6
  32. waldiez/models/flow/info.py +5 -1
  33. waldiez/models/model/_llm.py +28 -14
  34. waldiez/models/model/model.py +4 -1
  35. waldiez/models/model/model_data.py +18 -5
  36. waldiez/models/tool/predefined/_config.py +5 -1
  37. waldiez/models/tool/predefined/_duckduckgo.py +4 -0
  38. waldiez/models/tool/predefined/_email.py +474 -0
  39. waldiez/models/tool/predefined/_google.py +8 -6
  40. waldiez/models/tool/predefined/_perplexity.py +3 -0
  41. waldiez/models/tool/predefined/_searxng.py +3 -0
  42. waldiez/models/tool/predefined/_tavily.py +4 -1
  43. waldiez/models/tool/predefined/_wikipedia.py +4 -1
  44. waldiez/models/tool/predefined/_youtube.py +4 -1
  45. waldiez/models/tool/predefined/protocol.py +3 -0
  46. waldiez/models/tool/tool.py +22 -4
  47. waldiez/models/waldiez.py +12 -0
  48. waldiez/runner.py +37 -54
  49. waldiez/running/__init__.py +6 -0
  50. waldiez/running/base_runner.py +310 -353
  51. waldiez/running/environment.py +1 -0
  52. waldiez/running/exceptions.py +9 -0
  53. waldiez/running/post_run.py +4 -4
  54. waldiez/running/pre_run.py +51 -40
  55. waldiez/running/protocol.py +21 -101
  56. waldiez/running/run_results.py +1 -1
  57. waldiez/running/standard_runner.py +84 -277
  58. waldiez/running/step_by_step/__init__.py +46 -0
  59. waldiez/running/step_by_step/breakpoints_mixin.py +188 -0
  60. waldiez/running/step_by_step/step_by_step_models.py +224 -0
  61. waldiez/running/step_by_step/step_by_step_runner.py +745 -0
  62. waldiez/running/subprocess_runner/__base__.py +282 -0
  63. waldiez/running/subprocess_runner/__init__.py +16 -0
  64. waldiez/running/subprocess_runner/_async_runner.py +362 -0
  65. waldiez/running/subprocess_runner/_sync_runner.py +455 -0
  66. waldiez/running/subprocess_runner/runner.py +561 -0
  67. waldiez/running/timeline_processor.py +1 -1
  68. waldiez/running/utils.py +376 -1
  69. waldiez/utils/version.py +2 -6
  70. waldiez/ws/__init__.py +70 -0
  71. waldiez/ws/__main__.py +15 -0
  72. waldiez/ws/_file_handler.py +201 -0
  73. waldiez/ws/cli.py +211 -0
  74. waldiez/ws/client_manager.py +835 -0
  75. waldiez/ws/errors.py +416 -0
  76. waldiez/ws/models.py +971 -0
  77. waldiez/ws/reloader.py +342 -0
  78. waldiez/ws/server.py +469 -0
  79. waldiez/ws/session_manager.py +393 -0
  80. waldiez/ws/session_stats.py +83 -0
  81. waldiez/ws/utils.py +385 -0
  82. {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/METADATA +74 -74
  83. {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/RECORD +87 -65
  84. waldiez/running/patch_io_stream.py +0 -210
  85. {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/WHEEL +0 -0
  86. {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/entry_points.txt +0 -0
  87. {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/licenses/LICENSE +0 -0
  88. {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/licenses/NOTICE.md +0 -0
waldiez/logger.py CHANGED
@@ -4,6 +4,7 @@
4
4
  """Waldiez logger."""
5
5
 
6
6
  import inspect
7
+ import logging
7
8
  import os
8
9
  import re
9
10
  import string
@@ -11,23 +12,27 @@ import threading
11
12
  import traceback
12
13
  from datetime import datetime
13
14
  from enum import IntEnum
14
- from typing import Any, Callable, Optional
15
+ from pathlib import Path
16
+ from types import TracebackType
17
+ from typing import Any, Callable, Mapping, Optional
15
18
 
16
19
  import click
17
20
 
21
+ HERE = Path(__file__).parent
22
+
18
23
 
19
24
  class LogLevel(IntEnum):
20
25
  """Log level enumeration for comparison."""
21
26
 
22
27
  DEBUG = 10
23
28
  INFO = 20
24
- SUCCESS = 30
25
- WARNING = 40
26
- ERROR = 50
27
- CRITICAL = 60
29
+ SUCCESS = 21
30
+ WARNING = 30
31
+ ERROR = 40
32
+ CRITICAL = 50
28
33
 
29
34
 
30
- class WaldiezLogger:
35
+ class WaldiezLogger(logging.Logger):
31
36
  """A colorful logger implementation using Click.
32
37
 
33
38
  Supports both .format() and %-style formatting:
@@ -51,9 +56,29 @@ class WaldiezLogger:
51
56
  "critical",
52
57
  "exception",
53
58
  }
59
+ _level_map: dict[str, LogLevel] = {
60
+ "DEBUG": LogLevel.DEBUG,
61
+ "INFO": LogLevel.INFO,
62
+ "SUCCESS": LogLevel.SUCCESS,
63
+ "WARNING": LogLevel.WARNING,
64
+ "ERROR": LogLevel.ERROR,
65
+ "CRITICAL": LogLevel.CRITICAL,
66
+ }
67
+ # Map levels to click styling functions
68
+ _style_map: dict[str, Callable[[str], str]] = {
69
+ "DEBUG": lambda msg: click.style(msg, dim=True),
70
+ "INFO": lambda msg: click.style(msg, fg="blue"),
71
+ "SUCCESS": lambda msg: click.style(msg, fg="green"),
72
+ "WARNING": lambda msg: click.style(msg, fg="yellow"),
73
+ "ERROR": lambda msg: click.style(msg, fg="red"),
74
+ "CRITICAL": lambda msg: click.style(msg, fg="red", bold=True),
75
+ "EXCEPTION": lambda msg: click.style(msg, fg="red", bold=True),
76
+ }
54
77
 
55
78
  def __new__(cls, *args: Any, **kwargs: Any) -> "WaldiezLogger":
56
79
  """Ensure only one instance of the logger is created."""
80
+ for level_name, level_value in LogLevel.__members__.items():
81
+ logging.addLevelName(level_value, level_name)
57
82
  if cls._instance is None:
58
83
  with cls._lock:
59
84
  # Double-check locking pattern
@@ -75,6 +100,9 @@ class WaldiezLogger:
75
100
  timestamp_format : str, optional
76
101
  Timestamp format string, by default "%Y-%m-%d %H:%M:%S"
77
102
  """
103
+ super().__init__(self.__class__.__name__)
104
+ for level_name, level_value in LogLevel.__members__.items():
105
+ logging.addLevelName(level_value, level_name)
78
106
  if getattr(self, "_initialized", False) is True:
79
107
  if level != self.get_level():
80
108
  self.set_level(level)
@@ -84,29 +112,16 @@ class WaldiezLogger:
84
112
  ):
85
113
  self._instance.set_timestamp_format(timestamp_format)
86
114
  return
115
+ self._initialized = True
87
116
  self._level = level.upper()
88
117
  self._timestamp_format = timestamp_format
89
-
90
- self._level_map: dict[str, LogLevel] = {
91
- "DEBUG": LogLevel.DEBUG,
92
- "INFO": LogLevel.INFO,
93
- "SUCCESS": LogLevel.SUCCESS,
94
- "WARNING": LogLevel.WARNING,
95
- "ERROR": LogLevel.ERROR,
96
- "CRITICAL": LogLevel.CRITICAL,
97
- }
98
-
99
- # Map levels to click styling functions
100
- self._style_map: dict[str, Callable[[str], str]] = {
101
- "DEBUG": lambda msg: click.style(msg, dim=True),
102
- "INFO": lambda msg: click.style(msg, fg="blue"),
103
- "SUCCESS": lambda msg: click.style(msg, fg="green"),
104
- "WARNING": lambda msg: click.style(msg, fg="yellow"),
105
- "ERROR": lambda msg: click.style(msg, fg="red"),
106
- "CRITICAL": lambda msg: click.style(msg, fg="red", bold=True),
107
- "EXCEPTION": lambda msg: click.style(msg, fg="red", bold=True),
108
- }
109
- self._initialized = True
118
+ if self.get_level() != level:
119
+ self.set_level(level)
120
+ if (
121
+ self._instance
122
+ and self._instance.get_timestamp_format() != timestamp_format
123
+ ):
124
+ self._instance.set_timestamp_format(timestamp_format)
110
125
 
111
126
  @classmethod
112
127
  def get_instance(
@@ -130,129 +145,207 @@ class WaldiezLogger:
130
145
  """
131
146
  return cls(level, timestamp_format)
132
147
 
148
+ def _get_level_name(self, level: int) -> str:
149
+ """Get the string name of the logging level.
150
+
151
+ Parameters
152
+ ----------
153
+ level : int
154
+ The logging level.
155
+
156
+ Returns
157
+ -------
158
+ str
159
+ The string name of the logging level.
160
+ """
161
+ for name, lvl in self._level_map.items():
162
+ if lvl == level:
163
+ return name
164
+ return self.get_level()
165
+
166
+ def _get_level_number(self, level: str) -> int:
167
+ """Get the numeric value of the logging level.
168
+
169
+ Parameters
170
+ ----------
171
+ level : str
172
+ The logging level.
173
+
174
+ Returns
175
+ -------
176
+ int
177
+ The numeric value of the logging level.
178
+ """
179
+ return self._level_map.get(level.upper(), LogLevel.INFO)
180
+
181
+ # pylint: disable=unused-argument
133
182
  def log(
134
- self, message: Any, *args: Any, level: str = "info", **kwargs: Any
183
+ self,
184
+ level: int,
185
+ msg: object,
186
+ *args: object,
187
+ exc_info: (
188
+ bool
189
+ | tuple[type[BaseException], BaseException, TracebackType | None]
190
+ | tuple[None, None, None]
191
+ | BaseException
192
+ | None
193
+ ) = None,
194
+ stack_info: bool = False,
195
+ stacklevel: int = 1,
196
+ extra: Mapping[str, object] | None = None,
197
+ **kwargs: object,
135
198
  ) -> None:
136
199
  """Log a message with the specified level.
137
200
 
138
201
  Parameters
139
202
  ----------
140
- message : Any
203
+ level : int
204
+ The logging level to use (e.g., logging.DEBUG, logging.INFO, etc.).
205
+ msg : object
141
206
  The message to log or message template for formatting.
142
- level : str, optional
143
- The logging level to use (e.g., "debug", "info", "success",
144
- "warning", "error", "critical"). Defaults to "info".
145
- *args : Any
146
- Arguments to format into the message using % formatting.
147
- **kwargs : Any
207
+ *args : object
208
+ Arguments to format into the message.
209
+ exc_info :
210
+ bool |
211
+ tuple[type[BaseException], BaseException, TracebackType | None] |
212
+ tuple[None, None, None] |
213
+ BaseException |
214
+ None
215
+ Exception information to include in the log.
216
+ stack_info : bool
217
+ Whether to include stack information in the log.
218
+ stacklevel : int
219
+ The stack level to use for the log.
220
+ extra : Mapping[str, object] | None
221
+ Extra context information to include in the log.
222
+ **kwargs : object
148
223
  Additional keyword arguments for formatting.
224
+
149
225
  """
150
- if self._should_log(level):
151
- formatted_message_content = self._format_args(
152
- message, *args, **kwargs
153
- )
226
+ level_str = self._get_level_name(level)
227
+ if self._should_log(level_str):
228
+ # noinspection PyArgumentList
229
+ formatted_message_content = self._format_args(msg, *args, **kwargs)
154
230
  formatted_message = self._format_message(
155
- formatted_message_content, level
231
+ formatted_message_content, level_str
156
232
  )
157
233
  click.echo(formatted_message)
158
234
 
159
- def debug(self, message: Any, *args: Any, **kwargs: Any) -> None:
235
+ def do_log(self, msg: Any, *args: Any, level: str, **kwargs: Any) -> None:
236
+ """Log a message with the specified level.
237
+
238
+ Parameters
239
+ ----------
240
+ msg : Any
241
+ The message to log or message template for formatting.
242
+ *args : Any
243
+ Arguments to format into the message.
244
+ level : str
245
+ The logging level.
246
+ **kwargs : Any
247
+ Additional keyword arguments for formatting.
248
+ """
249
+ level_int = self._get_level_number(level)
250
+ self.log(level_int, msg, *args, **kwargs)
251
+
252
+ def debug(self, msg: Any, *args: Any, **kwargs: Any) -> None:
160
253
  """Log a debug message.
161
254
 
162
255
  Parameters
163
256
  ----------
164
- message : Any
257
+ msg : Any
165
258
  The debug message to log or message template.
166
259
  *args : Any
167
260
  Arguments to format into the message.
168
261
  **kwargs : Any
169
262
  Additional keyword arguments for formatting.
170
263
  """
171
- self.log(message, *args, level="debug", **kwargs)
264
+ self.do_log(msg, *args, level="debug", **kwargs)
172
265
 
173
- def info(self, message: Any, *args: Any, **kwargs: Any) -> None:
266
+ def info(self, msg: Any, *args: Any, **kwargs: Any) -> None:
174
267
  """Log an informational message.
175
268
 
176
269
  Parameters
177
270
  ----------
178
- message : Any
271
+ msg : Any
179
272
  The informational message to log or message template.
180
273
  *args : Any
181
274
  Arguments to format into the message.
182
275
  **kwargs : Any
183
276
  Additional keyword arguments for formatting.
184
277
  """
185
- self.log(message, *args, level="info", **kwargs)
278
+ self.do_log(msg, *args, level="info", **kwargs)
186
279
 
187
- def success(self, message: Any, *args: Any, **kwargs: Any) -> None:
280
+ def success(self, msg: Any, *args: Any, **kwargs: Any) -> None:
188
281
  """Log a success message.
189
282
 
190
283
  Parameters
191
284
  ----------
192
- message : Any
285
+ msg : Any
193
286
  The success message to log or message template.
194
287
  *args : Any
195
288
  Arguments to format into the message.
196
289
  **kwargs : Any
197
290
  Additional keyword arguments for formatting.
198
291
  """
199
- self.log(message, *args, level="success", **kwargs)
292
+ self.do_log(msg, *args, level="success", **kwargs)
200
293
 
201
- def warning(self, message: Any, *args: Any, **kwargs: Any) -> None:
294
+ def warning(self, msg: Any, *args: Any, **kwargs: Any) -> None:
202
295
  """Log a warning message.
203
296
 
204
297
  Parameters
205
298
  ----------
206
- message : Any
299
+ msg : Any
207
300
  The warning message to log or message template.
208
301
  *args : Any
209
302
  Arguments to format into the message.
210
303
  **kwargs : Any
211
304
  Additional keyword arguments for formatting.
212
305
  """
213
- self.log(message, *args, level="warning", **kwargs)
306
+ self.do_log(msg, *args, level="warning", **kwargs)
214
307
 
215
- def error(self, message: Any, *args: Any, **kwargs: Any) -> None:
308
+ def error(self, msg: Any, *args: Any, **kwargs: Any) -> None:
216
309
  """Log an error message.
217
310
 
218
311
  Parameters
219
312
  ----------
220
- message : Any
313
+ msg : Any
221
314
  The error message to log or message template.
222
315
  *args : Any
223
316
  Arguments to format into the message.
224
317
  **kwargs : Any
225
318
  Additional keyword arguments for formatting.
226
319
  """
227
- self.log(message, *args, level="error", **kwargs)
320
+ self.do_log(msg, *args, level="error", **kwargs)
228
321
 
229
- def critical(self, message: Any, *args: Any, **kwargs: Any) -> None:
322
+ def critical(self, msg: Any, *args: Any, **kwargs: Any) -> None:
230
323
  """Log a critical error message.
231
324
 
232
325
  Parameters
233
326
  ----------
234
- message : Any
327
+ msg : Any
235
328
  The critical error message to log or message template.
236
329
  *args : Any
237
330
  Arguments to format into the message.
238
331
  **kwargs : Any
239
332
  Additional keyword arguments for formatting.
240
333
  """
241
- self.log(message, *args, level="critical", **kwargs)
334
+ self.do_log(msg, *args, level="critical", **kwargs)
242
335
 
243
- def exception(self, message: Any, *args: Any, **kwargs: Any) -> None:
336
+ def exception(self, msg: Any, *args: Any, **kwargs: Any) -> None:
244
337
  """Log an exception message.
245
338
 
246
339
  Parameters
247
340
  ----------
248
- message : Any
341
+ msg : Any
249
342
  The exception message to log or message template.
250
343
  *args : Any
251
344
  Arguments to format into the message.
252
345
  **kwargs : Any
253
346
  Additional keyword arguments for formatting.
254
347
  """
255
- formatted_message_content = self._format_args(message, *args, **kwargs)
348
+ formatted_message_content = self._format_args(msg, *args, **kwargs)
256
349
  formatted_message = self._format_message(
257
350
  formatted_message_content, "exception"
258
351
  )
@@ -261,6 +354,21 @@ class WaldiezLogger:
261
354
  if tb and "NoneType: None" not in tb: # pragma: no branch
262
355
  click.echo(click.style(tb, fg="red", dim=True))
263
356
 
357
+ def setLevel(self, level: int | str) -> None:
358
+ """
359
+ Set the logging level.
360
+
361
+ Parameters
362
+ ----------
363
+ level : int | str
364
+ The logging level to set
365
+ (e.g., "debug", "info", "warning", "error", "critical").
366
+ """
367
+ level_str = (
368
+ level if isinstance(level, str) else self._get_level_name(level)
369
+ )
370
+ self.set_level(level_str)
371
+
264
372
  def set_level(self, level: str) -> None:
265
373
  """Set the logging level.
266
374
 
@@ -278,6 +386,7 @@ class WaldiezLogger:
278
386
  level_upper = level.upper()
279
387
  if level_upper in self._level_map:
280
388
  self._level = level_upper
389
+ super().setLevel(self._level_map[level_upper].value)
281
390
  else:
282
391
  raise ValueError(
283
392
  f"Invalid log level: {level}. "
@@ -369,8 +478,15 @@ class WaldiezLogger:
369
478
  @staticmethod
370
479
  def _format_caller_display(filename: str, line_number: int) -> str:
371
480
  """Format the caller information for display."""
372
- basename = os.path.realpath(filename)
373
- return f"{basename}:{line_number}"
481
+ full_path = Path(filename).resolve()
482
+ try:
483
+ relative = full_path.relative_to(Path.cwd())
484
+ except ValueError: # pragma: no cover
485
+ try:
486
+ relative = full_path.relative_to(HERE.parent)
487
+ except ValueError: # pragma: no cover
488
+ relative = full_path
489
+ return f"{relative}:{line_number}"
374
490
 
375
491
  def _get_timestamp(self) -> str:
376
492
  """Get the current timestamp in a human-readable format."""
@@ -400,6 +516,7 @@ class WaldiezLogger:
400
516
  ) -> tuple[bool, Any]:
401
517
  """Attempt .format() formatting, returning (success, result)."""
402
518
  try:
519
+ # noinspection StrFormat
403
520
  return True, msg_str.format(*args, **kwargs)
404
521
  except Exception as e:
405
522
  return False, e
@@ -1,7 +1,5 @@
1
1
  # SPDX-License-Identifier: Apache-2.0.
2
2
  # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
- # SPDX-License-Identifier: Apache-2.0.
4
- # Copyright (c) 2024 - 2025 Waldiez and contributors.
5
3
  """Update the agent's system message before they reply."""
6
4
 
7
5
  from typing import Optional
@@ -114,7 +114,14 @@ class WaldiezDocAgent(WaldiezAgent):
114
114
  requirements = {
115
115
  "llama-index",
116
116
  "llama-index-core",
117
- f"ag2[rag]=={ag2_version}",
117
+ # f"ag2[rag]=={ag2_version}",
118
+ "chromadb>=0.5,<2",
119
+ "docling>=2.15.1,<3",
120
+ "selenium>=4.28.1,<5",
121
+ "webdriver-manager==4.0.2",
122
+ "llama-index-embeddings-huggingface",
123
+ "llama-index-llms-langchain",
124
+ "llama-index-vector-stores-chroma",
118
125
  }
119
126
  if not self.data.model_ids:
120
127
  requirements.add("llama-index-llms-openai")
@@ -2,63 +2,192 @@
2
2
  # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
3
  """Dictionary related utilities."""
4
4
 
5
+ import ast
6
+ import json
5
7
  import re
6
- from typing import Any
8
+ from typing import Any, Union
7
9
 
8
10
  BOOL_VALUES = {"true", "false"}
9
11
  NULL_VALUES = {"none", "null", "nil", "undefined"}
10
12
 
11
13
 
14
+ def _strip_outer_quotes(value: str) -> str:
15
+ """Remove outer quotes from a string if present."""
16
+ value_stripped = value.strip()
17
+ if (value_stripped.startswith('"') and value_stripped.endswith('"')) or (
18
+ value_stripped.startswith("'") and value_stripped.endswith("'")
19
+ ):
20
+ return value_stripped[1:-1]
21
+ return value_stripped
22
+
23
+
24
+ def _detect_null_or_boolean(value: str) -> Union[None, bool, str]:
25
+ """
26
+ Detect null values or booleans.
27
+
28
+ Parameters
29
+ ----------
30
+ value : str
31
+ The string value to check.
32
+
33
+ Returns
34
+ -------
35
+ Union[None, bool, str]
36
+ None for null values, bool for booleans, or original string if neither.
37
+ """
38
+ value_lower = value.lower()
39
+
40
+ if value_lower in NULL_VALUES:
41
+ return None
42
+ if value_lower in BOOL_VALUES:
43
+ return value_lower == "true"
44
+
45
+ return value
46
+
47
+
48
+ def _detect_numeric_type(value: str) -> Union[int, float, str]:
49
+ """
50
+ Detect if string represents an integer or float.
51
+
52
+ Parameters
53
+ ----------
54
+ value : str
55
+ The string value to check.
56
+
57
+ Returns
58
+ -------
59
+ Union[int, float, str]
60
+ int for integers, float for floats, or original string if neither.
61
+ """
62
+ # Check for integer first (more specific)
63
+ if re.fullmatch(r"[-+]?\d+", value):
64
+ return int(value)
65
+
66
+ # Try float conversion
67
+ try:
68
+ return float(value)
69
+ except ValueError:
70
+ return value
71
+
72
+
73
+ def _detect_container_type(
74
+ value: str,
75
+ ) -> Union[dict[str, Any], list[Any], tuple[Any], set[Any], str]:
76
+ """
77
+ Detect if string represents a container type (dict, list, tuple, set).
78
+
79
+ Parameters
80
+ ----------
81
+ value : str
82
+ The string value to check.
83
+
84
+ Returns
85
+ -------
86
+ Union[dict[str, Any], list[Any], tuple[Any], set[Any], str]
87
+ Parsed container or original string if not a container.
88
+ """
89
+ if not (value[0] in "{[(" and value[-1] in "}])"):
90
+ return value
91
+
92
+ # Handle empty containers
93
+ if value in ("()", "[]", "{}"):
94
+ return ast.literal_eval(value)
95
+
96
+ # Try JSON first (expects double quotes)
97
+ try:
98
+ parsed = json.loads(value)
99
+ if isinstance(parsed, (dict, list)):
100
+ return parsed # pyright: ignore
101
+ except (json.JSONDecodeError, TypeError):
102
+ pass
103
+
104
+ # Fallback: Python literal (handles single quotes, tuples, sets)
105
+ try:
106
+ parsed = ast.literal_eval(value)
107
+ if isinstance(parsed, (dict, list, tuple, set)):
108
+ return parsed # pyright: ignore
109
+ except (ValueError, SyntaxError):
110
+ pass
111
+
112
+ return value
113
+
114
+
115
+ def _convert_string_value(value: str) -> Any:
116
+ """
117
+ Convert a string value to its detected type.
118
+
119
+ Parameters
120
+ ----------
121
+ value : str
122
+ The string value to convert.
123
+
124
+ Returns
125
+ -------
126
+ Any
127
+ The converted value or original string if no conversion possible.
128
+ """
129
+ # Strip outer quotes if present
130
+ cleaned_value = _strip_outer_quotes(value)
131
+
132
+ # Skip conversion for empty strings
133
+ if not cleaned_value:
134
+ return value
135
+
136
+ # Try conversions in order of specificity
137
+
138
+ # 1. Container types (most specific structure)
139
+ container_result = _detect_container_type(cleaned_value)
140
+ if container_result != cleaned_value:
141
+ return container_result
142
+
143
+ # 2. Null and boolean values
144
+ null_bool_result = _detect_null_or_boolean(cleaned_value)
145
+ if null_bool_result != cleaned_value:
146
+ return null_bool_result
147
+
148
+ # 3. Numeric types
149
+ numeric_result = _detect_numeric_type(cleaned_value)
150
+ if numeric_result != cleaned_value:
151
+ return numeric_result
152
+
153
+ # 4. Keep as string if no conversion succeeded
154
+ return cleaned_value
155
+
156
+
12
157
  def update_dict(original: dict[str, Any]) -> dict[str, Any]:
13
158
  """
14
- Try to determine the type of the dictionary values.
159
+ Convert string values in a dictionary to their detected types.
160
+
161
+ Automatically detects and converts strings that represent:
162
+ - Boolean values (true/false)
163
+ - Null values (none/null/nil/undefined)
164
+ - Integers and floats
165
+ - Container types (lists, dicts, tuples, sets)
15
166
 
16
167
  Parameters
17
168
  ----------
18
169
  original : dict[str, Any]
19
- The original dictionary.
170
+ The original dictionary with potentially string-encoded values.
20
171
 
21
172
  Returns
22
173
  -------
23
174
  dict[str, Any]
24
- The updated dictionary with values converted to the detected types.
175
+ A new dictionary with string values converted to their detected types.
176
+ Non-string values are preserved unchanged.
177
+
178
+ Examples
179
+ --------
180
+ >>> data = {"count": "42", "active": "true", "tags": "['a', 'b']"}
181
+ >>> update_dict(data)
182
+ {"count": 42, "active": True, "tags": ['a', 'b']}
25
183
  """
26
- new_dict: dict[str, Any] = {}
184
+ converted_dict: dict[str, Any] = {}
27
185
 
28
186
  for key, value in original.items():
29
- # Skip conversion if already the desired type
30
- if not isinstance(value, str):
31
- new_dict[key] = value
32
- continue
33
-
34
- value_stripped = value.strip()
35
- if (
36
- value_stripped.startswith('"') and value_stripped.endswith('"')
37
- ) or (value_stripped.startswith("'") and value_stripped.endswith("'")):
38
- value_stripped = value_stripped[1:-1]
39
- if not value_stripped: # Empty or whitespace-only
40
- new_dict[key] = value
41
- continue
42
-
43
- value_lower = value_stripped.lower()
44
-
45
- # Check for None/null
46
- if value_lower in NULL_VALUES:
47
- new_dict[key] = None
48
- # Check for boolean
49
- elif value_lower in BOOL_VALUES:
50
- new_dict[key] = value_lower == "true"
51
- # Check for integer
52
- elif re.fullmatch(r"[-+]?\d+", value_stripped):
53
- new_dict[key] = int(value_stripped)
54
- # Check for float
187
+ # Only process string values
188
+ if isinstance(value, str):
189
+ converted_dict[key] = _convert_string_value(value)
55
190
  else:
56
- try:
57
- # This handles floats, scientific notation, etc.
58
- float_val = float(value_stripped)
59
- new_dict[key] = float_val
60
- except ValueError:
61
- # Keep as string if conversion fails
62
- new_dict[key] = value_stripped
63
-
64
- return new_dict
191
+ converted_dict[key] = value
192
+
193
+ return converted_dict