waldiez 0.5.10__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.

Files changed (192) hide show
  1. waldiez/__init__.py +1 -1
  2. waldiez/_version.py +1 -1
  3. waldiez/cli.py +19 -7
  4. waldiez/cli_extras/jupyter.py +3 -0
  5. waldiez/cli_extras/runner.py +3 -1
  6. waldiez/cli_extras/studio.py +3 -1
  7. waldiez/exporter.py +9 -3
  8. waldiez/exporting/agent/exporter.py +15 -16
  9. waldiez/exporting/agent/extras/captain_agent_extras.py +6 -6
  10. waldiez/exporting/agent/extras/doc_agent_extras.py +6 -6
  11. waldiez/exporting/agent/extras/group_manager_agent_extas.py +40 -24
  12. waldiez/exporting/agent/extras/group_member_extras.py +6 -5
  13. waldiez/exporting/agent/extras/handoffs/after_work.py +2 -1
  14. waldiez/exporting/agent/extras/handoffs/available.py +2 -1
  15. waldiez/exporting/agent/extras/handoffs/condition.py +3 -2
  16. waldiez/exporting/agent/extras/handoffs/handoff.py +2 -1
  17. waldiez/exporting/agent/extras/handoffs/target.py +7 -4
  18. waldiez/exporting/agent/extras/rag/chroma_extras.py +27 -19
  19. waldiez/exporting/agent/extras/rag/mongo_extras.py +8 -8
  20. waldiez/exporting/agent/extras/rag/pgvector_extras.py +5 -5
  21. waldiez/exporting/agent/extras/rag/qdrant_extras.py +5 -4
  22. waldiez/exporting/agent/extras/rag/vector_db_extras.py +1 -1
  23. waldiez/exporting/agent/extras/rag_user_proxy_agent_extras.py +5 -7
  24. waldiez/exporting/agent/extras/reasoning_agent_extras.py +3 -5
  25. waldiez/exporting/agent/termination.py +1 -0
  26. waldiez/exporting/chats/exporter.py +4 -4
  27. waldiez/exporting/chats/processor.py +1 -2
  28. waldiez/exporting/chats/utils/common.py +89 -48
  29. waldiez/exporting/chats/utils/group.py +9 -9
  30. waldiez/exporting/chats/utils/nested.py +7 -7
  31. waldiez/exporting/chats/utils/sequential.py +1 -1
  32. waldiez/exporting/chats/utils/single.py +2 -2
  33. waldiez/exporting/core/constants.py +3 -1
  34. waldiez/exporting/core/content.py +7 -7
  35. waldiez/exporting/core/context.py +5 -3
  36. waldiez/exporting/core/exporter.py +5 -3
  37. waldiez/exporting/core/exporters.py +2 -2
  38. waldiez/exporting/core/extras/agent_extras/captain_extras.py +2 -2
  39. waldiez/exporting/core/extras/agent_extras/group_manager_extras.py +2 -2
  40. waldiez/exporting/core/extras/agent_extras/rag_user_extras.py +2 -2
  41. waldiez/exporting/core/extras/agent_extras/standard_extras.py +3 -8
  42. waldiez/exporting/core/extras/base.py +7 -5
  43. waldiez/exporting/core/extras/flow_extras.py +4 -5
  44. waldiez/exporting/core/extras/model_extras.py +2 -2
  45. waldiez/exporting/core/extras/path_resolver.py +1 -2
  46. waldiez/exporting/core/extras/serializer.py +13 -11
  47. waldiez/exporting/core/protocols.py +6 -5
  48. waldiez/exporting/core/result.py +25 -28
  49. waldiez/exporting/core/types.py +11 -10
  50. waldiez/exporting/core/utils/llm_config.py +4 -4
  51. waldiez/exporting/core/validation.py +10 -11
  52. waldiez/exporting/flow/execution_generator.py +99 -10
  53. waldiez/exporting/flow/exporter.py +2 -2
  54. waldiez/exporting/flow/factory.py +2 -2
  55. waldiez/exporting/flow/file_generator.py +4 -2
  56. waldiez/exporting/flow/merger.py +5 -3
  57. waldiez/exporting/flow/orchestrator.py +72 -2
  58. waldiez/exporting/flow/utils/common.py +6 -6
  59. waldiez/exporting/flow/utils/importing.py +7 -8
  60. waldiez/exporting/flow/utils/linting.py +25 -9
  61. waldiez/exporting/flow/utils/logging.py +5 -77
  62. waldiez/exporting/models/exporter.py +8 -8
  63. waldiez/exporting/models/processor.py +5 -5
  64. waldiez/exporting/tools/exporter.py +2 -2
  65. waldiez/exporting/tools/processor.py +7 -4
  66. waldiez/io/__init__.py +11 -5
  67. waldiez/io/_ws.py +12 -6
  68. waldiez/io/models/constants.py +10 -10
  69. waldiez/io/models/content/audio.py +1 -0
  70. waldiez/io/models/content/base.py +20 -18
  71. waldiez/io/models/content/file.py +1 -0
  72. waldiez/io/models/content/image.py +1 -0
  73. waldiez/io/models/content/text.py +1 -0
  74. waldiez/io/models/content/video.py +1 -0
  75. waldiez/io/models/user_input.py +10 -5
  76. waldiez/io/models/user_response.py +17 -16
  77. waldiez/io/mqtt.py +18 -31
  78. waldiez/io/redis.py +18 -22
  79. waldiez/io/structured.py +122 -70
  80. waldiez/io/utils.py +19 -10
  81. waldiez/io/ws.py +7 -3
  82. waldiez/logger.py +16 -3
  83. waldiez/models/agents/__init__.py +3 -0
  84. waldiez/models/agents/agent/agent.py +25 -17
  85. waldiez/models/agents/agent/agent_data.py +25 -22
  86. waldiez/models/agents/agent/code_execution.py +9 -11
  87. waldiez/models/agents/agent/termination_message.py +10 -12
  88. waldiez/models/agents/agent/update_system_message.py +2 -4
  89. waldiez/models/agents/agents.py +8 -8
  90. waldiez/models/agents/assistant/assistant.py +6 -3
  91. waldiez/models/agents/assistant/assistant_data.py +2 -2
  92. waldiez/models/agents/captain/captain_agent.py +7 -4
  93. waldiez/models/agents/captain/captain_agent_data.py +5 -7
  94. waldiez/models/agents/doc_agent/doc_agent.py +7 -4
  95. waldiez/models/agents/doc_agent/doc_agent_data.py +9 -10
  96. waldiez/models/agents/doc_agent/rag_query_engine.py +10 -12
  97. waldiez/models/agents/extra_requirements.py +3 -3
  98. waldiez/models/agents/group_manager/group_manager.py +12 -7
  99. waldiez/models/agents/group_manager/group_manager_data.py +13 -12
  100. waldiez/models/agents/group_manager/speakers.py +17 -19
  101. waldiez/models/agents/rag_user_proxy/rag_user_proxy.py +7 -4
  102. waldiez/models/agents/rag_user_proxy/rag_user_proxy_data.py +4 -1
  103. waldiez/models/agents/rag_user_proxy/retrieve_config.py +69 -63
  104. waldiez/models/agents/rag_user_proxy/vector_db_config.py +19 -19
  105. waldiez/models/agents/reasoning/reasoning_agent.py +7 -4
  106. waldiez/models/agents/reasoning/reasoning_agent_data.py +3 -2
  107. waldiez/models/agents/reasoning/reasoning_agent_reason_config.py +8 -8
  108. waldiez/models/agents/user_proxy/user_proxy.py +6 -3
  109. waldiez/models/agents/user_proxy/user_proxy_data.py +1 -1
  110. waldiez/models/chat/chat.py +28 -20
  111. waldiez/models/chat/chat_data.py +22 -21
  112. waldiez/models/chat/chat_message.py +9 -9
  113. waldiez/models/chat/chat_nested.py +9 -9
  114. waldiez/models/chat/chat_summary.py +6 -6
  115. waldiez/models/common/__init__.py +2 -0
  116. waldiez/models/common/ag2_version.py +2 -0
  117. waldiez/models/common/base.py +2 -0
  118. waldiez/models/common/dict_utils.py +8 -6
  119. waldiez/models/common/handoff.py +20 -17
  120. waldiez/models/common/method_utils.py +9 -7
  121. waldiez/models/common/naming.py +49 -0
  122. waldiez/models/flow/flow.py +11 -6
  123. waldiez/models/flow/flow_data.py +23 -17
  124. waldiez/models/flow/info.py +3 -3
  125. waldiez/models/flow/naming.py +2 -1
  126. waldiez/models/model/_aws.py +11 -13
  127. waldiez/models/model/_llm.py +8 -0
  128. waldiez/models/model/_price.py +2 -4
  129. waldiez/models/model/extra_requirements.py +1 -3
  130. waldiez/models/model/model.py +2 -2
  131. waldiez/models/model/model_data.py +21 -21
  132. waldiez/models/tool/extra_requirements.py +2 -4
  133. waldiez/models/tool/predefined/_duckduckgo.py +1 -0
  134. waldiez/models/tool/predefined/_email.py +4 -0
  135. waldiez/models/tool/predefined/_google.py +1 -0
  136. waldiez/models/tool/predefined/_perplexity.py +2 -1
  137. waldiez/models/tool/predefined/_searxng.py +2 -1
  138. waldiez/models/tool/predefined/_tavily.py +1 -0
  139. waldiez/models/tool/predefined/_wikipedia.py +2 -1
  140. waldiez/models/tool/predefined/_youtube.py +1 -0
  141. waldiez/models/tool/tool.py +8 -5
  142. waldiez/models/tool/tool_data.py +2 -2
  143. waldiez/models/waldiez.py +152 -4
  144. waldiez/runner.py +11 -5
  145. waldiez/running/async_utils.py +192 -0
  146. waldiez/running/base_runner.py +155 -241
  147. waldiez/running/dir_utils.py +52 -0
  148. waldiez/running/environment.py +10 -44
  149. waldiez/running/events_mixin.py +252 -0
  150. waldiez/running/exceptions.py +20 -0
  151. waldiez/running/gen_seq_diagram.py +18 -15
  152. waldiez/running/io_utils.py +216 -0
  153. waldiez/running/protocol.py +11 -5
  154. waldiez/running/requirements_mixin.py +65 -0
  155. waldiez/running/results_mixin.py +926 -0
  156. waldiez/running/standard_runner.py +24 -27
  157. waldiez/running/step_by_step/breakpoints_mixin.py +503 -47
  158. waldiez/running/step_by_step/command_handler.py +154 -0
  159. waldiez/running/step_by_step/events_processor.py +379 -0
  160. waldiez/running/step_by_step/step_by_step_models.py +425 -41
  161. waldiez/running/step_by_step/step_by_step_runner.py +437 -382
  162. waldiez/running/subprocess_runner/__base__.py +13 -8
  163. waldiez/running/subprocess_runner/_async_runner.py +6 -4
  164. waldiez/running/subprocess_runner/_sync_runner.py +11 -6
  165. waldiez/running/subprocess_runner/runner.py +48 -23
  166. waldiez/running/timeline_processor.py +1 -1
  167. waldiez/utils/__init__.py +2 -0
  168. waldiez/utils/conflict_checker.py +4 -4
  169. waldiez/utils/python_manager.py +415 -0
  170. waldiez/ws/__init__.py +8 -7
  171. waldiez/ws/_file_handler.py +18 -20
  172. waldiez/ws/_mock.py +75 -0
  173. waldiez/ws/cli.py +58 -10
  174. waldiez/ws/client_manager.py +77 -53
  175. waldiez/ws/errors.py +3 -0
  176. waldiez/ws/models.py +61 -53
  177. waldiez/ws/reloader.py +33 -4
  178. waldiez/ws/server.py +121 -52
  179. waldiez/ws/session_manager.py +8 -9
  180. waldiez/ws/session_stats.py +1 -1
  181. waldiez/ws/utils.py +33 -5
  182. {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/METADATA +107 -109
  183. waldiez-0.6.1.dist-info/RECORD +254 -0
  184. waldiez/running/post_run.py +0 -180
  185. waldiez/running/pre_run.py +0 -159
  186. waldiez/running/run_results.py +0 -14
  187. waldiez/running/utils.py +0 -511
  188. waldiez-0.5.10.dist-info/RECORD +0 -248
  189. {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/WHEEL +0 -0
  190. {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/entry_points.txt +0 -0
  191. {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/licenses/LICENSE +0 -0
  192. {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/licenses/NOTICE.md +0 -0
@@ -0,0 +1,154 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+
4
+ # pylint: disable=unused-argument
5
+ # pyright: reportImportCycles=false, reportUnusedParameter=false
6
+
7
+ """Command handler for step-by-step execution."""
8
+
9
+ from typing import TYPE_CHECKING, Callable
10
+
11
+ from .step_by_step_models import (
12
+ HELP_MESSAGE,
13
+ WaldiezDebugError,
14
+ WaldiezDebugStepAction,
15
+ )
16
+
17
+ if TYPE_CHECKING:
18
+ # noinspection PyUnusedImports
19
+ from .step_by_step_runner import WaldiezStepByStepRunner
20
+
21
+
22
+ # pylint: disable=too-few-public-methods
23
+ class CommandHandler:
24
+ """Handler for debug commands to reduce complexity in main runner."""
25
+
26
+ def __init__(self, runner: "WaldiezStepByStepRunner"):
27
+ self.runner = runner
28
+ self._command_map: dict[
29
+ str, Callable[[str | None], WaldiezDebugStepAction]
30
+ ] = {
31
+ "c": self._handle_continue,
32
+ "s": self._handle_step,
33
+ "r": self._handle_run,
34
+ "q": self._handle_quit,
35
+ "i": self._handle_info,
36
+ "h": self._handle_help,
37
+ "st": self._handle_stats,
38
+ "ab": self._handle_add_breakpoint,
39
+ "rb": self._handle_remove_breakpoint,
40
+ "lb": self._handle_list_breakpoints,
41
+ "cb": self._handle_clear_breakpoints,
42
+ }
43
+
44
+ def handle_command(self, command_line: str) -> WaldiezDebugStepAction:
45
+ """Handle a command line input.
46
+
47
+ Parameters
48
+ ----------
49
+ command_line : str
50
+ The command line input to handle.
51
+
52
+ Returns
53
+ -------
54
+ WaldiezDebugStepAction
55
+ The action to take for the command.
56
+ """
57
+ if not command_line or not command_line.strip():
58
+ return self._handle_step(None) # Enter = step
59
+ parts = command_line.strip().split(maxsplit=1)
60
+ if not parts or not parts[0]:
61
+ return self._handle_unknown(None)
62
+ command = parts[0].lower()
63
+ args = parts[1] if len(parts) > 1 else None
64
+
65
+ handler = self._command_map.get(command, self._handle_unknown)
66
+ return handler(args)
67
+
68
+ def _handle_continue(self, args: str | None) -> WaldiezDebugStepAction:
69
+ self.runner.step_mode = True
70
+ return WaldiezDebugStepAction.CONTINUE
71
+
72
+ def _handle_step(self, args: str | None) -> WaldiezDebugStepAction:
73
+ self.runner.step_mode = True
74
+ return WaldiezDebugStepAction.STEP
75
+
76
+ def _handle_run(self, args: str | None) -> WaldiezDebugStepAction:
77
+ self.runner.step_mode = False
78
+ return WaldiezDebugStepAction.RUN
79
+
80
+ def _handle_quit(self, args: str | None) -> WaldiezDebugStepAction:
81
+ self.runner.set_stop_requested()
82
+ return WaldiezDebugStepAction.QUIT
83
+
84
+ def _handle_info(self, args: str | None) -> WaldiezDebugStepAction:
85
+ self.runner.show_event_info()
86
+ return WaldiezDebugStepAction.INFO
87
+
88
+ def _handle_help(self, args: str | None) -> WaldiezDebugStepAction:
89
+ self.runner.emit(HELP_MESSAGE)
90
+ return WaldiezDebugStepAction.HELP
91
+
92
+ def _handle_stats(self, args: str | None) -> WaldiezDebugStepAction:
93
+ self.runner.show_stats()
94
+ return WaldiezDebugStepAction.STATS
95
+
96
+ def _handle_add_breakpoint(
97
+ self, args: str | None
98
+ ) -> WaldiezDebugStepAction:
99
+ if args:
100
+ self.runner.add_breakpoint(args)
101
+ return WaldiezDebugStepAction.ADD_BREAKPOINT
102
+ current_event = self.runner.current_event
103
+ if current_event and hasattr(current_event, "type"):
104
+ self.runner.add_breakpoint(getattr(current_event, "type", ""))
105
+ return WaldiezDebugStepAction.ADD_BREAKPOINT
106
+ # else:
107
+ self.runner.emit(
108
+ WaldiezDebugError(
109
+ error=(
110
+ "No breakpoint specification provided "
111
+ "and no current event available"
112
+ )
113
+ )
114
+ )
115
+ return WaldiezDebugStepAction.ADD_BREAKPOINT
116
+
117
+ def _handle_remove_breakpoint(
118
+ self, args: str | None
119
+ ) -> WaldiezDebugStepAction:
120
+ if args:
121
+ self.runner.remove_breakpoint(args)
122
+ return WaldiezDebugStepAction.REMOVE_BREAKPOINT
123
+ current_event = self.runner.current_event
124
+ if current_event and hasattr(current_event, "type"):
125
+ self.runner.remove_breakpoint(getattr(current_event, "type", ""))
126
+ return WaldiezDebugStepAction.REMOVE_BREAKPOINT
127
+ # else:
128
+ self.runner.emit(
129
+ WaldiezDebugError(
130
+ error=(
131
+ "No breakpoint specification provided and "
132
+ "no current event available"
133
+ )
134
+ )
135
+ )
136
+ return WaldiezDebugStepAction.REMOVE_BREAKPOINT
137
+
138
+ def _handle_list_breakpoints(
139
+ self, args: str | None
140
+ ) -> WaldiezDebugStepAction:
141
+ self.runner.list_breakpoints()
142
+ return WaldiezDebugStepAction.LIST_BREAKPOINTS
143
+
144
+ def _handle_clear_breakpoints(
145
+ self, args: str | None
146
+ ) -> WaldiezDebugStepAction:
147
+ self.runner.clear_breakpoints()
148
+ return WaldiezDebugStepAction.CLEAR_BREAKPOINTS
149
+
150
+ def _handle_unknown(self, args: str | None) -> WaldiezDebugStepAction:
151
+ self.runner.emit(
152
+ WaldiezDebugError(error="Unknown command. Use 'h' for help")
153
+ )
154
+ return WaldiezDebugStepAction.UNKNOWN
@@ -0,0 +1,379 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+
4
+ # pylint: disable=unused-argument,no-self-use
5
+ # pyright: reportMissingTypeStubs=false, reportImportCycles=false
6
+ # pyright: reportDeprecated=false, reportUnknownMemberType=false
7
+ # pyright: reportUnknownVariableType=false, reportUnknownArgumentType=false
8
+
9
+ """Command handler for step-by-step execution."""
10
+
11
+ import inspect
12
+ from collections.abc import Mapping, Sequence
13
+ from dataclasses import asdict, is_dataclass
14
+ from typing import TYPE_CHECKING, Any, Optional, Union
15
+
16
+ if TYPE_CHECKING:
17
+ from autogen.agentchat import ConversableAgent # type: ignore
18
+ from autogen.events.base_event import BaseEvent # type: ignore
19
+ from autogen.messages.base_message import BaseMessage # type: ignore
20
+
21
+ # noinspection PyUnusedImports
22
+ from .step_by_step_runner import WaldiezStepByStepRunner
23
+
24
+
25
+ # pylint: disable=too-few-public-methods
26
+ class EventProcessor:
27
+ """Processes events for the step-by-step runner."""
28
+
29
+ def __init__(self, runner: "WaldiezStepByStepRunner"):
30
+ self.runner = runner
31
+
32
+ def process_event(
33
+ self,
34
+ event: Union["BaseEvent", "BaseMessage"],
35
+ agents: list["ConversableAgent"],
36
+ ) -> dict[str, Any]:
37
+ """Shared logic for both sync and async event processing.
38
+
39
+ Parameters
40
+ ----------
41
+ event : BaseEvent | BaseMessage
42
+ The event to process.
43
+ agents : list[ConversableAgent]
44
+ The workflow's known agents.
45
+
46
+ Returns
47
+ -------
48
+ dict[str, Any]
49
+ The result of processing the event.
50
+ """
51
+ self.runner.event_plus_one()
52
+ self.runner.current_event = event
53
+
54
+ if self.runner.is_stop_requested():
55
+ return {"action": "stop", "reason": "stop_requested"}
56
+
57
+ event_info = self._create_event_info(event)
58
+ self._update_participant_info(event_info)
59
+ self._manage_event_history(event_info)
60
+ self._check_for_input_request(event_info)
61
+ self._add_agents_info(event_info, agents)
62
+
63
+ should_break = self.runner.should_break_on_event(
64
+ event, sender_only=True
65
+ )
66
+
67
+ return {
68
+ "action": "break" if should_break else "continue",
69
+ "event_info": event_info,
70
+ }
71
+
72
+ def _create_event_info(
73
+ self,
74
+ event: Union["BaseEvent", "BaseMessage"],
75
+ ) -> dict[str, Any]:
76
+ """Create event info dictionary from event object.
77
+
78
+ Parameters
79
+ ----------
80
+ event : BaseEvent | BaseMessage
81
+ The event to convert to info dict.
82
+
83
+ Returns
84
+ -------
85
+ dict[str, Any]
86
+ Event information dictionary.
87
+ """
88
+ event_info = event.model_dump(
89
+ mode="json", exclude_none=True, fallback=str
90
+ )
91
+ event_info["count"] = self.runner.event_count
92
+ event_info["sender"] = getattr(event, "sender", self.runner.last_sender)
93
+ event_info["recipient"] = getattr(
94
+ event, "recipient", self.runner.last_recipient
95
+ )
96
+ return event_info
97
+
98
+ def _update_participant_info(self, event_info: dict[str, Any]) -> None:
99
+ """Update sender and recipient information in event_info.
100
+
101
+ Parameters
102
+ ----------
103
+ event_info : dict[str, Any]
104
+ Event information dictionary to update.
105
+ """
106
+ if not event_info["sender"] or not event_info["recipient"]:
107
+ self._extract_participants_from_content(event_info)
108
+
109
+ self._check_for_event_speaker(event_info)
110
+ self._extract_participants_from_direct_content(event_info)
111
+
112
+ # Update last known participants
113
+ self.runner.last_sender = event_info["sender"]
114
+ self.runner.last_recipient = event_info["recipient"]
115
+
116
+ def _extract_participants_from_content(
117
+ self, event_info: dict[str, Any]
118
+ ) -> None:
119
+ """Extract sender/recipient from nested content structure.
120
+
121
+ Parameters
122
+ ----------
123
+ event_info : dict[str, Any]
124
+ Event information dictionary to update.
125
+ """
126
+ content = event_info.get("content", {})
127
+ if (
128
+ isinstance(content, dict)
129
+ and "chat_info" in content
130
+ and isinstance(content["chat_info"], dict)
131
+ ):
132
+ content = content.get("chat_info", {})
133
+
134
+ if not event_info["sender"] and "sender" in content:
135
+ event_info["sender"] = content["sender"]
136
+ if not event_info["recipient"] and "recipient" in content:
137
+ event_info["recipient"] = content["recipient"]
138
+
139
+ def _check_for_event_speaker(self, event_info: dict[str, Any]) -> None:
140
+ """Handle speaker information for group chat events.
141
+
142
+ Parameters
143
+ ----------
144
+ event_info : dict[str, Any]
145
+ Event information dictionary to update.
146
+ """
147
+ if "content" in event_info and isinstance(event_info["content"], dict):
148
+ content = event_info.get("content", {})
149
+ speaker = content.get("speaker")
150
+ if isinstance(speaker, str) and speaker:
151
+ event_info["sender"] = speaker
152
+
153
+ def _extract_participants_from_direct_content(
154
+ self, event_info: dict[str, Any]
155
+ ) -> None:
156
+ """Extract sender/recipient directly from content dictionary.
157
+
158
+ Parameters
159
+ ----------
160
+ event_info : dict[str, Any]
161
+ Event information dictionary to update.
162
+ """
163
+ if "content" in event_info and isinstance(event_info["content"], dict):
164
+ content = event_info.get("content", {})
165
+
166
+ sender = content.get("sender", "")
167
+ if isinstance(sender, str) and sender:
168
+ event_info["sender"] = sender
169
+
170
+ recipient = content.get("recipient", "")
171
+ if isinstance(recipient, str) and recipient:
172
+ event_info["recipient"] = recipient
173
+
174
+ def _manage_event_history(self, event_info: dict[str, Any]) -> None:
175
+ """Add event to history and manage history size limits.
176
+
177
+ Parameters
178
+ ----------
179
+ event_info : dict[str, Any]
180
+ Event information to add to history.
181
+ """
182
+ self.runner.add_to_history(event_info)
183
+ self._trim_history_if_needed()
184
+
185
+ def _trim_history_if_needed(self) -> None:
186
+ """Remove oldest events from history if over size limit."""
187
+ current_history = self.runner.event_history
188
+ if len(current_history) > self.runner.max_event_history:
189
+ excess = len(current_history) - self.runner.max_event_history
190
+ for _ in range(excess):
191
+ self.runner.pop_event()
192
+
193
+ def _check_for_input_request(self, event_info: dict[str, Any]) -> None:
194
+ """Swap participant names if we have an input request."""
195
+ if (
196
+ event_info["type"] in ("input_request", "debug_input_request")
197
+ and "sender" in event_info
198
+ and "recipient" in event_info
199
+ ):
200
+ # swap them,
201
+ # before:
202
+ # "recipient" is the user (the one received the input request),
203
+ # make her the "sender" (the one typing...)
204
+ sender = event_info["sender"]
205
+ recipient = event_info["recipient"]
206
+ event_info["sender"] = recipient
207
+ event_info["recipient"] = sender
208
+ self.runner.last_sender = recipient
209
+ self.runner.last_recipient = sender
210
+
211
+ @staticmethod
212
+ def _get_agent_dump(
213
+ agent: Optional["ConversableAgent"],
214
+ ) -> dict[str, Any] | None:
215
+ if not agent:
216
+ return None
217
+ dump = {
218
+ name: _trimmed(value)
219
+ for name, value in vars(agent).items()
220
+ if not name.startswith("_")
221
+ and not inspect.ismethod(value)
222
+ and not inspect.isfunction(value)
223
+ and name.upper() != name
224
+ and name != "chat_messages"
225
+ }
226
+ for attr_key in ["name", "description", "system_message"]:
227
+ attr_val = getattr(agent, attr_key)
228
+ if attr_val:
229
+ dump[attr_key] = _trimmed(attr_val)
230
+ try:
231
+ dump["chat_messages"] = {
232
+ _to_json_key(agent): [_trimmed(message) for message in messages]
233
+ for _agent, messages in agent.chat_messages.items()
234
+ }
235
+ except BaseException: # pylint: disable=broad-exception-caught
236
+ pass
237
+ dump["cost"] = {
238
+ "actual": agent.get_actual_usage(),
239
+ "total": agent.get_total_usage(),
240
+ }
241
+ return dump
242
+
243
+ def _add_agents_info(
244
+ self, event_info: dict[str, Any], agents: list["ConversableAgent"]
245
+ ) -> None:
246
+ """Add agents info."""
247
+ sender = event_info.get("sender", self.runner.last_sender)
248
+ recipient = event_info.get("recipient", self.runner.last_recipient)
249
+
250
+ agent_map = {agent.name: agent for agent in agents}
251
+
252
+ sender_agent = agent_map.get(sender)
253
+ recipient_agent = agent_map.get(recipient)
254
+
255
+ ordered_agents: list["ConversableAgent"] = []
256
+ seen: set[str] = set()
257
+
258
+ for a in (sender_agent, recipient_agent):
259
+ if a and a.name not in seen:
260
+ ordered_agents.append(a)
261
+ seen.add(a.name)
262
+
263
+ for a in agents:
264
+ if a.name not in seen:
265
+ ordered_agents.append(a)
266
+ seen.add(a.name)
267
+
268
+ event_info["agents"] = {
269
+ "sender": self._get_agent_dump(sender_agent),
270
+ "recipient": self._get_agent_dump(recipient_agent),
271
+ "all": [self._get_agent_dump(a) for a in ordered_agents if a],
272
+ }
273
+
274
+
275
+ # pylint: disable=too-complex,too-many-return-statements,
276
+ # pylint: disable=broad-exception-caught,too-complex,too-many-branches
277
+ def _trimmed( # noqa: C901
278
+ value: Any, max_len: int = 200, _depth: int = 0, _max_depth: int = 10
279
+ ) -> Any:
280
+ """Recursively trim values for serialization."""
281
+ if _depth >= _max_depth:
282
+ # Hard stop
283
+ s = str(value)
284
+ return (s[:max_len] + "...") if len(s) > max_len else s
285
+
286
+ if isinstance(value, str):
287
+ return value[:max_len] + "..." if len(value) > max_len else value
288
+
289
+ if value is None or isinstance(value, (int, float, bool)):
290
+ return value
291
+
292
+ if isinstance(value, (bytes, bytearray)):
293
+ try:
294
+ s = value.decode("utf-8", errors="replace")
295
+ return s[:max_len] + "..." if len(s) > max_len else s
296
+ except Exception:
297
+ return f"<{type(value).__name__}> {len(value)} bytes"
298
+
299
+ if callable(value):
300
+ # pylint: disable=too-many-try-statements
301
+ try:
302
+ name = getattr(value, "__name__", repr(value))
303
+ module = getattr(value, "__module__", "")
304
+ if module:
305
+ s = f"<callable> {module}.{name}"
306
+ else:
307
+ s = f"<callable> {name}"
308
+ return s[:max_len] + "..." if len(s) > max_len else s
309
+ except Exception:
310
+ return "<callable>"
311
+
312
+ if _is_dataclass_instance(value):
313
+ try:
314
+ value = asdict(value)
315
+ except Exception:
316
+ s = str(value)
317
+ return (s[:max_len] + "...") if len(s) > max_len else s
318
+
319
+ if isinstance(value, Mapping):
320
+ out: dict[str, Any] = {}
321
+ for k, v in value.items():
322
+ jk = _to_json_key(k, max_len)
323
+ out[jk] = _trimmed(v, max_len, _depth + 1, _max_depth)
324
+ return out
325
+ if isinstance(value, (set, frozenset)):
326
+ items = [_trimmed(it, max_len, _depth + 1, _max_depth) for it in value]
327
+ try:
328
+ items.sort(key=repr)
329
+ except Exception:
330
+ pass
331
+ return items
332
+
333
+ if isinstance(value, Sequence) and not isinstance(
334
+ value, (str, bytes, bytearray)
335
+ ):
336
+ return [_trimmed(it, max_len, _depth + 1, _max_depth) for it in value]
337
+ if hasattr(value, "__dict__"):
338
+ try:
339
+ return _trimmed(
340
+ {
341
+ k: v
342
+ for k, v in vars(value).items()
343
+ if not str(k).startswith("_") and str(k) != "key"
344
+ },
345
+ max_len,
346
+ _depth + 1,
347
+ _max_depth,
348
+ )
349
+ except Exception:
350
+ pass
351
+
352
+ # Fallback: stringify and trim
353
+ try:
354
+ s = str(value)
355
+ return s[:max_len] + "..." if len(s) > max_len else s
356
+ except Exception:
357
+ return None
358
+
359
+
360
+ def _is_dataclass_instance(obj: Any) -> bool:
361
+ """Check if an object is an instance of a dataclass."""
362
+ return is_dataclass(obj) and not isinstance(obj, type)
363
+
364
+
365
+ def _to_json_key(key: Any, max_len: int = 200) -> str:
366
+ """Convert dict key to a JSON-legal key."""
367
+ if key is None or isinstance(key, (bool, int, float)):
368
+ return str(key)
369
+
370
+ if isinstance(key, str):
371
+ return key[:max_len] + "..." if len(key) > max_len else key
372
+
373
+ for _key in ["name", "_name", "__name__"]:
374
+ if hasattr(key, _key):
375
+ return str(getattr(key, _key))
376
+
377
+ # Non-primitive key: include type to avoid collisions like 1 vs "1"
378
+ s = f"<{type(key).__name__}> {repr(key)}"
379
+ return s[:max_len] + "..." if len(s) > max_len else s