waldiez 0.4.7__py3-none-any.whl → 0.4.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of waldiez might be problematic. Click here for more details.

Files changed (248) hide show
  1. waldiez/__init__.py +5 -5
  2. waldiez/_version.py +1 -1
  3. waldiez/cli.py +97 -102
  4. waldiez/exporter.py +61 -19
  5. waldiez/exporting/__init__.py +25 -6
  6. waldiez/exporting/agent/__init__.py +7 -3
  7. waldiez/exporting/agent/code_execution.py +114 -0
  8. waldiez/exporting/agent/exporter.py +354 -0
  9. waldiez/exporting/agent/extras/__init__.py +15 -0
  10. waldiez/exporting/agent/extras/captain_agent_extras.py +315 -0
  11. waldiez/exporting/agent/extras/group/target.py +178 -0
  12. waldiez/exporting/agent/extras/group_manager_agent_extas.py +500 -0
  13. waldiez/exporting/agent/extras/group_member_extras.py +181 -0
  14. waldiez/exporting/agent/extras/handoffs/__init__.py +19 -0
  15. waldiez/exporting/agent/extras/handoffs/after_work.py +78 -0
  16. waldiez/exporting/agent/extras/handoffs/available.py +74 -0
  17. waldiez/exporting/agent/extras/handoffs/condition.py +158 -0
  18. waldiez/exporting/agent/extras/handoffs/handoff.py +171 -0
  19. waldiez/exporting/agent/extras/handoffs/target.py +189 -0
  20. waldiez/exporting/agent/extras/rag/__init__.py +10 -0
  21. waldiez/exporting/agent/{utils/rag_user/chroma_utils.py → extras/rag/chroma_extras.py} +37 -24
  22. waldiez/exporting/agent/{utils/rag_user/mongo_utils.py → extras/rag/mongo_extras.py} +10 -10
  23. waldiez/exporting/agent/{utils/rag_user/pgvector_utils.py → extras/rag/pgvector_extras.py} +13 -13
  24. waldiez/exporting/agent/{utils/rag_user/qdrant_utils.py → extras/rag/qdrant_extras.py} +13 -13
  25. waldiez/exporting/agent/{utils/rag_user/vector_db.py → extras/rag/vector_db_extras.py} +59 -46
  26. waldiez/exporting/agent/extras/rag_user_proxy_agent_extras.py +245 -0
  27. waldiez/exporting/agent/extras/reasoning_agent_extras.py +88 -0
  28. waldiez/exporting/agent/factory.py +95 -0
  29. waldiez/exporting/agent/processor.py +150 -0
  30. waldiez/exporting/agent/system_message.py +36 -0
  31. waldiez/exporting/agent/termination.py +50 -0
  32. waldiez/exporting/chats/__init__.py +7 -3
  33. waldiez/exporting/chats/exporter.py +97 -0
  34. waldiez/exporting/chats/factory.py +65 -0
  35. waldiez/exporting/chats/processor.py +226 -0
  36. waldiez/exporting/chats/utils/__init__.py +6 -5
  37. waldiez/exporting/chats/utils/common.py +11 -45
  38. waldiez/exporting/chats/utils/group.py +55 -0
  39. waldiez/exporting/chats/utils/nested.py +37 -52
  40. waldiez/exporting/chats/utils/sequential.py +72 -61
  41. waldiez/exporting/chats/utils/{single_chat.py → single.py} +48 -50
  42. waldiez/exporting/core/__init__.py +196 -0
  43. waldiez/exporting/core/constants.py +17 -0
  44. waldiez/exporting/core/content.py +69 -0
  45. waldiez/exporting/core/context.py +244 -0
  46. waldiez/exporting/core/enums.py +89 -0
  47. waldiez/exporting/core/errors.py +19 -0
  48. waldiez/exporting/core/exporter.py +390 -0
  49. waldiez/exporting/core/exporters.py +67 -0
  50. waldiez/exporting/core/extras/__init__.py +39 -0
  51. waldiez/exporting/core/extras/agent_extras/__init__.py +27 -0
  52. waldiez/exporting/core/extras/agent_extras/captain_extras.py +57 -0
  53. waldiez/exporting/core/extras/agent_extras/group_manager_extras.py +102 -0
  54. waldiez/exporting/core/extras/agent_extras/rag_user_extras.py +53 -0
  55. waldiez/exporting/core/extras/agent_extras/reasoning_extras.py +68 -0
  56. waldiez/exporting/core/extras/agent_extras/standard_extras.py +263 -0
  57. waldiez/exporting/core/extras/base.py +241 -0
  58. waldiez/exporting/core/extras/chat_extras.py +118 -0
  59. waldiez/exporting/core/extras/flow_extras.py +70 -0
  60. waldiez/exporting/core/extras/model_extras.py +73 -0
  61. waldiez/exporting/core/extras/path_resolver.py +93 -0
  62. waldiez/exporting/core/extras/serializer.py +138 -0
  63. waldiez/exporting/core/extras/tool_extras.py +82 -0
  64. waldiez/exporting/core/protocols.py +259 -0
  65. waldiez/exporting/core/result.py +705 -0
  66. waldiez/exporting/core/types.py +329 -0
  67. waldiez/exporting/core/utils/__init__.py +11 -0
  68. waldiez/exporting/core/utils/comment.py +33 -0
  69. waldiez/exporting/core/utils/llm_config.py +117 -0
  70. waldiez/exporting/core/validation.py +96 -0
  71. waldiez/exporting/flow/__init__.py +6 -2
  72. waldiez/exporting/flow/execution_generator.py +193 -0
  73. waldiez/exporting/flow/exporter.py +107 -0
  74. waldiez/exporting/flow/factory.py +94 -0
  75. waldiez/exporting/flow/file_generator.py +214 -0
  76. waldiez/exporting/flow/merger.py +387 -0
  77. waldiez/exporting/flow/orchestrator.py +411 -0
  78. waldiez/exporting/flow/utils/__init__.py +9 -36
  79. waldiez/exporting/flow/utils/common.py +206 -0
  80. waldiez/exporting/flow/utils/importing.py +373 -0
  81. waldiez/exporting/flow/utils/linting.py +200 -0
  82. waldiez/exporting/flow/utils/{logging_utils.py → logging.py} +23 -9
  83. waldiez/exporting/models/__init__.py +3 -1
  84. waldiez/exporting/models/exporter.py +233 -0
  85. waldiez/exporting/models/factory.py +66 -0
  86. waldiez/exporting/models/processor.py +139 -0
  87. waldiez/exporting/tools/__init__.py +11 -0
  88. waldiez/exporting/tools/exporter.py +207 -0
  89. waldiez/exporting/tools/factory.py +57 -0
  90. waldiez/exporting/tools/processor.py +248 -0
  91. waldiez/exporting/tools/registration.py +133 -0
  92. waldiez/io/__init__.py +128 -0
  93. waldiez/io/_ws.py +199 -0
  94. waldiez/io/models/__init__.py +60 -0
  95. waldiez/io/models/base.py +66 -0
  96. waldiez/io/models/constants.py +78 -0
  97. waldiez/io/models/content/__init__.py +23 -0
  98. waldiez/io/models/content/audio.py +43 -0
  99. waldiez/io/models/content/base.py +45 -0
  100. waldiez/io/models/content/file.py +43 -0
  101. waldiez/io/models/content/image.py +96 -0
  102. waldiez/io/models/content/text.py +37 -0
  103. waldiez/io/models/content/video.py +43 -0
  104. waldiez/io/models/user_input.py +269 -0
  105. waldiez/io/models/user_response.py +215 -0
  106. waldiez/io/mqtt.py +681 -0
  107. waldiez/io/redis.py +782 -0
  108. waldiez/io/structured.py +439 -0
  109. waldiez/io/utils.py +184 -0
  110. waldiez/io/ws.py +298 -0
  111. waldiez/logger.py +481 -0
  112. waldiez/models/__init__.py +108 -51
  113. waldiez/models/agents/__init__.py +34 -70
  114. waldiez/models/agents/agent/__init__.py +10 -4
  115. waldiez/models/agents/agent/agent.py +466 -65
  116. waldiez/models/agents/agent/agent_data.py +119 -47
  117. waldiez/models/agents/agent/agent_type.py +13 -2
  118. waldiez/models/agents/agent/code_execution.py +12 -12
  119. waldiez/models/agents/agent/human_input_mode.py +8 -0
  120. waldiez/models/agents/agent/{linked_skill.py → linked_tool.py} +7 -7
  121. waldiez/models/agents/agent/nested_chat.py +35 -7
  122. waldiez/models/agents/agent/termination_message.py +30 -22
  123. waldiez/models/agents/{swarm_agent → agent}/update_system_message.py +22 -22
  124. waldiez/models/agents/agents.py +58 -63
  125. waldiez/models/agents/assistant/assistant.py +4 -4
  126. waldiez/models/agents/assistant/assistant_data.py +13 -1
  127. waldiez/models/agents/{captain_agent → captain}/captain_agent.py +5 -5
  128. waldiez/models/agents/{captain_agent → captain}/captain_agent_data.py +5 -5
  129. waldiez/models/agents/extra_requirements.py +11 -16
  130. waldiez/models/agents/group_manager/group_manager.py +103 -13
  131. waldiez/models/agents/group_manager/group_manager_data.py +36 -14
  132. waldiez/models/agents/group_manager/speakers.py +77 -24
  133. waldiez/models/agents/{rag_user → rag_user_proxy}/__init__.py +16 -16
  134. waldiez/models/agents/rag_user_proxy/rag_user_proxy.py +64 -0
  135. waldiez/models/agents/{rag_user/rag_user_data.py → rag_user_proxy/rag_user_proxy_data.py} +6 -5
  136. waldiez/models/agents/{rag_user → rag_user_proxy}/retrieve_config.py +182 -114
  137. waldiez/models/agents/{rag_user → rag_user_proxy}/vector_db_config.py +13 -13
  138. waldiez/models/agents/reasoning/reasoning_agent.py +6 -6
  139. waldiez/models/agents/reasoning/reasoning_agent_data.py +110 -63
  140. waldiez/models/agents/reasoning/reasoning_agent_reason_config.py +38 -10
  141. waldiez/models/agents/user_proxy/user_proxy.py +11 -7
  142. waldiez/models/agents/user_proxy/user_proxy_data.py +2 -2
  143. waldiez/models/chat/__init__.py +2 -1
  144. waldiez/models/chat/chat.py +166 -87
  145. waldiez/models/chat/chat_data.py +99 -136
  146. waldiez/models/chat/chat_message.py +33 -23
  147. waldiez/models/chat/chat_nested.py +31 -30
  148. waldiez/models/chat/chat_summary.py +10 -8
  149. waldiez/models/common/__init__.py +52 -2
  150. waldiez/models/common/ag2_version.py +1 -1
  151. waldiez/models/common/base.py +38 -7
  152. waldiez/models/common/dict_utils.py +42 -17
  153. waldiez/models/common/handoff.py +459 -0
  154. waldiez/models/common/id_generator.py +19 -0
  155. waldiez/models/common/method_utils.py +130 -68
  156. waldiez/{exporting/base/utils → models/common}/naming.py +38 -61
  157. waldiez/models/common/waldiez_version.py +37 -0
  158. waldiez/models/flow/__init__.py +9 -2
  159. waldiez/models/flow/connection.py +18 -0
  160. waldiez/models/flow/flow.py +311 -215
  161. waldiez/models/flow/flow_data.py +207 -40
  162. waldiez/models/flow/info.py +85 -0
  163. waldiez/models/flow/naming.py +131 -0
  164. waldiez/models/model/__init__.py +7 -1
  165. waldiez/models/model/extra_requirements.py +3 -12
  166. waldiez/models/model/model.py +76 -21
  167. waldiez/models/model/model_data.py +108 -20
  168. waldiez/models/tool/__init__.py +16 -0
  169. waldiez/models/tool/extra_requirements.py +36 -0
  170. waldiez/models/{skill/skill.py → tool/tool.py} +88 -88
  171. waldiez/models/tool/tool_data.py +51 -0
  172. waldiez/models/tool/tool_type.py +8 -0
  173. waldiez/models/waldiez.py +97 -80
  174. waldiez/runner.py +115 -61
  175. waldiez/running/__init__.py +13 -7
  176. waldiez/running/environment.py +49 -68
  177. waldiez/running/gen_seq_diagram.py +16 -14
  178. waldiez/running/post_run.py +119 -0
  179. waldiez/running/pre_run.py +149 -0
  180. waldiez/running/util.py +134 -0
  181. waldiez/utils/__init__.py +2 -4
  182. waldiez/utils/cli_extras/jupyter.py +5 -3
  183. waldiez/utils/cli_extras/runner.py +6 -4
  184. waldiez/utils/cli_extras/studio.py +6 -4
  185. waldiez/utils/conflict_checker.py +15 -9
  186. waldiez/utils/flaml_warnings.py +5 -5
  187. waldiez/utils/version.py +47 -0
  188. {waldiez-0.4.7.dist-info → waldiez-0.4.9.dist-info}/METADATA +235 -91
  189. waldiez-0.4.9.dist-info/RECORD +203 -0
  190. waldiez/exporting/agent/agent_exporter.py +0 -297
  191. waldiez/exporting/agent/utils/__init__.py +0 -23
  192. waldiez/exporting/agent/utils/captain_agent.py +0 -263
  193. waldiez/exporting/agent/utils/code_execution.py +0 -65
  194. waldiez/exporting/agent/utils/group_manager.py +0 -220
  195. waldiez/exporting/agent/utils/rag_user/__init__.py +0 -7
  196. waldiez/exporting/agent/utils/rag_user/rag_user.py +0 -209
  197. waldiez/exporting/agent/utils/reasoning.py +0 -36
  198. waldiez/exporting/agent/utils/swarm_agent.py +0 -469
  199. waldiez/exporting/agent/utils/teachability.py +0 -41
  200. waldiez/exporting/agent/utils/termination_message.py +0 -44
  201. waldiez/exporting/base/__init__.py +0 -25
  202. waldiez/exporting/base/agent_position.py +0 -75
  203. waldiez/exporting/base/base_exporter.py +0 -118
  204. waldiez/exporting/base/export_position.py +0 -48
  205. waldiez/exporting/base/import_position.py +0 -23
  206. waldiez/exporting/base/mixin.py +0 -137
  207. waldiez/exporting/base/utils/__init__.py +0 -18
  208. waldiez/exporting/base/utils/comments.py +0 -96
  209. waldiez/exporting/base/utils/path_check.py +0 -68
  210. waldiez/exporting/base/utils/to_string.py +0 -84
  211. waldiez/exporting/chats/chats_exporter.py +0 -240
  212. waldiez/exporting/chats/utils/swarm.py +0 -210
  213. waldiez/exporting/flow/flow_exporter.py +0 -528
  214. waldiez/exporting/flow/utils/agent_utils.py +0 -204
  215. waldiez/exporting/flow/utils/chat_utils.py +0 -71
  216. waldiez/exporting/flow/utils/def_main.py +0 -77
  217. waldiez/exporting/flow/utils/flow_content.py +0 -202
  218. waldiez/exporting/flow/utils/flow_names.py +0 -116
  219. waldiez/exporting/flow/utils/importing_utils.py +0 -227
  220. waldiez/exporting/models/models_exporter.py +0 -199
  221. waldiez/exporting/models/utils.py +0 -174
  222. waldiez/exporting/skills/__init__.py +0 -9
  223. waldiez/exporting/skills/skills_exporter.py +0 -176
  224. waldiez/exporting/skills/utils.py +0 -369
  225. waldiez/models/agents/agent/teachability.py +0 -70
  226. waldiez/models/agents/rag_user/rag_user.py +0 -60
  227. waldiez/models/agents/swarm_agent/__init__.py +0 -50
  228. waldiez/models/agents/swarm_agent/after_work.py +0 -179
  229. waldiez/models/agents/swarm_agent/on_condition.py +0 -105
  230. waldiez/models/agents/swarm_agent/on_condition_available.py +0 -142
  231. waldiez/models/agents/swarm_agent/on_condition_target.py +0 -40
  232. waldiez/models/agents/swarm_agent/swarm_agent.py +0 -107
  233. waldiez/models/agents/swarm_agent/swarm_agent_data.py +0 -124
  234. waldiez/models/flow/utils.py +0 -232
  235. waldiez/models/skill/__init__.py +0 -16
  236. waldiez/models/skill/extra_requirements.py +0 -36
  237. waldiez/models/skill/skill_data.py +0 -53
  238. waldiez/models/skill/skill_type.py +0 -8
  239. waldiez/running/running.py +0 -369
  240. waldiez/utils/pysqlite3_checker.py +0 -308
  241. waldiez/utils/rdps_checker.py +0 -122
  242. waldiez-0.4.7.dist-info/RECORD +0 -149
  243. /waldiez/models/agents/{captain_agent → captain}/__init__.py +0 -0
  244. /waldiez/models/agents/{captain_agent → captain}/captain_agent_lib_entry.py +0 -0
  245. {waldiez-0.4.7.dist-info → waldiez-0.4.9.dist-info}/WHEEL +0 -0
  246. {waldiez-0.4.7.dist-info → waldiez-0.4.9.dist-info}/entry_points.txt +0 -0
  247. {waldiez-0.4.7.dist-info → waldiez-0.4.9.dist-info}/licenses/LICENSE +0 -0
  248. {waldiez-0.4.7.dist-info → waldiez-0.4.9.dist-info}/licenses/NOTICE.md +0 -0
