waldiez 0.5.9__py3-none-any.whl → 0.6.0__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 (109) hide show
  1. waldiez/_version.py +1 -1
  2. waldiez/cli.py +113 -24
  3. waldiez/exporting/agent/exporter.py +9 -6
  4. waldiez/exporting/agent/extras/captain_agent_extras.py +44 -7
  5. waldiez/exporting/agent/extras/group_manager_agent_extas.py +6 -1
  6. waldiez/exporting/agent/extras/handoffs/after_work.py +1 -0
  7. waldiez/exporting/agent/extras/handoffs/available.py +1 -0
  8. waldiez/exporting/agent/extras/handoffs/condition.py +3 -1
  9. waldiez/exporting/agent/extras/handoffs/handoff.py +1 -0
  10. waldiez/exporting/agent/extras/handoffs/target.py +1 -0
  11. waldiez/exporting/agent/termination.py +1 -0
  12. waldiez/exporting/chats/utils/common.py +25 -23
  13. waldiez/exporting/core/__init__.py +0 -2
  14. waldiez/exporting/core/constants.py +3 -1
  15. waldiez/exporting/core/context.py +13 -13
  16. waldiez/exporting/core/extras/serializer.py +12 -10
  17. waldiez/exporting/core/protocols.py +0 -141
  18. waldiez/exporting/core/result.py +5 -5
  19. waldiez/exporting/core/types.py +1 -0
  20. waldiez/exporting/core/utils/llm_config.py +2 -2
  21. waldiez/exporting/flow/execution_generator.py +1 -0
  22. waldiez/exporting/flow/merger.py +2 -2
  23. waldiez/exporting/flow/orchestrator.py +1 -0
  24. waldiez/exporting/flow/utils/common.py +3 -3
  25. waldiez/exporting/flow/utils/importing.py +1 -0
  26. waldiez/exporting/flow/utils/logging.py +7 -80
  27. waldiez/exporting/tools/exporter.py +5 -0
  28. waldiez/exporting/tools/factory.py +4 -0
  29. waldiez/exporting/tools/processor.py +5 -1
  30. waldiez/io/__init__.py +3 -1
  31. waldiez/io/_ws.py +15 -5
  32. waldiez/io/models/content/image.py +1 -0
  33. waldiez/io/models/user_input.py +4 -4
  34. waldiez/io/models/user_response.py +1 -0
  35. waldiez/io/mqtt.py +1 -1
  36. waldiez/io/structured.py +98 -45
  37. waldiez/io/utils.py +17 -11
  38. waldiez/io/ws.py +10 -12
  39. waldiez/logger.py +180 -63
  40. waldiez/models/agents/agent/agent.py +2 -1
  41. waldiez/models/agents/agent/update_system_message.py +0 -2
  42. waldiez/models/agents/doc_agent/doc_agent.py +8 -1
  43. waldiez/models/chat/chat.py +1 -0
  44. waldiez/models/chat/chat_data.py +0 -2
  45. waldiez/models/common/base.py +2 -0
  46. waldiez/models/common/dict_utils.py +169 -40
  47. waldiez/models/common/handoff.py +2 -0
  48. waldiez/models/common/method_utils.py +2 -0
  49. waldiez/models/flow/flow.py +6 -6
  50. waldiez/models/flow/info.py +5 -1
  51. waldiez/models/model/_llm.py +31 -14
  52. waldiez/models/model/model.py +4 -1
  53. waldiez/models/model/model_data.py +18 -5
  54. waldiez/models/tool/predefined/_config.py +5 -1
  55. waldiez/models/tool/predefined/_duckduckgo.py +4 -0
  56. waldiez/models/tool/predefined/_email.py +477 -0
  57. waldiez/models/tool/predefined/_google.py +4 -1
  58. waldiez/models/tool/predefined/_perplexity.py +4 -1
  59. waldiez/models/tool/predefined/_searxng.py +4 -1
  60. waldiez/models/tool/predefined/_tavily.py +4 -1
  61. waldiez/models/tool/predefined/_wikipedia.py +5 -2
  62. waldiez/models/tool/predefined/_youtube.py +4 -1
  63. waldiez/models/tool/predefined/protocol.py +3 -0
  64. waldiez/models/tool/tool.py +22 -4
  65. waldiez/models/waldiez.py +12 -0
  66. waldiez/runner.py +37 -54
  67. waldiez/running/__init__.py +6 -0
  68. waldiez/running/base_runner.py +381 -363
  69. waldiez/running/environment.py +1 -0
  70. waldiez/running/exceptions.py +9 -0
  71. waldiez/running/post_run.py +10 -4
  72. waldiez/running/pre_run.py +199 -66
  73. waldiez/running/protocol.py +21 -101
  74. waldiez/running/run_results.py +1 -1
  75. waldiez/running/standard_runner.py +83 -276
  76. waldiez/running/step_by_step/__init__.py +46 -0
  77. waldiez/running/step_by_step/breakpoints_mixin.py +512 -0
  78. waldiez/running/step_by_step/command_handler.py +151 -0
  79. waldiez/running/step_by_step/events_processor.py +199 -0
  80. waldiez/running/step_by_step/step_by_step_models.py +541 -0
  81. waldiez/running/step_by_step/step_by_step_runner.py +750 -0
  82. waldiez/running/subprocess_runner/__base__.py +279 -0
  83. waldiez/running/subprocess_runner/__init__.py +16 -0
  84. waldiez/running/subprocess_runner/_async_runner.py +362 -0
  85. waldiez/running/subprocess_runner/_sync_runner.py +456 -0
  86. waldiez/running/subprocess_runner/runner.py +570 -0
  87. waldiez/running/timeline_processor.py +1 -1
  88. waldiez/running/utils.py +492 -3
  89. waldiez/utils/version.py +2 -6
  90. waldiez/ws/__init__.py +71 -0
  91. waldiez/ws/__main__.py +15 -0
  92. waldiez/ws/_file_handler.py +199 -0
  93. waldiez/ws/_mock.py +74 -0
  94. waldiez/ws/cli.py +235 -0
  95. waldiez/ws/client_manager.py +851 -0
  96. waldiez/ws/errors.py +416 -0
  97. waldiez/ws/models.py +988 -0
  98. waldiez/ws/reloader.py +363 -0
  99. waldiez/ws/server.py +508 -0
  100. waldiez/ws/session_manager.py +393 -0
  101. waldiez/ws/session_stats.py +83 -0
  102. waldiez/ws/utils.py +410 -0
  103. {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/METADATA +105 -96
  104. {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/RECORD +108 -83
  105. waldiez/running/patch_io_stream.py +0 -210
  106. {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/WHEEL +0 -0
  107. {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/entry_points.txt +0 -0
  108. {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/licenses/LICENSE +0 -0
  109. {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/licenses/NOTICE.md +0 -0
@@ -97,7 +97,7 @@ class UserInputData(BaseModel):
97
97
  parsed = json.loads(parsed)
98
98
  except json.JSONDecodeError:
99
99
  return TextMediaContent(type="text", text=parsed)
100
- if isinstance(parsed, str):
100
+ if isinstance(parsed, str): # pragma: no cover
101
101
  return TextMediaContent(type="text", text=parsed)
102
102
  return cls.validate_content(parsed)
103
103
 
@@ -256,15 +256,15 @@ def extract_filename_from_path(path_or_url: str) -> str:
256
256
  filename = os.path.basename(parsed.path)
257
257
 
258
258
  # Handle edge cases where path might be empty or end with /
259
- if not filename and parsed.path.endswith("/"):
259
+ if not filename and parsed.path.endswith("/"): # pragma: no cover
260
260
  # Try to get the last directory name
261
261
  path_parts = [p for p in parsed.path.split("/") if p]
262
262
  filename = path_parts[-1] if path_parts else parsed.netloc
263
- elif not filename:
263
+ elif not filename: # pragma: no cover
264
264
  # Fallback to using netloc (domain) if no path
265
265
  filename = parsed.netloc
266
266
 
267
267
  return filename
268
268
  # else:
269
269
  # Local file path
270
- return os.path.basename(path_or_url)
270
+ return os.path.basename(path_or_url) # pragma: no cover
@@ -209,6 +209,7 @@ class UserResponse(StructuredBase):
209
209
  # we have probably returned sth till here
210
210
  if isinstance(self.data, str): # pyright: ignore # pragma: no cover
211
211
  return self.data
212
+ # noinspection PyUnreachableCode
212
213
  return ( # pragma: no cover
213
214
  json.dumps(self.data)
214
215
  if hasattr(self.data, "__dict__")
waldiez/io/mqtt.py CHANGED
@@ -473,7 +473,7 @@ class MqttIOStream(IOStream):
473
473
  print_message = PrintMessage.create(*args, **kwargs)
474
474
  try:
475
475
  payload = print_message.model_dump(mode="json")
476
- except Exception:
476
+ except Exception: # pragma: no cover
477
477
  payload = print_message.model_dump(
478
478
  serialize_as_any=True, mode="json", fallback=str
479
479
  )
waldiez/io/structured.py CHANGED
@@ -10,7 +10,6 @@ import threading
10
10
  from getpass import getpass
11
11
  from pathlib import Path
12
12
  from typing import Any
13
- from uuid import uuid4
14
13
 
15
14
  from autogen.events import BaseEvent # type: ignore
16
15
  from autogen.io import IOStream # type: ignore
@@ -23,6 +22,9 @@ from .models import (
23
22
  UserResponse,
24
23
  )
25
24
  from .utils import (
25
+ DEBUG_INPUT_PROMPT,
26
+ START_CHAT_PROMPT,
27
+ MessageType,
26
28
  gen_id,
27
29
  get_image,
28
30
  get_message_dump,
@@ -63,29 +65,43 @@ class StructuredIOStream(IOStream):
63
65
  """
64
66
  sep = kwargs.get("sep", " ")
65
67
  end = kwargs.get("end", "\n")
68
+ flush = kwargs.get("flush", True)
69
+ payload_type = kwargs.get("type", "print")
66
70
  message = sep.join(map(str, args))
67
- is_dumped = is_json_dumped(message)
68
- if is_dumped and end.endswith("\n"):
71
+ if len(args) == 1 and isinstance(args[0], dict):
72
+ message = args[0] # pyright: ignore
73
+ payload_type = message.get("type", payload_type) # pyright: ignore
74
+ is_dumped = True
75
+ else:
76
+ is_dumped, message = is_json_dumped(message)
77
+ if is_dumped:
69
78
  # If the message is already JSON-dumped,
70
79
  # let's try not to double dump it
71
- message = json.loads(message)
72
80
  payload: dict[str, Any] = {
73
- "type": "print",
74
- "id": uuid4().hex,
81
+ "id": gen_id(),
75
82
  "timestamp": now(),
76
- "data": message,
83
+ # "data": message,
77
84
  }
85
+ if isinstance(message, dict):
86
+ payload.update(message) # pyright: ignore
87
+ else:
88
+ payload["data"] = message
89
+ if "type" not in payload:
90
+ payload["type"] = payload_type
78
91
  else:
79
- message += end
80
- payload = PrintMessage(
81
- id=uuid4().hex,
82
- timestamp=now(),
83
- data=message,
84
- ).model_dump(mode="json")
85
- flush = kwargs.get("flush", True)
86
- payload_type = kwargs.get("type", "print")
87
- payload["type"] = payload_type
88
- print(json.dumps(payload, default=str), flush=flush)
92
+ print_message = PrintMessage(data=message) # pyright: ignore
93
+ payload = print_message.model_dump(mode="json", fallback=str)
94
+ payload["type"] = payload_type
95
+ dumped = json.dumps(payload, default=str, ensure_ascii=False) + end
96
+ if kwargs.get("file") and kwargs["file"] in [
97
+ sys.stderr,
98
+ sys.__stderr__,
99
+ sys.stdout,
100
+ sys.__stdout__,
101
+ ]:
102
+ print(dumped, file=kwargs["file"], flush=flush)
103
+ else:
104
+ print(dumped, flush=flush)
89
105
 
90
106
  def input(self, prompt: str = "", *, password: bool = False) -> str:
91
107
  """Structured input from stdin.
@@ -102,36 +118,27 @@ class StructuredIOStream(IOStream):
102
118
  str
103
119
  The line read from the input stream.
104
120
  """
105
- request_id = uuid4().hex
121
+ request_id = gen_id()
106
122
  prompt = prompt or ">"
107
- if not prompt or prompt in [">", "> "]:
123
+ if not prompt or prompt in [">", "> "]: # pragma: no cover
108
124
  # if the prompt is just ">" or "> ",
109
125
  # let's use a more descriptive one
110
- prompt = "Enter your message to start the conversation: "
111
-
112
- self._send_input_request(prompt, request_id, password)
126
+ prompt = START_CHAT_PROMPT
127
+ input_type = "chat"
128
+ if prompt.strip() == DEBUG_INPUT_PROMPT.strip():
129
+ input_type = "debug"
130
+ self._send_input_request(
131
+ prompt,
132
+ request_id,
133
+ password,
134
+ input_type=input_type,
135
+ )
113
136
  user_input_raw = self._read_user_input(prompt, password, request_id)
114
137
  response = self._handle_user_input(user_input_raw, request_id)
115
138
  user_response = response.to_string(
116
139
  uploads_root=self.uploads_root,
117
140
  base_name=request_id,
118
141
  )
119
- # let's keep an eye here:
120
- # https://github.com/ag2ai/ag2/blob/main/autogen/agentchat/conversable_agent.py#L2973
121
- # reply = await iostream.input(prompt) ???? (await???)
122
- if self.is_async:
123
- # let's make a coroutine to just return the user response
124
- async def async_input() -> str:
125
- """Asynchronous input method.
126
-
127
- Returns
128
- -------
129
- str
130
- The line read from the input stream.
131
- """
132
- return user_response
133
-
134
- return async_input() # type: ignore
135
142
  return user_response
136
143
 
137
144
  # noinspection PyMethodMayBeStatic
@@ -168,14 +175,21 @@ class StructuredIOStream(IOStream):
168
175
  self,
169
176
  prompt: str,
170
177
  request_id: str,
171
- password: bool,
178
+ password: bool = False,
179
+ input_type: str = "chat",
172
180
  ) -> None:
181
+ if input_type not in ("chat", "debug"):
182
+ input_type = "chat"
183
+ request_type = (
184
+ "debug_input_request" if input_type == "debug" else "input_request"
185
+ )
173
186
  payload = UserInputRequest(
187
+ type=request_type, # type: ignore
174
188
  request_id=request_id,
175
189
  prompt=prompt,
176
190
  password=password,
177
191
  ).model_dump(mode="json")
178
- print(json.dumps(payload), flush=True)
192
+ print(json.dumps(payload, default=str), flush=True)
179
193
 
180
194
  def _read_user_input(
181
195
  self,
@@ -205,6 +219,11 @@ class StructuredIOStream(IOStream):
205
219
  except queue.Empty:
206
220
  self._send_timeout_message(request_id)
207
221
  return ""
222
+ except BaseException as e:
223
+ self._send_error_message(request_id, str(e))
224
+ return ""
225
+ finally:
226
+ input_thread.join(timeout=1)
208
227
 
209
228
  def _send_timeout_message(self, request_id: str) -> None:
210
229
  timeout_payload = {
@@ -216,6 +235,17 @@ class StructuredIOStream(IOStream):
216
235
  }
217
236
  print(json.dumps(timeout_payload), flush=True, file=sys.stderr)
218
237
 
238
+ # noinspection PyMethodMayBeStatic
239
+ def _send_error_message(self, request_id: str, error_message: str) -> None:
240
+ error_payload = {
241
+ "id": gen_id(),
242
+ "type": "error",
243
+ "request_id": request_id,
244
+ "timestamp": now(),
245
+ "data": error_message,
246
+ }
247
+ print(json.dumps(error_payload), flush=True, file=sys.stderr)
248
+
219
249
  def _handle_user_input(
220
250
  self, user_input_raw: str, request_id: str
221
251
  ) -> UserResponse:
@@ -294,7 +324,12 @@ class StructuredIOStream(IOStream):
294
324
  UserResponse
295
325
  The structured user response.
296
326
  """
297
- # Load the user input
327
+ response_type: MessageType
328
+ _response_type = user_input.get("type", "input_response")
329
+ if _response_type not in ("input_response", "debug_input_response"):
330
+ response_type = "input_response"
331
+ else:
332
+ response_type = _response_type
298
333
  if user_input.get("request_id") == request_id:
299
334
  # We have a valid response to our request
300
335
  data = user_input.get("data")
@@ -302,6 +337,7 @@ class StructuredIOStream(IOStream):
302
337
  # let's check if text|image keys are sent (outside data)
303
338
  if "image" in user_input or "text" in user_input:
304
339
  return UserResponse(
340
+ type=response_type,
305
341
  request_id=request_id,
306
342
  data=self._format_multimedia_response(
307
343
  request_id=request_id, data=user_input
@@ -311,10 +347,12 @@ class StructuredIOStream(IOStream):
311
347
  return self._handle_list_response(
312
348
  data, # pyright: ignore
313
349
  request_id=request_id,
350
+ response_type=response_type,
314
351
  )
315
352
  if not data or not isinstance(data, (str, dict)):
316
353
  # No / invalid data provided in the response
317
354
  return UserResponse(
355
+ type=response_type,
318
356
  request_id=request_id,
319
357
  data="",
320
358
  )
@@ -324,6 +362,7 @@ class StructuredIOStream(IOStream):
324
362
  data = self._load_user_input(data)
325
363
  if isinstance(data, dict):
326
364
  return UserResponse(
365
+ type=response_type,
327
366
  data=self._format_multimedia_response(
328
367
  request_id=request_id,
329
368
  data=data, # pyright: ignore
@@ -333,11 +372,14 @@ class StructuredIOStream(IOStream):
333
372
  # For other types (numbers, bools ,...),
334
373
  # let's just convert to string
335
374
  return UserResponse(
336
- data=str(data), request_id=request_id
375
+ type=response_type,
376
+ data=str(data),
377
+ request_id=request_id,
337
378
  ) # pragma: no cover
338
379
  # This response doesn't match our request_id, log and return empty
339
380
  self._log_mismatched_response(request_id, user_input)
340
381
  return UserResponse(
382
+ type=response_type,
341
383
  request_id=request_id,
342
384
  data="",
343
385
  )
@@ -347,10 +389,12 @@ class StructuredIOStream(IOStream):
347
389
  self,
348
390
  data: list[dict[str, Any]],
349
391
  request_id: str,
392
+ response_type: MessageType,
350
393
  ) -> UserResponse:
351
394
  if len(data) == 0: # pyright: ignore
352
395
  # Empty list, return empty response
353
396
  return UserResponse(
397
+ type=response_type,
354
398
  request_id=request_id,
355
399
  data="",
356
400
  )
@@ -367,10 +411,12 @@ class StructuredIOStream(IOStream):
367
411
  if not input_data: # pragma: no cover
368
412
  # No valid data in the list, return empty response
369
413
  return UserResponse(
414
+ type=response_type,
370
415
  request_id=request_id,
371
416
  data="",
372
417
  )
373
418
  return UserResponse(
419
+ type=response_type,
374
420
  request_id=request_id,
375
421
  data=input_data,
376
422
  )
@@ -387,9 +433,16 @@ class StructuredIOStream(IOStream):
387
433
  The response received
388
434
  """
389
435
  # Create a log message
436
+ got_id: str | None = None
437
+ if isinstance(response, dict):
438
+ got_id = response.get("request_id") # pyright: ignore
439
+ response_str = str(response) # pyright: ignore
440
+ message = response_str[:100] + (
441
+ "..." if len(response_str) > 100 else ""
442
+ )
390
443
  log_payload: dict[str, Any] = {
391
444
  "type": "warning",
392
- "id": uuid4().hex,
445
+ "id": gen_id(),
393
446
  "timestamp": now(),
394
447
  "data": {
395
448
  "message": (
@@ -398,8 +451,8 @@ class StructuredIOStream(IOStream):
398
451
  ),
399
452
  "details": {
400
453
  "expected_id": expected_id,
401
- "received": str(response)[:100]
402
- + ("..." if len(str(response)) > 100 else ""),
454
+ "received_id": got_id,
455
+ "message": message,
403
456
  },
404
457
  },
405
458
  }
waldiez/io/utils.py CHANGED
@@ -14,9 +14,16 @@ from autogen.agentchat.contrib.img_utils import get_pil_image # type: ignore
14
14
  from autogen.events import BaseEvent # type: ignore
15
15
  from autogen.messages import BaseMessage # type: ignore
16
16
 
17
+ DEBUG_INPUT_PROMPT = (
18
+ "[Step] (c)ontinue, (r)un, (q)uit, (i)nfo, (h)elp, (st)ats: "
19
+ )
20
+ START_CHAT_PROMPT = "Enter your message to start the conversation: "
21
+
17
22
  MessageType = Literal[
18
23
  "input_request",
19
24
  "input_response",
25
+ "debug_input_request",
26
+ "debug_input_response",
20
27
  "print",
21
28
  "input",
22
29
  ]
@@ -131,26 +138,25 @@ def get_image(
131
138
  return image_data
132
139
 
133
140
 
134
- def is_json_dumped(value: str) -> bool:
135
- """Check if a string is JSON-dumped.
141
+ def is_json_dumped(value: Any) -> tuple[bool, Any]:
142
+ """Check if a value is JSON-dumped.
136
143
 
137
144
  Parameters
138
145
  ----------
139
- value : str
140
- The string to check.
146
+ value : Any
147
+ The value to check.
141
148
 
142
149
  Returns
143
150
  -------
144
151
  bool
145
- True if the string is JSON-dumped, False otherwise.
152
+ True if the value is JSON-dumped, False otherwise.
146
153
  """
154
+ to_check = value.strip() if isinstance(value, str) else value
147
155
  try:
148
- parsed = json.loads(value)
149
- # If we can parse it as JSON and it's not a string,
150
- # we consider it JSON-dumped
151
- return not isinstance(parsed, str)
156
+ parsed = json.loads(to_check)
157
+ return True, parsed
152
158
  except json.JSONDecodeError:
153
- return False
159
+ return False, value
154
160
 
155
161
 
156
162
  def try_parse_maybe_serialized(value: str) -> Any:
@@ -170,7 +176,7 @@ def try_parse_maybe_serialized(value: str) -> Any:
170
176
  """
171
177
  for parser in (json.loads, ast.literal_eval):
172
178
  # pylint: disable=broad-exception-caught, too-many-try-statements
173
- # noinspection PyBroadException
179
+ # noinspection PyBroadException,TryExceptPass
174
180
  try:
175
181
  parsed: dict[str, Any] | list[Any] | str = parser(value)
176
182
  # Normalize: if it's a single-item list of a string
waldiez/io/ws.py CHANGED
@@ -29,7 +29,7 @@ from .utils import (
29
29
 
30
30
  LOG = logging.getLogger(__name__)
31
31
 
32
- if not is_websocket_available():
32
+ if not is_websocket_available(): # pragma: no cover
33
33
  raise ImportError(
34
34
  "WebSocket support requires "
35
35
  "either the 'websockets' or 'starlette' package. "
@@ -72,14 +72,14 @@ class AsyncWebsocketsIOStream(IOStream):
72
72
  websocket, "receive_message"
73
73
  ):
74
74
  self.websocket: WebSocketConnection = websocket
75
- else:
75
+ else: # pragma: no cover
76
76
  self.websocket = create_websocket_adapter(websocket)
77
77
 
78
78
  self.is_async = is_async
79
79
  self.verbose = verbose
80
80
  self.receive_timeout = receive_timeout
81
81
 
82
- if isinstance(uploads_root, str):
82
+ if isinstance(uploads_root, str): # pragma: no cover
83
83
  uploads_root = Path(uploads_root)
84
84
  if uploads_root is not None:
85
85
  uploads_root = uploads_root.resolve()
@@ -94,7 +94,7 @@ class AsyncWebsocketsIOStream(IOStream):
94
94
  except RuntimeError:
95
95
  pass
96
96
 
97
- if loop is not None:
97
+ if loop is not None: # pragma: no cover
98
98
  # We're in an async context, create a task
99
99
  asyncio.create_task(self.websocket.send_message(json_dump))
100
100
  else:
@@ -118,8 +118,8 @@ class AsyncWebsocketsIOStream(IOStream):
118
118
  end = kwargs.get("end", "\n")
119
119
  msg = sep.join(str(arg) for arg in args)
120
120
 
121
- is_dumped = is_json_dumped(msg)
122
- if is_dumped and end.endswith("\n"):
121
+ is_dumped, msg = is_json_dumped(msg)
122
+ if is_dumped: # pragma: no cover
123
123
  msg = json.loads(msg)
124
124
  else:
125
125
  msg = f"{msg}{end}"
@@ -145,7 +145,7 @@ class AsyncWebsocketsIOStream(IOStream):
145
145
  """
146
146
  message_dump = get_message_dump(message)
147
147
 
148
- if message_dump.get("type") == "text":
148
+ if message_dump.get("type") == "text": # pragma: no cover
149
149
  content_block = message_dump.get("content")
150
150
  if (
151
151
  isinstance(content_block, dict)
@@ -186,7 +186,7 @@ class AsyncWebsocketsIOStream(IOStream):
186
186
 
187
187
  try:
188
188
  return asyncio.run(coro)
189
- except RuntimeError:
189
+ except RuntimeError: # pragma: no cover
190
190
  loop = asyncio.get_event_loop()
191
191
  future = asyncio.run_coroutine_threadsafe(coro, loop)
192
192
  return future.result()
@@ -215,7 +215,7 @@ class AsyncWebsocketsIOStream(IOStream):
215
215
  str
216
216
  The user input, or empty string if timeout exceeded.
217
217
  """
218
- if timeout is None:
218
+ if timeout is None: # pragma: no branch
219
219
  timeout = self.receive_timeout or 120.0
220
220
 
221
221
  request_id = uuid.uuid4().hex
@@ -239,9 +239,7 @@ class AsyncWebsocketsIOStream(IOStream):
239
239
  await self.websocket.send_message(prompt_dump)
240
240
  response = await self.websocket.receive_message(timeout=timeout)
241
241
 
242
- # Handle empty response from timeout
243
- if not response:
244
- LOG.warning("Input request timed out after %s seconds", timeout)
242
+ if not response: # pragma: no cover
245
243
  return ""
246
244
 
247
245
  if self.verbose: