waldiez 0.2.2__py3-none-any.whl → 0.3.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 (138) hide show
  1. waldiez/__init__.py +2 -0
  2. waldiez/__main__.py +2 -0
  3. waldiez/_version.py +3 -1
  4. waldiez/cli.py +13 -3
  5. waldiez/cli_extras.py +4 -3
  6. waldiez/conflict_checker.py +4 -3
  7. waldiez/exporter.py +28 -105
  8. waldiez/exporting/__init__.py +8 -9
  9. waldiez/exporting/agent/__init__.py +7 -0
  10. waldiez/exporting/agent/agent_exporter.py +279 -0
  11. waldiez/exporting/agent/utils/__init__.py +23 -0
  12. waldiez/exporting/agent/utils/agent_class_name.py +34 -0
  13. waldiez/exporting/agent/utils/agent_imports.py +50 -0
  14. waldiez/exporting/{agents → agent/utils}/code_execution.py +9 -11
  15. waldiez/exporting/{agents → agent/utils}/group_manager.py +47 -35
  16. waldiez/exporting/{agents → agent/utils}/rag_user/__init__.py +2 -0
  17. waldiez/exporting/{agents → agent/utils}/rag_user/chroma_utils.py +22 -17
  18. waldiez/exporting/{agents → agent/utils}/rag_user/mongo_utils.py +14 -10
  19. waldiez/exporting/{agents → agent/utils}/rag_user/pgvector_utils.py +12 -8
  20. waldiez/exporting/{agents → agent/utils}/rag_user/qdrant_utils.py +11 -8
  21. waldiez/exporting/{agents → agent/utils}/rag_user/rag_user.py +78 -55
  22. waldiez/exporting/{agents → agent/utils}/rag_user/vector_db.py +10 -8
  23. waldiez/exporting/agent/utils/swarm_agent.py +463 -0
  24. waldiez/exporting/{agents → agent/utils}/teachability.py +10 -6
  25. waldiez/exporting/{agents → agent/utils}/termination_message.py +7 -8
  26. waldiez/exporting/base/__init__.py +25 -0
  27. waldiez/exporting/base/agent_position.py +75 -0
  28. waldiez/exporting/base/base_exporter.py +118 -0
  29. waldiez/exporting/base/export_position.py +48 -0
  30. waldiez/exporting/base/import_position.py +23 -0
  31. waldiez/exporting/base/mixin.py +134 -0
  32. waldiez/exporting/base/utils/__init__.py +18 -0
  33. waldiez/exporting/{utils → base/utils}/comments.py +12 -55
  34. waldiez/exporting/{utils → base/utils}/naming.py +14 -4
  35. waldiez/exporting/base/utils/path_check.py +68 -0
  36. waldiez/exporting/{utils/object_string.py → base/utils/to_string.py} +21 -20
  37. waldiez/exporting/chats/__init__.py +5 -12
  38. waldiez/exporting/chats/chats_exporter.py +240 -0
  39. waldiez/exporting/chats/utils/__init__.py +15 -0
  40. waldiez/exporting/chats/utils/common.py +81 -0
  41. waldiez/exporting/chats/{nested.py → utils/nested.py} +125 -86
  42. waldiez/exporting/chats/utils/sequential.py +244 -0
  43. waldiez/exporting/chats/utils/single_chat.py +313 -0
  44. waldiez/exporting/chats/utils/swarm.py +207 -0
  45. waldiez/exporting/flow/__init__.py +5 -3
  46. waldiez/exporting/flow/flow_exporter.py +503 -0
  47. waldiez/exporting/flow/utils/__init__.py +47 -0
  48. waldiez/exporting/flow/utils/agent_utils.py +204 -0
  49. waldiez/exporting/flow/utils/chat_utils.py +71 -0
  50. waldiez/exporting/flow/utils/def_main.py +62 -0
  51. waldiez/exporting/flow/utils/flow_content.py +112 -0
  52. waldiez/exporting/flow/utils/flow_names.py +115 -0
  53. waldiez/exporting/flow/utils/importing_utils.py +179 -0
  54. waldiez/exporting/{utils → flow/utils}/logging_utils.py +34 -31
  55. waldiez/exporting/models/__init__.py +7 -242
  56. waldiez/exporting/models/models_exporter.py +192 -0
  57. waldiez/exporting/models/utils.py +166 -0
  58. waldiez/exporting/skills/__init__.py +7 -161
  59. waldiez/exporting/skills/skills_exporter.py +169 -0
  60. waldiez/exporting/skills/utils.py +281 -0
  61. waldiez/models/__init__.py +25 -7
  62. waldiez/models/agents/__init__.py +70 -0
  63. waldiez/models/agents/agent/__init__.py +11 -1
  64. waldiez/models/agents/agent/agent.py +9 -4
  65. waldiez/models/agents/agent/agent_data.py +3 -1
  66. waldiez/models/agents/agent/code_execution.py +2 -0
  67. waldiez/models/agents/agent/linked_skill.py +2 -0
  68. waldiez/models/agents/agent/nested_chat.py +2 -0
  69. waldiez/models/agents/agent/teachability.py +2 -0
  70. waldiez/models/agents/agent/termination_message.py +49 -13
  71. waldiez/models/agents/agents.py +15 -3
  72. waldiez/models/agents/assistant/__init__.py +2 -0
  73. waldiez/models/agents/assistant/assistant.py +2 -0
  74. waldiez/models/agents/assistant/assistant_data.py +2 -0
  75. waldiez/models/agents/group_manager/__init__.py +9 -1
  76. waldiez/models/agents/group_manager/group_manager.py +2 -0
  77. waldiez/models/agents/group_manager/group_manager_data.py +2 -0
  78. waldiez/models/agents/group_manager/speakers.py +49 -13
  79. waldiez/models/agents/rag_user/__init__.py +21 -4
  80. waldiez/models/agents/rag_user/rag_user.py +3 -1
  81. waldiez/models/agents/rag_user/rag_user_data.py +2 -0
  82. waldiez/models/agents/rag_user/retrieve_config.py +268 -17
  83. waldiez/models/agents/rag_user/vector_db_config.py +5 -3
  84. waldiez/models/agents/swarm_agent/__init__.py +49 -0
  85. waldiez/models/agents/swarm_agent/after_work.py +178 -0
  86. waldiez/models/agents/swarm_agent/on_condition.py +103 -0
  87. waldiez/models/agents/swarm_agent/on_condition_available.py +140 -0
  88. waldiez/models/agents/swarm_agent/on_condition_target.py +40 -0
  89. waldiez/models/agents/swarm_agent/swarm_agent.py +107 -0
  90. waldiez/models/agents/swarm_agent/swarm_agent_data.py +125 -0
  91. waldiez/models/agents/swarm_agent/update_system_message.py +144 -0
  92. waldiez/models/agents/user_proxy/__init__.py +2 -0
  93. waldiez/models/agents/user_proxy/user_proxy.py +2 -0
  94. waldiez/models/agents/user_proxy/user_proxy_data.py +2 -0
  95. waldiez/models/chat/__init__.py +21 -3
  96. waldiez/models/chat/chat.py +241 -7
  97. waldiez/models/chat/chat_data.py +192 -48
  98. waldiez/models/chat/chat_message.py +153 -144
  99. waldiez/models/chat/chat_nested.py +33 -53
  100. waldiez/models/chat/chat_summary.py +2 -0
  101. waldiez/models/common/__init__.py +6 -6
  102. waldiez/models/common/base.py +4 -1
  103. waldiez/models/common/method_utils.py +163 -83
  104. waldiez/models/flow/__init__.py +2 -0
  105. waldiez/models/flow/flow.py +176 -40
  106. waldiez/models/flow/flow_data.py +63 -2
  107. waldiez/models/flow/utils.py +172 -0
  108. waldiez/models/model/__init__.py +2 -0
  109. waldiez/models/model/model.py +25 -6
  110. waldiez/models/model/model_data.py +3 -1
  111. waldiez/models/skill/__init__.py +4 -1
  112. waldiez/models/skill/skill.py +30 -2
  113. waldiez/models/skill/skill_data.py +2 -0
  114. waldiez/models/waldiez.py +28 -4
  115. waldiez/runner.py +142 -228
  116. waldiez/running/__init__.py +33 -0
  117. waldiez/running/environment.py +83 -0
  118. waldiez/running/gen_seq_diagram.py +185 -0
  119. waldiez/running/running.py +300 -0
  120. {waldiez-0.2.2.dist-info → waldiez-0.3.0.dist-info}/METADATA +32 -26
  121. waldiez-0.3.0.dist-info/RECORD +125 -0
  122. waldiez-0.3.0.dist-info/licenses/LICENSE +201 -0
  123. waldiez/exporting/agents/__init__.py +0 -5
  124. waldiez/exporting/agents/agent.py +0 -236
  125. waldiez/exporting/agents/agent_skills.py +0 -67
  126. waldiez/exporting/agents/llm_config.py +0 -53
  127. waldiez/exporting/chats/chats.py +0 -46
  128. waldiez/exporting/chats/helpers.py +0 -420
  129. waldiez/exporting/flow/def_main.py +0 -32
  130. waldiez/exporting/flow/flow.py +0 -189
  131. waldiez/exporting/utils/__init__.py +0 -36
  132. waldiez/exporting/utils/importing.py +0 -265
  133. waldiez/exporting/utils/method_utils.py +0 -35
  134. waldiez/exporting/utils/path_check.py +0 -51
  135. waldiez-0.2.2.dist-info/RECORD +0 -92
  136. waldiez-0.2.2.dist-info/licenses/LICENSE +0 -21
  137. {waldiez-0.2.2.dist-info → waldiez-0.3.0.dist-info}/WHEEL +0 -0
  138. {waldiez-0.2.2.dist-info → waldiez-0.3.0.dist-info}/entry_points.txt +0 -0
@@ -1,29 +1,18 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
1
3
  """Waldiez flow model."""
2
4
 
3
5
  import uuid
4
- from datetime import datetime, timezone
5
6
  from typing import List, Optional, Tuple
6
7
 
7
8
  from pydantic import Field, model_validator
8
9
  from typing_extensions import Annotated, Literal, Self
9
10
 
10
- from ..agents import WaldiezAgent
11
+ from ..agents import WaldiezAgent, WaldiezSwarmAgent
11
12
  from ..chat import WaldiezChat
12
13
  from ..common import WaldiezBase, now
13
14
  from .flow_data import WaldiezFlowData
14
-
15
-
16
- def id_factory() -> str:
17
- """Generate a unique ID.
18
-
19
- Returns
20
- -------
21
- str
22
- The unique ID.
23
- """
24
- now_td = datetime.now(timezone.utc)
25
- now_str = now_td.strftime("%Y%m%d%H%M%S%f")
26
- return f"{now_str}-{uuid.uuid4().hex}"
15
+ from .utils import check_handoff_to_nested_chat, id_factory
27
16
 
28
17
 
29
18
  class WaldiezFlow(WaldiezBase):
@@ -138,6 +127,41 @@ class WaldiezFlow(WaldiezBase):
138
127
  List[Tuple[WaldiezChat, WaldiezAgent, WaldiezAgent]]