@@ -0,0 +1,387 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+
4
+ """Content merger for combining multiple export results."""
5
+
6
+ from dataclasses import dataclass, field
7
+ from typing import Optional, Union
8
+
9
+ from ..core import (
10
+ ContentOrder,
11
+ EnvironmentVariable,
12
+ ExporterContext,
13
+ ExportResult,
14
+ ImportStatement,
15
+ PositionedContent,
16
+ ValidationError,
17
+ ValidationResult,
18
+ )
19
+
20
+
21
+ @dataclass
22
+ class MergeStatistics:
23
+ """Statistics about the merge operation."""
24
+
25
+ total_results: int = 0
26
+ total_imports: int = 0
27
+ deduplicated_imports: int = 0
28
+ total_content_items: int = 0
29
+ total_env_vars: int = 0
30
+ deduplicated_env_vars: int = 0
31
+ conflicts_found: list[str] = field(default_factory=list[str])
32
+
33
+
34
+ class ContentMerger:
35
+ """Intelligently merges multiple ExportResult objects."""
36
+
37
+ def __init__(self, context: Optional[ExporterContext] = None):
38
+ """Initialize the content merger.
39
+
40
+ Parameters
41
+ ----------
42
+ context : Optional[ExporterContext], optional
43
+ The exporter context, by default None
44
+ """
45
+ self.context = context or ExporterContext()
46
+ self.statistics = MergeStatistics()
47
+
48
+ def merge_results(self, results: list[ExportResult]) -> ExportResult:
49
+ """Merge multiple export results into one.
50
+
51
+ Parameters
52
+ ----------
53
+ results : list[ExportResult]
54
+ The export results to merge
55
+
56
+ Returns
57
+ -------
58
+ ExportResult
59
+ The merged export result
60
+ """
61
+ if not results:
62
+ return ExportResult()
63
+
64
+ if len(results) == 1:
65
+ return results[0]
66
+
67
+ # Initialize statistics
68
+ self.statistics = MergeStatistics(total_results=len(results))
69
+
70
+ # Create merged result
71
+ merged = ExportResult()
72
+
73
+ # 1. Merge imports with intelligent deduplication
74
+ merged.imports = self._merge_imports(results)
75
+
76
+ # 2. Merge positioned content with proper ordering
77
+ merged.positioned_content = self._merge_positioned_content(results)
78
+
79
+ # 3. Merge environment variables with conflict detection
80
+ merged.environment_variables = self._merge_environment_variables(
81
+ results
82
+ )
83
+
84
+ # 4. Merge validation results
85
+ merged.validation_result = self._merge_validation_results(results)
86
+
87
+ # 5. Handle main content (typically empty for merged results)
88
+ merged.main_content = self._merge_main_content(results)
89
+
90
+ return merged
91
+
92
+ def _merge_imports(
93
+ self, results: list[ExportResult]
94
+ ) -> set[ImportStatement]:
95
+ """Merge imports with intelligent deduplication and prioritization.
96
+
97
+ Parameters
98
+ ----------
99
+ results : list[ExportResult]
100
+ The export results containing imports
101
+
102
+ Returns
103
+ -------
104
+ set[ImportStatement]
105
+ The merged and deduplicated imports
106
+ """
107
+ all_imports: set[ImportStatement] = set()
108
+
109
+ # Collect all imports
110
+ for result in results:
111
+ all_imports.update(result.imports)
112
+ self.statistics.total_imports += len(result.imports)
113
+
114
+ # Deduplicate based on statement content, keeping best position
115
+ deduplicated = self._deduplicate_imports(all_imports)
116
+
117
+ self.statistics.deduplicated_imports = len(deduplicated)
118
+
119
+ return deduplicated
120
+
121
+ def _deduplicate_imports(
122
+ self, imports: set[ImportStatement]
123
+ ) -> set[ImportStatement]:
124
+ """Deduplicate imports intelligently.
125
+
126
+ Rules:
127
+ 1. Keep import with highest priority (BUILTINS > THIRD_PARTY > LOCAL)
128
+ 2. For same position, keep the one with more metadata
129
+ 3. Detect conflicts and warn
130
+
131
+ Parameters
132
+ ----------
133
+ imports : set[ImportStatement]
134
+ The imports to deduplicate
135
+
136
+ Returns
137
+ -------
138
+ set[ImportStatement]
139
+ The deduplicated imports
140
+ """
141
+ # Group by statement content
142
+ grouped: dict[str, ImportStatement] = {}
143
+ conflicts: list[str] = []
144
+
145
+ for imp in imports:
146
+ key = imp.statement.strip()
147
+
148
+ if key not in grouped:
149
+ grouped[key] = imp
150
+ else:
151
+ existing = grouped[key]
152
+
153
+ # Prioritize by position (lower enum value = higher priority)
154
+ if imp.position.value < existing.position.value:
155
+ # New import has higher priority position
156
+ grouped[key] = imp
157
+ elif imp.position.value == existing.position.value:
158
+ # Same position - check for conflicts
159
+ if imp.metadata != existing.metadata:
160
+ conflicts.append(
161
+ f"Import '{key}' has conflicting metadata: "
162
+ f"{existing.metadata} vs {imp.metadata}"
163
+ )
164
+ # Keep existing (first wins for same priority)
165
+
166
+ if conflicts:
167
+ self.statistics.conflicts_found.extend(conflicts)
168
+ # Log conflicts but continue
169
+ for conflict in conflicts:
170
+ self.context.get_logger().warning(
171
+ f"Import conflict: {conflict}"
172
+ )
173
+
174
+ return set(grouped.values())
175
+
176
+ def _merge_positioned_content(
177
+ self, results: list[ExportResult]
178
+ ) -> list[PositionedContent]:
179
+ """Merge positioned content with proper ordering.
180
+
181
+ The challenge: Different exporters add content at different positions,
182
+ and we need to maintain the correct order while handling a
183
+ gent-specific positioning.
184
+
185
+ Parameters
186
+ ----------
187
+ results : list[ExportResult]
188
+ The export results containing positioned content
189
+
190
+ Returns
191
+ -------
192
+ list[PositionedContent]
193
+ The merged and properly ordered content
194
+ """
195
+ all_content: list[PositionedContent] = []
196
+
197
+ # Collect all positioned content
198
+ for result in results:
199
+ all_content.extend(result.positioned_content)
200
+ self.statistics.total_content_items += len(
201
+ result.positioned_content
202
+ )
203
+
204
+ # Sort by complex criteria for proper ordering
205
+ sorted_content = self._sort_positioned_content(all_content)
206
+
207
+ return sorted_content
208
+
209
+ def _sort_positioned_content(
210
+ self, content: list[PositionedContent]
211
+ ) -> list[PositionedContent]:
212
+ """Sort positioned content by multiple criteria.
213
+
214
+ Sorting Priority:
215
+ 1. ExportPosition (TOP, IMPORTS, TOOLS, MODELS, AGENTS, CHATS, BOTTOM)
216
+ 2. ContentOrder within position (EARLY_SETUP, SETUP, MAIN_CONTENT, etc.)
217
+ 3. Agent ID (for agent-specific content)
218
+ 4. AgentPosition (BEFORE_ALL, BEFORE, AS_ARGUMENT, AFTER, AFTER_ALL)
219
+ 5. Original order (for stability)
220
+
221
+ Parameters
222
+ ----------
223
+ content : list[PositionedContent]
224
+ The content to sort
225
+
226
+ Returns
227
+ -------
228
+ list[PositionedContent]
229
+ The sorted content
230
+ """
231
+ return sorted(
232
+ content,
233
+ key=lambda pc: (
234
+ pc.position.value, # 1. ExportPosition enum value
235
+ self._get_order_value(
236
+ pc.order
237
+ ), # 2. ContentOrder numeric value
238
+ pc.agent_id
239
+ or "", # 3. Agent ID (empty string for non-agent content)
240
+ (
241
+ pc.agent_position.value if pc.agent_position else 0
242
+ ), # 4. AgentPosition
243
+ id(pc), # 5. Original object ID for stability
244
+ ),
245
+ )
246
+
247
+ # pylint: disable=no-self-use
248
+ def _get_order_value(self, order: Union[int, ContentOrder]) -> int:
249
+ """Get numeric value from order for sorting.
250
+
251
+ Parameters
252
+ ----------
253
+ order : Union[int, ContentOrder]
254
+ The order value
255
+
256
+ Returns
257
+ -------
258
+ int
259
+ The numeric order value
260
+ """
261
+ return order.value if isinstance(order, ContentOrder) else order
262
+
263
+ def _merge_environment_variables(
264
+ self, results: list[ExportResult]
265
+ ) -> list[EnvironmentVariable]:
266
+ """Merge environment variables with conflict detection.
267
+
268
+ Rules:
269
+ 1. Deduplicate by variable name
270
+ 2. Detect value conflicts and warn
271
+ 3. Keep first occurrence for conflicts
272
+ 4. Preserve metadata and descriptions
273
+
274
+ Parameters
275
+ ----------
276
+ results : list[ExportResult]
277
+ The export results containing environment variables
278
+
279
+ Returns
280
+ -------
281
+ list[EnvironmentVariable]
282
+ The merged environment variables
283
+ """
284
+ env_vars: dict[str, EnvironmentVariable] = {}
285
+ conflicts: list[str] = []
286
+ for result in results:
287
+ for env_var in result.environment_variables:
288
+ self.statistics.total_env_vars += 1
289
+ key = env_var.name
290
+ if key in env_vars:
291
+ existing = env_vars[key]
292
+
293
+ # Check for value conflicts
294
+ if existing.value != env_var.value:
295
+ conflicts.append(
296
+ f"Environment variable '{key}' has conflicting "
297
+ f"values: '{existing.value}' vs '{env_var.value}'"
298
+ )
299
+ # Keep first occurrence
300
+ # (tools/models take precedence over agents)
301
+
302
+ else:
303
+ env_vars[key] = env_var
304
+
305
+ self.statistics.deduplicated_env_vars = len(env_vars)
306
+
307
+ if conflicts:
308
+ self.statistics.conflicts_found.extend(conflicts)
309
+ # Log conflicts but continue (non-fatal)
310
+ for conflict in conflicts:
311
+ self.context.get_logger().warning(
312
+ f"Environment variable conflict: {conflict}"
313
+ )
314
+
315
+ return list(env_vars.values())
316
+
317
+ def _merge_validation_results(
318
+ self, results: list[ExportResult]
319
+ ) -> ValidationResult:
320
+ """Merge validation results from all export results.
321
+
322
+ Parameters
323
+ ----------
324
+ results : list[ExportResult]
325
+ The export results containing validation results
326
+
327
+ Returns
328
+ -------
329
+ ValidationResult
330
+ The merged validation result
331
+ """
332
+ all_errors: list[ValidationError] = []
333
+ all_warnings: list[ValidationError] = []
334
+
335
+ for result in results:
336
+ if result.validation_result:
337
+ all_errors.extend(result.validation_result.errors)
338
+ all_warnings.extend(result.validation_result.warnings)
339
+
340
+ # Add merger-specific warnings
341
+ if self.statistics.conflicts_found:
342
+ all_warnings.extend(
343
+ [
344
+ ValidationError(message=f"Merge conflict: {conflict}")
345
+ for conflict in self.statistics.conflicts_found
346
+ ]
347
+ )
348
+
349
+ return ValidationResult(
350
+ is_valid=not all_errors,
351
+ errors=all_errors,
352
+ warnings=all_warnings,
353
+ )
354
+
355
+ def _merge_main_content(self, results: list[ExportResult]) -> Optional[str]:
356
+ """Merge main content from results.
357
+
358
+ For flow exports, main content is typically empty since all content
359
+ is positioned. But we handle it for completeness.
360
+
361
+ Parameters
362
+ ----------
363
+ results : list[ExportResult]
364
+ The export results
365
+
366
+ Returns
367
+ -------
368
+ Optional[str]
369
+ The merged main content, or None
370
+ """
371
+ main_contents: list[str] = []
372
+
373
+ for result in results:
374
+ if result.main_content and result.main_content.strip():
375
+ main_contents.append(result.main_content.strip())
376
+
377
+ return "\n\n".join(main_contents) if main_contents else None
378
+
379
+ def get_merge_statistics(self) -> MergeStatistics:
380
+ """Get statistics about the last merge operation.
381
+
382
+ Returns
383
+ -------
384
+ MergeStatistics
385
+ Statistics about the merge operation
386
+ """
387
+ return self.statistics