139
128
  ] = None
140
129
 
130
+ @property
131
+ def is_async(self) -> bool:
132
+ """Check if the flow is asynchronous.
133
+
134
+ Returns
135
+ -------
136
+ bool
137
+ True if the flow is asynchronous, False otherwise.
138
+ """
139
+ return self.data.is_async
140
+
141
+ @property
142
+ def has_shared_skill(self) -> bool:
143
+ """Check if the flow has a skill with shared variables (global).
144
+
145
+ Returns
146
+ -------
147
+ bool
148
+ True if the flow has shared skills, False otherwise.
149
+ """
150
+ return any(skill.is_shared for skill in self.data.skills)
151
+
152
+ @property
153
+ def is_swarm_flow(self) -> bool:
154
+ """Check if the flow is a swarm flow.
155
+
156
+ Returns
157
+ -------
158
+ bool
159
+ True if the flow is a swarm flow, False otherwise.
160
+ """
161
+ return any(
162
+ agent.agent_type == "swarm" for agent in self.data.agents.members
163
+ )
164
+
141
165
  @property
142
166
  def ordered_flow(
143
167
  self,
@@ -165,18 +189,9 @@ class WaldiezFlow(WaldiezBase):
165
189
  ValueError
166
190
  If the agent with the given ID is not found.
167
191
  """
168
- for user in self.data.agents.users:
169
- if user.id == agent_id:
170
- return user
171
- for assistant in self.data.agents.assistants:
172
- if assistant.id == agent_id:
173
- return assistant
174
- for manager in self.data.agents.managers:
175
- if manager.id == agent_id:
176
- return manager
177
- for rag_user in self.data.agents.rag_users:
178
- if rag_user.id == agent_id:
179
- return rag_user
192
+ for agent in self.data.agents.members:
193
+ if agent.id == agent_id:
194
+ return agent
180
195
  raise ValueError(f"Agent with ID {agent_id} not found.")
181
196
 
182
197
  def _get_flow_order(
@@ -187,10 +202,12 @@ class WaldiezFlow(WaldiezBase):
187
202
  # we only keep the ones with order >=0
188
203
  # and sort them by this property
189
204
  ordered_flow: List[Tuple[WaldiezChat, WaldiezAgent, WaldiezAgent]] = []
190
- sorted_chats_by_order = sorted(
191
- self.data.chats, key=lambda chat: chat.data.order
192
- )
193
- for chat in sorted_chats_by_order:
205
+ # if swarm, we only keep the first chat
206
+ if self.is_swarm_flow:
207
+ ordered_flow = self._get_swarm_flow()
208
+ if ordered_flow:
209
+ return ordered_flow
210
+ for chat in self.data.chats:
194
211
  if chat.data.order < 0:
195
212
  continue
196
213
  source = self.get_agent_by_id(chat.source)
@@ -204,6 +221,51 @@ class WaldiezFlow(WaldiezBase):
204
221
  ordered_flow.append((chat, source, target))
205
222
  return ordered_flow
206
223
 
224
+ def _get_swarm_flow(
225
+ self,
226
+ ) -> List[Tuple[WaldiezChat, WaldiezAgent, WaldiezAgent]]:
227
+ # valid "first" chat:
228
+ # - source is a user|rag_user and target is a swarm
229
+ # - source is a swarm and target is a (and source.is_initial)
230
+ valid_chats: List[WaldiezChat] = []
231
+ for chat in self.data.chats:
232
+ target = self.get_agent_by_id(chat.target)
233
+ source = self.get_agent_by_id(chat.source)
234
+ if (
235
+ source.agent_type in ["user", "rag_user"]
236
+ and target.agent_type == "swarm"
237
+ ):
238
+ return [(chat, source, target)]
239
+ if source.agent_type == "swarm" and target.agent_type == "swarm":
240
+ if isinstance(source, WaldiezSwarmAgent) and source.is_initial:
241
+ return [(chat, source, target)]
242
+ valid_chats.append(chat)
243
+ if not valid_chats:
244
+ return []
245
+ first_chat: Optional[WaldiezChat] = None
246
+ # first check the order
247
+ by_order = sorted(
248
+ filter(lambda edge: edge.data.order >= 0, valid_chats),
249
+ key=lambda edge: edge.data.order,
250
+ )
251
+ if not by_order:
252
+ # let's order by position
253
+ by_position = sorted(
254
+ valid_chats,
255
+ key=lambda chat: chat.data.position,
256
+ )
257
+ if by_position:
258
+ first_chat = by_position[0]
259
+ else:
260
+ first_chat = valid_chats[0]
261
+ else:
262
+ first_chat = by_order[0]
263
+ if first_chat:
264
+ source = self.get_agent_by_id(first_chat.source)
265
+ target = self.get_agent_by_id(first_chat.target)
266
+ return [(first_chat, source, target)]
267
+ return []
268
+
207
269
  def get_agent_connections(
208
270
  self, agent_id: str, all_chats: bool = True
209
271
  ) -> List[str]:
@@ -222,7 +284,7 @@ class WaldiezFlow(WaldiezBase):
222
284
  List[str]
223
285
  The list of agent ids that the agent with the given ID connects to.
224
286
  """
225
- connections = []
287
+ connections: List[str] = []
226
288
  if all_chats:
227
289
  for chat in self.data.chats:
228
290
  if chat.source == agent_id:
@@ -255,9 +317,81 @@ class WaldiezFlow(WaldiezBase):
255
317
  agent = self.get_agent_by_id(group_manager_id)
256
318
  if agent.agent_type != "manager":
257
319
  return []
258
- connections = self.get_agent_connections(group_manager_id)
320
+ connections = self.get_agent_connections(
321
+ group_manager_id,
322
+ all_chats=True,
323
+ )
259
324
  return [self.get_agent_by_id(member_id) for member_id in connections]
260
325
 
326
+ def get_initial_swarm_agent(self) -> Optional[WaldiezAgent]:
327
+ """Get the initial swarm agent.
328
+
329
+ Returns
330
+ -------
331
+ Optional[WaldiezAgent]
332
+ The initial swarm agent if found, None otherwise.
333
+ """
334
+ fallback_agent = None
335
+ for chat in self.data.chats:
336
+ source_agent = self.get_agent_by_id(chat.source)
337
+ target_agent = self.get_agent_by_id(chat.target)
338
+ if (
339
+ target_agent.agent_type == "swarm"
340
+ and source_agent.agent_type != "swarm"
341
+ ):
342
+ return target_agent
343
+ if (
344
+ source_agent.agent_type == "swarm"
345
+ and target_agent.agent_type == "swarm"
346
+ ):
347
+ fallback_agent = source_agent
348
+ break
349
+ for swarm_agent in self.data.agents.swarm_agents:
350
+ if swarm_agent.is_initial:
351
+ return swarm_agent
352
+ return fallback_agent
353
+
354
+ def get_swarm_chat_members(
355
+ self,
356
+ initial_agent: WaldiezAgent,
357
+ ) -> Tuple[List[WaldiezAgent], Optional[WaldiezAgent]]:
358
+ """Get the swarm chat members.
359
+
360
+ Parameters
361
+ ----------
362
+ initial_agent : WaldiezAgent
363
+ The initial agent.
364
+
365
+ Returns
366
+ -------
367
+ Tuple[List[WaldiezAgent], Optional[WaldiezAgent]]
368
+ The list of swarm chat members and the user agent if any.
369
+ """
370
+ if initial_agent.agent_type != "swarm":
371
+ return [], None
372
+ members: List[WaldiezAgent] = [initial_agent]
373
+ user_agent: Optional[WaldiezAgent] = None
374
+ visited_agents = set()
375
+ visited_agents.add(initial_agent.id)
376
+ connections = self.get_agent_connections(
377
+ initial_agent.id,
378
+ all_chats=True,
379
+ )
380
+ while connections:
381
+ agent_id = connections.pop()
382
+ if agent_id in visited_agents:
383
+ continue
384
+ agent = self.get_agent_by_id(agent_id)
385
+ visited_agents.add(agent_id)
386
+ if agent.agent_type == "swarm":
387
+ members.append(agent)
388
+ connections.extend(
389
+ self.get_agent_connections(agent_id, all_chats=True)
390
+ )
391
+ if agent.agent_type in ["user", "rag_user"] and not user_agent:
392
+ user_agent = agent
393
+ return members, user_agent
394
+
261
395
  def _validate_agent_connections(self) -> None:
262
396
  for agent in self.data.agents.members:
263
397
  if not any(
@@ -268,13 +402,6 @@ class WaldiezFlow(WaldiezBase):
268
402
  f"Agent {agent.id} ({agent.name}) "
269
403
  "does not connect to any other node."
270
404
  )
271
- # already covered above
272
- # if agent.agent_type == "manager":
273
- # chat_member_ids = self.get_agent_connections(agent.id)
274
- # if not chat_member_ids:
275
- # raise ValueError(
276
- # f"Manager's {agent.id} group chat has no members."
277
- # )
278
405
 
279
406
  @model_validator(mode="after")
280
407
  def validate_flow(self) -> Self:
@@ -288,6 +415,8 @@ class WaldiezFlow(WaldiezBase):
288
415
  - all the managers have at least one member in the chat group
289
416
  - the ordered flow (chats with position >=0) is not empty
290
417
  - all agents' code execution config functions exist in the flow skills
418
+ - if swarm flow, there is at least one swarm agent
419
+ - if swarm flow, there is an initial swarm agent
291
420
 
292
421
  Returns
293
422
  -------
@@ -313,4 +442,11 @@ class WaldiezFlow(WaldiezBase):
313
442
  raise ValueError("Skill IDs must be unique.")
314
443
  self.data.agents.validate_flow(model_ids, skills_ids)
315
444
  self._validate_agent_connections()
445
+ if self.is_swarm_flow:
446
+ for swarm_agent in self.data.agents.swarm_agents:
447
+ check_handoff_to_nested_chat(
448
+ swarm_agent,
449
+ all_agents=list(self.data.agents.members),
450
+ all_chats=self.data.chats,
451
+ )
316
452
  return self
@@ -1,9 +1,11 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
1
3
  """Waldiez flow data."""
2
4
 
3
5
  from typing import Any, Dict, List
4
6
 
5
- from pydantic import Field
6
- from typing_extensions import Annotated
7
+ from pydantic import Field, model_validator
8
+ from typing_extensions import Annotated, Self
7
9
 
8
10
  from ..agents import WaldiezAgents
9
11
  from ..chat import WaldiezChat
@@ -36,6 +38,8 @@ class WaldiezFlowData(WaldiezBase):
36
38
  The skills of the flow. See `WaldiezSkill`.
37
39
  chats : List[WaldiezChat]
38
40
  The chats of the flow. See `WaldiezChat`.
41
+ is_async : bool
42
+ Whether the flow is asynchronous or not.
39
43
  """
40
44
 
41
45
  # the ones below (nodes,edges, viewport) we ignore
@@ -97,3 +101,60 @@ class WaldiezFlowData(WaldiezBase):
97
101
  default_factory=list,
98
102
  ),
99
103
  ]
104
+ is_async: Annotated[
105
+ bool,
106
+ Field(
107
+ False,
108
+ description="Whether the flow is asynchronous or not",
109
+ title="Is Async",
110
+ ),
111
+ ]
112
+
113
+ @model_validator(mode="after")
114
+ def validate_flow_chats(self) -> Self:
115
+ """Validate the flow chats.
116
+
117
+ Returns
118
+ -------
119
+ WaldiezFlowData
120
+ The flow data.
121
+
122
+ Raises
123
+ ------
124
+ ValueError
125
+ If there is a chat with a prerequisite that does not exist.
126
+ """
127
+ self.chats = sorted(self.chats, key=lambda x: x.order)
128
+ # in async, ag2 uses the "chat_id" field (and it must be an int):
129
+ # ```
130
+ # prerequisites = []
131
+ # for chat_info in chat_queue:
132
+ # if "chat_id" not in chat_info:
133
+ # raise ValueError(
134
+ # "Each chat must have a unique id for "
135
+ # "async multi-chat execution."
136
+ # )
137
+ # chat_id = chat_info["chat_id"]
138
+ # pre_chats = chat_info.get("prerequisites", [])
139
+ # for pre_chat_id in pre_chats:
140
+ # if not isinstance(pre_chat_id, int):
141
+ # raise ValueError("Prerequisite chat id is not int.")
142
+ # prerequisites.append((chat_id, pre_chat_id))
143
+ # return prerequisites
144
+ # ```
145
+ id_to_chat_id: Dict[str, int] = {}
146
+ for index, chat in enumerate(self.chats):
147
+ id_to_chat_id[chat.id] = index
148
+ chat.set_chat_id(index)
149
+ # also update the chat prerequisites
150
+ # we have ids(str), not chat_ids(int)
151
+ for chat in self.chats:
152
+ chat_prerequisites = []
153
+ for chat_id in chat.data.prerequisites:
154
+ if chat_id not in id_to_chat_id:
155
+ raise ValueError(
156
+ f"Chat with id {chat_id} not found in the flow."
157
+ )
158
+ chat_prerequisites.append(id_to_chat_id[chat_id])
159
+ chat.set_prerequisites(chat_prerequisites)
160
+ return self
@@ -0,0 +1,172 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+ """Helpers for the flow model."""
4
+
5
+ import uuid
6
+ from datetime import datetime, timezone
7
+ from typing import List
8
+
9
+ from ..agents import (
10
+ WaldiezAgent,
11
+ WaldiezAgentNestedChat,
12
+ WaldiezAgentNestedChatMessage,
13
+ WaldiezSwarmAgent,
14
+ WaldiezSwarmOnCondition,
15
+ )
16
+ from ..chat import WaldiezChat
17
+
18
+
19
+ def id_factory() -> str:
20
+ """Generate a unique ID.
21
+
22
+ Returns
23
+ -------
24
+ str
25
+ The unique ID.
26
+ """
27
+ now_td = datetime.now(timezone.utc)
28
+ now_str = now_td.strftime("%Y%m%d%H%M%S%f")
29
+ return f"{now_str}-{uuid.uuid4().hex}"
30
+
31
+
32
+ def check_handoff_to_nested_chat(
33
+ agent: WaldiezSwarmAgent,
34
+ all_agents: List[WaldiezAgent],
35
+ all_chats: List[WaldiezChat],
36
+ ) -> None:
37
+ """Check the handoffs to a nested chat.
38
+
39
+ If we have one and the agent does not have nested_chats,
40
+ we should generate them with the `handoff.target.id`
41
+ as the first (chat's) message.
42
+
43
+ Parameters
44
+ ----------
45
+ agent : WaldiezSwarmAgent
46
+ The swarm agent.
47
+ all_agents : List[WaldiezAgent]
48
+ All agents.
49
+ all_chats : List[WaldiezChat]
50
+ All chats.
51
+
52
+ Raises
53
+ ------
54
+ ValueError
55
+ If the agent has a handoff to a nested chat,
56
+ but no chat found with it as a source.
57
+ """
58
+ # pylint: disable=too-complex
59
+ for handoff in agent.handoffs:
60
+ if not isinstance(handoff, WaldiezSwarmOnCondition):
61
+ continue
62
+ is_nested_chat = handoff.target_type == "nested_chat"
63
+ if is_nested_chat:
64
+ # check if the agent already has nested_chats
65
+ # but only to get the order (and the first chat/message)
66
+ # either way, we must include all the connections that
67
+ # are swarm => non-swarm
68
+ # if we have the orders, ok, else we get them from the
69
+ # edges positions
70
+ all_connections = sorted(
71
+ get_nested_chat_swarm_connections(agent, all_agents, all_chats),
72
+ key=lambda x: x.data.order,
73
+ )
74
+ if not all_connections:
75
+ raise ValueError(
76
+ f"Agent {agent.name} has a handoff to a nested chat, "
77
+ "but no chat found with it as a source."
78
+ )
79
+ agent_nested_chats = agent.nested_chats
80
+ if not agent_nested_chats or not agent_nested_chats[0].messages:
81
+ agent.data.nested_chats = [
82
+ WaldiezAgentNestedChat(
83
+ triggered_by=[],
84
+ messages=[
85
+ WaldiezAgentNestedChatMessage(
86
+ id=chat.id,
87
+ is_reply=False,
88
+ )
89
+ for chat in all_connections
90
+ ],
91
+ )
92
+ ]
93
+ break
94
+ agent.data.nested_chats = merge_nested_chat_messages(
95
+ agent_nested_chats[0].messages, all_connections
96
+ )
97
+
98
+
99
+ def get_nested_chat_swarm_connections(
100
+ agent: WaldiezSwarmAgent,
101
+ all_agents: List[WaldiezAgent],
102
+ all_chats: List[WaldiezChat],
103
+ ) -> List[WaldiezChat]:
104
+ """Get the nested chat connections.
105
+
106
+ Parameters
107
+ ----------
108
+ agent : WaldiezSwarmAgent
109
+ The swarm agent.
110
+ all_agents : Iterator[WaldiezAgent]
111
+ All agents.
112
+ all_chats : List[WaldiezChat]
113
+ All chats.
114
+
115
+ Returns
116
+ -------
117
+ List[WaldiezAgentNestedChat]
118
+ The nested chat connections.
119
+ """
120
+ connections_with_non_swarm_targets = []
121
+ for chat in all_chats:
122
+ if chat.source != agent.id:
123
+ continue
124
+ target_agent = next(
125
+ (a for a in all_agents if a.id == chat.target), None
126
+ )
127
+ if not target_agent or target_agent.agent_type != "swarm":
128
+ connections_with_non_swarm_targets.append(chat)
129
+ return connections_with_non_swarm_targets
130
+
131
+
132
+ def merge_nested_chat_messages(
133
+ agent_nested_chat_messages: List[WaldiezAgentNestedChatMessage],
134
+ all_connections: List[WaldiezChat],
135
+ ) -> List[WaldiezAgentNestedChat]:
136
+ """Merge the nested chat messages.
137
+
138
+ Parameters
139
+ ----------
140
+ all_connections : List[WaldiezChat]
141
+ The connections.
142
+ agent_nested_chat_messages : List[WaldiezAgentNestedChatMessage]
143
+ The agent's nested chat messages.
144
+
145
+ Returns
146
+ -------
147
+ List[WaldiezAgentNestedChat]
148
+ The merged nested chat with all the messages.
149
+ """
150
+ nested_chat = WaldiezAgentNestedChat(triggered_by=[], messages=[])
151
+ chat_ids_added: List[str] = []
152
+ for message in agent_nested_chat_messages:
153
+ chat = next((c for c in all_connections if c.id == message.id), None)
154
+ if chat and chat.id not in chat_ids_added:
155
+ nested_chat.messages.append(
156
+ WaldiezAgentNestedChatMessage(
157
+ id=chat.id,
158
+ is_reply=False,
159
+ )
160
+ )
161
+ chat_ids_added.append(chat.id)
162
+ for chat in all_connections:
163
+ if chat.id not in chat_ids_added:
164
+ nested_chat.messages.append(
165
+ WaldiezAgentNestedChatMessage(
166
+ id=chat.id,
167
+ is_reply=False,
168
+ )
169
+ )
170
+ chat_ids_added.append(chat.id)
171
+ nested_chat.messages.sort(key=lambda x: chat_ids_added.index(x.id))
172
+ return [nested_chat]
@@ -1,3 +1,5 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
1
3
  """Waldiez model."""
2
4
 
3
5
  from .model import DEFAULT_BASE_URLS, WaldiezModel
@@ -1,4 +1,6 @@
1
- """Waldiez model(llm) model."""
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+ """Waldiez model model."""
2
4
 
3
5
  import os
4
6
  from typing import Any, Dict, List, Optional
@@ -102,6 +104,27 @@ class WaldiezModel(WaldiezBase):
102
104
  Field(..., title="Data", description="The data of the model."),
103
105
  ]
104
106
 
107
+ @property
108
+ def api_key_env_key(self) -> str:
109
+ """Get the model's api key environment key to check.
110
+
111
+ - openai: 'OPENAI_API_KEY',
112
+ - azure: 'AZURE_API_KEY',
113
+ - google: 'GOOGLE_GEMINI_API_KEY',
114
+ - anthropic: 'ANTHROPIC_API_KEY',
115
+ - mistral: 'MISTRAL_API_KEY',
116
+ - groq: 'GROQ_API_KEY',
117
+ - together: 'TOGETHER_API_KEY',
118
+ - nim: 'NIM_API_KEY',
119
+ - other: 'OPENAI_API_KEY'
120
+ """
121
+ env_key = "OPENAI_API_KEY"
122
+ if self.data.api_type == "google":
123
+ env_key = "GOOGLE_GEMINI_API_KEY"
124
+ elif self.data.api_type not in ["openai", "other"]:
125
+ env_key = f"{self.data.api_type.upper()}_API_KEY"
126
+ return env_key
127
+
105
128
  @property
106
129
  def api_key(self) -> str:
107
130
  """Get the model's api key.
@@ -120,11 +143,7 @@ class WaldiezModel(WaldiezBase):
120
143
  """
121
144
  if self.data.api_key:
122
145
  return self.data.api_key
123
- env_key = "OPENAI_API_KEY"
124
- if self.data.api_type == "google":
125
- env_key = "GOOGLE_GEMINI_API_KEY"
126
- elif self.data.api_type not in ["openai", "other"]:
127
- env_key = f"{self.data.api_type.upper()}_API_KEY"
146
+ env_key = self.api_key_env_key
128
147
  api_key = os.environ.get(env_key, "")
129
148
  return api_key
130
149
 
@@ -1,6 +1,8 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+ # flake8: noqa E501
1
4
  """Waldiez Model Data."""
2
5
 
3
- # flake8: noqa E501
4
6
  from typing import Dict, Optional
5
7
 
6
8
  from pydantic import Field
@@ -1,9 +1,12 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
1
3
  """Waldiez Skill related models."""
2
4
 
3
- from .skill import WaldiezSkill
5
+ from .skill import SHARED_SKILL_NAME, WaldiezSkill
4
6
  from .skill_data import WaldiezSkillData
5
7
 
6
8
  __all__ = [
9
+ "SHARED_SKILL_NAME",
7
10
  "WaldiezSkill",
8
11
  "WaldiezSkillData",
9
12
  ]
@@ -1,3 +1,5 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
1
3
  """Waldiez Skill model."""
2
4
 
3
5
  from typing import Dict, List
@@ -5,9 +7,11 @@ from typing import Dict, List
5
7
  from pydantic import Field, model_validator
6
8
  from typing_extensions import Annotated, Literal, Self
7
9
 
8
- from ..common import WaldiezBase, now, parse_code_string
10
+ from ..common import WaldiezBase, get_function, now, parse_code_string
9
11
  from .skill_data import WaldiezSkillData
10
12
 
13
+ SHARED_SKILL_NAME = "waldiez_shared"
14
+
11
15
 
12
16
  class WaldiezSkill(WaldiezBase):
13
17
  """Waldiez Skill.
@@ -93,6 +97,30 @@ class WaldiezSkill(WaldiezBase):
93
97
  ),
94
98
  ]
95
99
 
100
+ @property
101
+ def is_shared(self) -> bool:
102
+ """Check if the skill is shared.
103
+
104
+ Returns
105
+ -------
106
+ bool
107
+ True if the skill is shared, False otherwise.
108
+ """
109
+ return self.name == SHARED_SKILL_NAME
110
+
111
+ def get_content(self) -> str:
112
+ """Get the content of the skill.
113
+
114
+ Returns
115
+ -------
116
+ str
117
+ The content of the skill.
118
+ """
119
+ if self.name == SHARED_SKILL_NAME:
120
+ # the whole content (globals)
121
+ return self.data.content
122
+ return get_function(self.data.content, self.name)
123
+
96
124
  @model_validator(mode="after")
97
125
  def validate_data(self) -> Self:
98
126
  """Validate the data.
@@ -109,7 +137,7 @@ class WaldiezSkill(WaldiezBase):
109
137
  If the skill content is invalid.
110
138
  """
111
139
  search = f"def {self.name}("
112
- if search not in self.data.content:
140
+ if self.name != SHARED_SKILL_NAME and search not in self.data.content:
113
141
  raise ValueError(
114
142
  f"The skill name '{self.name}' is not in the content."
115
143
  )
@@ -1,3 +1,5 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
1
3
  """Waldiez Skill model."""
2
4
 
3
5
  from typing import Dict