waldiez 0.3.12__py3-none-any.whl → 0.4.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 (47) hide show
  1. waldiez/_version.py +1 -1
  2. waldiez/cli.py +1 -3
  3. waldiez/exporting/agent/agent_exporter.py +5 -1
  4. waldiez/exporting/agent/utils/captain_agent.py +4 -8
  5. waldiez/exporting/agent/utils/swarm_agent.py +12 -7
  6. waldiez/exporting/base/utils/comments.py +1 -0
  7. waldiez/exporting/chats/utils/swarm.py +1 -1
  8. waldiez/exporting/flow/flow_exporter.py +2 -0
  9. waldiez/exporting/flow/utils/__init__.py +3 -6
  10. waldiez/exporting/flow/utils/flow_content.py +38 -0
  11. waldiez/exporting/flow/utils/importing_utils.py +64 -29
  12. waldiez/exporting/skills/skills_exporter.py +13 -6
  13. waldiez/exporting/skills/utils.py +92 -6
  14. waldiez/models/agents/agent/__init__.py +2 -1
  15. waldiez/models/agents/agent/agent.py +5 -8
  16. waldiez/models/agents/agent/agent_type.py +11 -0
  17. waldiez/models/agents/captain_agent/captain_agent.py +1 -1
  18. waldiez/models/agents/group_manager/speakers.py +3 -0
  19. waldiez/models/agents/rag_user/retrieve_config.py +3 -0
  20. waldiez/models/agents/reasoning/reasoning_agent_reason_config.py +1 -0
  21. waldiez/models/agents/swarm_agent/after_work.py +13 -11
  22. waldiez/models/agents/swarm_agent/on_condition.py +3 -2
  23. waldiez/models/agents/swarm_agent/on_condition_available.py +1 -0
  24. waldiez/models/agents/swarm_agent/swarm_agent_data.py +3 -3
  25. waldiez/models/agents/swarm_agent/update_system_message.py +1 -0
  26. waldiez/models/chat/chat_message.py +1 -0
  27. waldiez/models/chat/chat_summary.py +1 -0
  28. waldiez/models/common/__init__.py +2 -0
  29. waldiez/models/common/method_utils.py +98 -0
  30. waldiez/models/model/extra_requirements.py +2 -0
  31. waldiez/models/model/model_data.py +1 -0
  32. waldiez/models/skill/__init__.py +4 -0
  33. waldiez/models/skill/extra_requirements.py +39 -0
  34. waldiez/models/skill/skill.py +157 -13
  35. waldiez/models/skill/skill_data.py +14 -0
  36. waldiez/models/skill/skill_type.py +8 -0
  37. waldiez/models/waldiez.py +36 -6
  38. waldiez/runner.py +19 -7
  39. waldiez/running/environment.py +30 -1
  40. waldiez/running/running.py +0 -6
  41. waldiez/utils/pysqlite3_checker.py +18 -5
  42. {waldiez-0.3.12.dist-info → waldiez-0.4.0.dist-info}/METADATA +24 -21
  43. {waldiez-0.3.12.dist-info → waldiez-0.4.0.dist-info}/RECORD +47 -44
  44. {waldiez-0.3.12.dist-info → waldiez-0.4.0.dist-info}/WHEEL +0 -0
  45. {waldiez-0.3.12.dist-info → waldiez-0.4.0.dist-info}/entry_points.txt +0 -0
  46. {waldiez-0.3.12.dist-info → waldiez-0.4.0.dist-info}/licenses/LICENSE +0 -0
  47. {waldiez-0.3.12.dist-info → waldiez-0.4.0.dist-info}/licenses/NOTICE.md +0 -0
@@ -13,8 +13,11 @@ from ...common import WaldiezBase, check_function, generate_function
13
13
  from .vector_db_config import WaldiezRagUserVectorDbConfig
14
14
 
15
15
  WaldiezRagUserTask = Literal["code", "qa", "default"]
16
+ """Possible tasks for the retrieve chat."""
16
17
  WaldiezRagUserVectorDb = Literal["chroma", "pgvector", "mongodb", "qdrant"]
18
+ """Possible vector dbs for the retrieve chat."""
17
19
  WaldiezRagUserChunkMode = Literal["multi_lines", "one_line"]
20
+ """Possible chunk modes for the retrieve chat."""
18
21
  WaldiezRagUserModels: Dict[WaldiezRagUserVectorDb, str] = {
19
22
  "chroma": "all-MiniLM-L6-v2",
20
23
  "mongodb": "all-MiniLM-L6-v2",
@@ -8,6 +8,7 @@ from typing_extensions import Annotated, Literal
8
8
  from ...common import WaldiezBase
9
9
 
10
10
  ReasoningConfigMethod = Literal["beam_search", "mcts", "lats", "dfs"]
11
+ """Possible reasoning methods."""
11
12
 
12
13
 
13
14
  class WaldiezReasoningAgentReasonConfig(WaldiezBase):
@@ -17,16 +17,18 @@ from typing_extensions import Annotated, Literal, Self
17
17
  from ...common import WaldiezBase, check_function, generate_function
18
18
 
19
19
  WaldiezSwarmAfterWorkRecipientType = Literal["agent", "option", "callable"]
20
+ """The possible AfterWork recipient types."""
20
21
  WaldiezSwarmAfterWorkOption = Literal[
21
22
  "TERMINATE", "REVERT_TO_USER", "STAY", "SWARM_MANAGER"
22
23
  ]
24
+ """The possible AfterWork options."""
23
25
 
24
26
 
25
27
  CUSTOM_AFTER_WORK = "custom_after_work"
26
28
  CUSTOM_AFTER_WORK_ARGS = ["last_speaker", "messages", "groupchat"]
27
29
  CUSTOM_AFTER_WORK_TYPES = (
28
- ["SwarmAgent", "List[Dict[str, Any]]", "GroupChat"],
29
- "Union[AfterWorkOption, SwarmAgent, str]",
30
+ ["ConversableAgent", "List[Dict[str, Any]]", "GroupChat"],
31
+ "Union[AfterWorkOption, ConversableAgent, str]",
30
32
  )
31
33
 
32
34
 
@@ -45,15 +47,15 @@ class WaldiezSwarmAfterWork(WaldiezBase):
45
47
  recipient_type : WaldiezSwarmAfterWorkRecipientType
46
48
  The type of recipient.
47
49
  Can be 'agent', 'option', or 'callable'.
48
- If 'agent', the recipient is a SwarmAgent.
50
+ If 'agent', the recipient is a Swarm Agent.
49
51
  If 'option', the recipient is an AfterWorkOption :
50
52
  ('TERMINATE', 'REVERT_TO_USER', 'STAY', 'SWARM_MANAGER').
51
53
  If 'callable', it should have the signature:
52
54
  def custom_after_work(
53
- last_speaker: SwarmAgent,
55
+ last_speaker: ConversableAgent,
54
56
  messages: List[dict],
55
57
  groupchat: GroupChat,
56
- ) -> Union[AfterWorkOption, SwarmAgent, str]:
58
+ ) -> Union[AfterWorkOption, ConversableAgent, str]:
57
59
 
58
60
  """
59
61
 
@@ -79,15 +81,15 @@ class WaldiezSwarmAfterWork(WaldiezBase):
79
81
  description=(
80
82
  "The type of recipient. "
81
83
  "Can be 'agent', 'option', or 'callable'. "
82
- "If 'agent', the recipient is a SwarmAgent. "
84
+ "If 'agent', the recipient is a Swarm Agent. "
83
85
  "If 'option', the recipient is an AfterWorkOption :"
84
86
  " ('TERMINATE', 'REVERT_TO_USER', 'STAY', 'SWARM_MANAGER'). "
85
87
  "If 'callable', it should have the signature: "
86
88
  "def custom_after_work("
87
- " last_speaker: SwarmAgent,"
89
+ " last_speaker: ConversableAgent,"
88
90
  " messages: List[Dict[str, Any]],"
89
91
  " groupchat: GroupChat,"
90
- ") -> Union[AfterWorkOption, SwarmAgent, str]:"
92
+ ") -> Union[AfterWorkOption, ConversableAgent, str]:"
91
93
  ),
92
94
  ),
93
95
  ]
@@ -117,13 +119,13 @@ class WaldiezSwarmAfterWork(WaldiezBase):
117
119
  The recipient string and the function content if applicable.
118
120
  """
119
121
  if self.recipient_type == "option":
120
- return f"AFTER_WORK(AfterWorkOption.{self.recipient})", ""
122
+ return f"AfterWork(AfterWorkOption.{self.recipient})", ""
121
123
  if self.recipient_type == "agent":
122
124
  # the the recipient is passed as the agent name
123
125
  # (and not its id), care should be taken to ensure
124
126
  # the all the agents in the flow have unique names
125
127
  agent_instance = agent_names.get(self.recipient, self.recipient)
126
- return f"AFTER_WORK({agent_instance})", ""
128
+ return f"AfterWork({agent_instance})", ""
127
129
 
128
130
  function_name = CUSTOM_AFTER_WORK
129
131
  if name_prefix:
@@ -131,7 +133,7 @@ class WaldiezSwarmAfterWork(WaldiezBase):
131
133
  if name_suffix:
132
134
  function_name = f"{function_name}_{name_suffix}"
133
135
  return (
134
- f"AFTER_WORK({function_name})",
136
+ f"AfterWork({function_name})",
135
137
  generate_function(
136
138
  function_name=function_name,
137
139
  function_args=CUSTOM_AFTER_WORK_ARGS,
@@ -12,6 +12,7 @@ from .on_condition_available import WaldiezSwarmOnConditionAvailable
12
12
  from .on_condition_target import WaldiezSwarmOnConditionTarget
13
13
 
14
14
  WaldiezSwarmOnConditionTargetType = Literal["agent", "nested_chat"]
15
+ """Possible types for the target of the OnCondition."""
15
16
 
16
17
 
17
18
  class WaldiezSwarmOnCondition(WaldiezBase):
@@ -30,7 +31,7 @@ class WaldiezSwarmOnCondition(WaldiezBase):
30
31
  The condition for transitioning to the target agent
31
32
 
32
33
  available: str, optional
33
- Optional condition to determine if this ON_CONDITION is available.
34
+ Optional condition to determine if this OnCondition is available.
34
35
  Can be a Callable or a string. If a string, it will look up the
35
36
  value of the context variable with that name, which should be a bool.
36
37
 
@@ -73,7 +74,7 @@ class WaldiezSwarmOnCondition(WaldiezBase):
73
74
  default_factory=WaldiezSwarmOnConditionAvailable,
74
75
  title="Available",
75
76
  description=(
76
- "Optional condition to determine if this ON_CONDITION "
77
+ "Optional condition to determine if this OnCondition "
77
78
  "is available."
78
79
  ),
79
80
  ),
@@ -12,6 +12,7 @@ from ...common import WaldiezBase, check_function, generate_function
12
12
  WaldiezSwarmOnConditionAvailableCheckType = Literal[
13
13
  "string", "callable", "none"
14
14
  ]
15
+ """Possible types for the `available` check."""
15
16
 
16
17
  CUSTOM_ON_CONDITION_AVAILABLE = "custom_on_condition_available"
17
18
  CUSTOM_ON_CONDITION_AVAILABLE_ARGS = ["agent", "message"]
@@ -29,7 +29,7 @@ class WaldiezSwarmAgentData(WaldiezAgentData):
29
29
  A list of functions (skill ids) to register with the agent.
30
30
 
31
31
  update_agent_state_before_reply : List[str]
32
- A list of functions, including `UPDATE_SYSTEM_MESSAGE`,
32
+ A list of functions, including `UpdateSystemMessage`,
33
33
  called to update the agent's state before it replies. Each function
34
34
  is called when the agent is selected and before it speaks.
35
35
 
@@ -67,10 +67,10 @@ class WaldiezSwarmAgentData(WaldiezAgentData):
67
67
  title="Update Agent State Before Reply",
68
68
  alias="updateAgentStateBeforeReply",
69
69
  description=(
70
- "A list of functions, including UPDATE_SYSTEM_MESSAGEs,"
70
+ "A list of functions, including UpdateSystemMessage,"
71
71
  "called to update the agent's state before it replies. "
72
72
  " Each function is called when the agent is selected "
73
- "and before it speaks. If not an UPDATE_SYSTEM_MESSAGE, "
73
+ "and before it speaks. If not an UpdateSystemMessage, "
74
74
  "it should be a skill id."
75
75
  ),
76
76
  default_factory=list,
@@ -10,6 +10,7 @@ from typing_extensions import Annotated, Literal, Self
10
10
  from ...common import WaldiezBase, check_function, generate_function
11
11
 
12
12
  WaldiezSwarmUpdateFunctionType = Literal["string", "callable"]
13
+ """Possible types for the update function."""
13
14
 
14
15
  CUSTOM_UPDATE_SYSTEM_MESSAGE = "custom_update_system_message"
15
16
  CUSTOM_UPDATE_SYSTEM_MESSAGE_ARGS = ["agent", "messages"]
@@ -12,6 +12,7 @@ from ..common import WaldiezBase, check_function, update_dict
12
12
  WaldiezChatMessageType = Literal[
13
13
  "string", "method", "rag_message_generator", "none"
14
14
  ]
15
+ """Possible types for the message."""
15
16
 
16
17
  CALLABLE_MESSAGE = "callable_message"
17
18
  CALLABLE_MESSAGE_ARGS = ["sender", "recipient", "context"]
@@ -20,6 +20,7 @@ WaldiezChatSummaryMethod = Literal[
20
20
  "reflection_with_llm",
21
21
  "last_msg",
22
22
  ]
23
+ """Possible methods for the LLM summary."""
23
24
 
24
25
 
25
26
  class WaldiezChatSummary(WaldiezBase):
@@ -8,6 +8,7 @@ from .date_utils import now
8
8
  from .dict_utils import update_dict
9
9
  from .method_utils import (
10
10
  check_function,
11
+ gather_code_imports,
11
12
  generate_function,
12
13
  get_function,
13
14
  parse_code_string,
@@ -17,6 +18,7 @@ __all__ = [
17
18
  "WaldiezBase",
18
19
  "now",
19
20
  "check_function",
21
+ "gather_code_imports",
20
22
  "get_autogen_version",
21
23
  "get_function",
22
24
  "generate_function",
@@ -3,6 +3,10 @@
3
3
  """Function related utilities."""
4
4
 
5
5
  import ast
6
+ import importlib.util
7
+ import sys
8
+ import sysconfig
9
+ from pathlib import Path
6
10
  from typing import List, Optional, Tuple
7
11
 
8
12
  import parso
@@ -12,6 +16,35 @@ import parso.tree
12
16
  MAX_VAR_NAME_LENGTH = 64
13
17
 
14
18
 
19
+ def is_standard_library(module_name: str) -> bool:
20
+ """Check if the module is part of the standard library.
21
+
22
+ Parameters
23
+ ----------
24
+ module_name : str
25
+ The module name.
26
+
27
+ Returns
28
+ -------
29
+ bool
30
+ True if the module is part of the standard library.
31
+ """
32
+ if module_name in sys.builtin_module_names:
33
+ return True
34
+ try:
35
+ spec = importlib.util.find_spec(module_name)
36
+ except BaseException: # pylint: disable=broad-except
37
+ return False
38
+ if spec is None or not spec.origin:
39
+ return False
40
+ if "site-packages" in spec.origin:
41
+ return False
42
+ if spec.origin.startswith(sys.prefix) or spec.origin == "frozen":
43
+ return True
44
+ stdlib_path = str(Path(sysconfig.get_path("stdlib")).resolve())
45
+ return spec.origin.startswith(stdlib_path)
46
+
47
+
15
48
  def parse_code_string(
16
49
  code_string: str,
17
50
  ) -> Tuple[Optional[str], Optional[ast.Module]]:
@@ -38,6 +71,71 @@ def parse_code_string(
38
71
  return None, tree
39
72
 
40
73
 
74
+ def gather_code_imports(
75
+ code_string: str,
76
+ is_interop: bool,
77
+ ) -> Tuple[List[str], List[str]]:
78
+ """Gather the imports from the code string.
79
+
80
+ Parameters
81
+ ----------
82
+ code_string : str
83
+ The code string.
84
+ is_interop : bool
85
+ If True, make sure the interoperability import is present.
86
+
87
+ Returns
88
+ -------
89
+ Tuple[List[str], List[str]]
90
+ The standard library imports and the third party imports.
91
+ """
92
+ standard_lib_imports: List[str] = []
93
+ third_party_imports: List[str] = []
94
+ tree = parso.parse(code_string) # type: ignore
95
+ for node in tree.iter_imports():
96
+ if node.type == "import_name":
97
+ full_import_statement = node.get_code().strip()
98
+ module_name = (
99
+ node.get_code().replace("import", "").strip().split(" ")[0]
100
+ )
101
+ if not module_name:
102
+ continue
103
+ if is_standard_library(module_name):
104
+ standard_lib_imports.append(full_import_statement)
105
+ else:
106
+ third_party_imports.append(full_import_statement)
107
+ elif node.type == "import_from":
108
+ full_import_statement = node.get_code().strip()
109
+ module_name = (
110
+ node.get_code().replace("from", "").strip().split(" ")[0]
111
+ )
112
+ if not module_name:
113
+ continue
114
+ if is_standard_library(module_name):
115
+ standard_lib_imports.append(full_import_statement)
116
+ else:
117
+ third_party_imports.append(full_import_statement)
118
+ if is_interop and (
119
+ "from autogen.interop import Interoperability"
120
+ not in third_party_imports
121
+ ):
122
+ third_party_imports.append(
123
+ "from autogen.interop import Interoperability"
124
+ )
125
+ # sorted_standard_lib_imports = # first import x, then from a import b
126
+ sorted_standard_lib_imports = sorted(
127
+ [stmt for stmt in standard_lib_imports if stmt.startswith("import ")]
128
+ ) + sorted(
129
+ [stmt for stmt in standard_lib_imports if stmt.startswith("from ")]
130
+ )
131
+ sorted_third_party_imports = sorted(
132
+ [stmt for stmt in third_party_imports if stmt.startswith("import ")]
133
+ ) + sorted(
134
+ [stmt for stmt in third_party_imports if stmt.startswith("from ")]
135
+ )
136
+ return sorted_standard_lib_imports, sorted_third_party_imports
137
+
138
+
41
139
  def check_function(
42
140
  code_string: str,
43
141
  function_name: str,
@@ -36,6 +36,8 @@ def get_models_extra_requirements(
36
36
  "bedrock", # we might add this later
37
37
  ]
38
38
  for model in models:
39
+ for requirement in model.requirements:
40
+ model_requirements.add(requirement)
39
41
  if model.data.api_type == "google":
40
42
  model_requirements.add(f"pyautogen[gemini]=={autogen_version}")
41
43
  continue
@@ -23,6 +23,7 @@ WaldiezModelAPIType = Literal[
23
23
  "cohere",
24
24
  "other",
25
25
  ]
26
+ """Possible API types for the model."""
26
27
 
27
28
 
28
29
  class WaldiezModelPrice(WaldiezBase):
@@ -2,11 +2,15 @@
2
2
  # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
3
  """Waldiez Skill related models."""
4
4
 
5
+ from .extra_requirements import get_skills_extra_requirements
5
6
  from .skill import SHARED_SKILL_NAME, WaldiezSkill
6
7
  from .skill_data import WaldiezSkillData
8
+ from .skill_type import WaldiezSkillType
7
9
 
8
10
  __all__ = [
9
11
  "SHARED_SKILL_NAME",
10
12
  "WaldiezSkill",
11
13
  "WaldiezSkillData",
14
+ "WaldiezSkillType",
15
+ "get_skills_extra_requirements",
12
16
  ]
@@ -0,0 +1,39 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+ """Waldiez skill extra requirements."""
4
+
5
+ from typing import Iterator, Set
6
+
7
+ from .skill import WaldiezSkill
8
+
9
+
10
+ def get_skills_extra_requirements(
11
+ skills: Iterator[WaldiezSkill],
12
+ autogen_version: str,
13
+ ) -> Set[str]:
14
+ """Get the skills extra requirements.
15
+
16
+ Parameters
17
+ ----------
18
+ skills : List[WaldiezSkill]
19
+ The skills.
20
+ autogen_version : str
21
+ The ag2 version.
22
+ Returns
23
+ -------
24
+ List[str]
25
+ The skills extra requirements.
26
+ """
27
+ skill_requirements: Set[str] = set()
28
+ for skill in skills:
29
+ if skill.skill_type == "langchain":
30
+ skill_requirements.add(
31
+ f"pyautogen[interop-langchain]=={autogen_version}"
32
+ )
33
+ if skill.skill_type == "crewai":
34
+ skill_requirements.add(
35
+ f"pyautogen[interop-crewai]=={autogen_version}"
36
+ )
37
+ for requirement in skill.requirements:
38
+ skill_requirements.add(requirement)
39
+ return skill_requirements
@@ -2,13 +2,23 @@
2
2
  # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
3
  """Waldiez Skill model."""
4
4
 
5
- from typing import Dict, List
5
+ import json
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List, Tuple, Union
6
9
 
7
10
  from pydantic import Field, model_validator
8
11
  from typing_extensions import Annotated, Literal, Self
9
12
 
10
- from ..common import WaldiezBase, get_function, now, parse_code_string
13
+ from ..common import (
14
+ WaldiezBase,
15
+ gather_code_imports,
16
+ get_function,
17
+ now,
18
+ parse_code_string,
19
+ )
11
20
  from .skill_data import WaldiezSkillData
21
+ from .skill_type import WaldiezSkillType
12
22
 
13
23
  SHARED_SKILL_NAME = "waldiez_shared"
14
24
 
@@ -97,6 +107,66 @@ class WaldiezSkill(WaldiezBase):
97
107
  ),
98
108
  ]
99
109
 
110
+ @staticmethod
111
+ def load(data_or_path: Union[str, Path, Dict[str, Any]]) -> "WaldiezSkill":
112
+ """Load a skill from a read-only file.
113
+
114
+ Parameters
115
+ ----------
116
+ data_or_path : Union[str, Path, Dict[str, Any]]
117
+ The path to the read-only file or the loaded data.
118
+
119
+ Returns
120
+ -------
121
+ WaldiezSkill
122
+ The skill.
123
+
124
+ Raises
125
+ ------
126
+ FileNotFoundError
127
+ If the file is not found.
128
+ ValueError
129
+ If the JSON is invalid or the data is invalid.
130
+ """
131
+ if isinstance(data_or_path, dict):
132
+ return WaldiezSkill.model_validate(data_or_path)
133
+ if not isinstance(data_or_path, Path):
134
+ data_or_path = Path(data_or_path)
135
+ resolved = data_or_path.resolve()
136
+ if not resolved.is_file():
137
+ raise FileNotFoundError(f"File not found: {resolved}")
138
+ with resolved.open("r", encoding="utf-8") as file:
139
+ data_string = file.read()
140
+ try:
141
+ data_dict = json.loads(data_string)
142
+ except BaseException as exc: # pylint: disable=broad-except
143
+ raise ValueError(f"Invalid WaldiezSkill/JSON: {exc}") from exc
144
+ return WaldiezSkill.model_validate(data_dict)
145
+
146
+ @property
147
+ def skill_type(self) -> WaldiezSkillType:
148
+ """Get the skill type.
149
+
150
+ Returns
151
+ -------
152
+ WaldiezSkillType
153
+ The type of the skill:
154
+ [shared, custom, langchain, crewai].
155
+ """
156
+ return self.data.skill_type
157
+
158
+ _skill_imports: Tuple[List[str], List[str]] = ([], [])
159
+
160
+ def get_imports(self) -> Tuple[List[str], List[str]]:
161
+ """Get the skill imports.
162
+
163
+ Returns
164
+ -------
165
+ Tuple[List[str], List[str]]
166
+ The builtin and external imports.
167
+ """
168
+ return self._skill_imports
169
+
100
170
  @property
101
171
  def is_shared(self) -> bool:
102
172
  """Check if the skill is shared.
@@ -106,7 +176,18 @@ class WaldiezSkill(WaldiezBase):
106
176
  bool
107
177
  True if the skill is shared, False otherwise.
108
178
  """
109
- return self.name == SHARED_SKILL_NAME
179
+ return self.skill_type == "shared" or self.name == SHARED_SKILL_NAME
180
+
181
+ @property
182
+ def is_interop(self) -> bool:
183
+ """Check if the skill is interoperability.
184
+
185
+ Returns
186
+ -------
187
+ bool
188
+ True if the skill is interoperability, False otherwise.
189
+ """
190
+ return self.skill_type in ("langchain", "crewai")
110
191
 
111
192
  def get_content(self) -> str:
112
193
  """Get the content of the skill.
@@ -116,11 +197,61 @@ class WaldiezSkill(WaldiezBase):
116
197
  str
117
198
  The content of the skill.
118
199
  """
119
- if self.name == SHARED_SKILL_NAME:
120
- # the whole content (globals)
200
+ if self.is_shared or self.is_interop:
121
201
  return self.data.content
202
+ # if custom, only the function content
122
203
  return get_function(self.data.content, self.name)
123
204
 
205
+ def _validate_interop_skill(self) -> None:
206
+ """Validate the interoperability skill.
207
+
208
+ Raises
209
+ ------
210
+ ValueError
211
+ If the skill name is not in the content.
212
+ """
213
+ if self.is_interop:
214
+ # we expect sth like:
215
+ # with single or double quotes for type={skill_type}
216
+ # {skill_name} = *.convert_tool(..., type="{skill_type}", ...)
217
+ if f"{self.name} = " not in self.data.content:
218
+ raise ValueError(
219
+ f"The skill name '{self.name}' is not in the content."
220
+ )
221
+ # we don't want the conversion to ag2 tool (we do it internally)
222
+ # or the skill registration (we do it after having the agent names)
223
+ # so no" .convert_tool(... type="...")
224
+ # or .register_for_llm(...), .register_for_execution(...)
225
+ to_exclude = [
226
+ r".convert_tool\(.+?type=",
227
+ rf"{self.name}.register_for_llm\(",
228
+ rf"{self.name}.register_for_execution\(",
229
+ ]
230
+ for exclude in to_exclude:
231
+ if re.search(exclude, self.data.content):
232
+ raise ValueError(
233
+ f"Invalid skill content: '{exclude}' is not allowed."
234
+ )
235
+
236
+ def _validate_custom_skill(self) -> None:
237
+ """Validate a custom skill.
238
+
239
+ Raises
240
+ ------
241
+ ValueError
242
+ If the skill name is not in the content.
243
+ If the skill content is invalid.
244
+ """
245
+ search = f"def {self.name}("
246
+ if self.skill_type == "custom" and not self.is_shared:
247
+ if search not in self.data.content:
248
+ raise ValueError(
249
+ f"The skill name '{self.name}' is not in the content."
250
+ )
251
+ error, tree = parse_code_string(self.data.content)
252
+ if error is not None or tree is None:
253
+ raise ValueError(f"Invalid skill content: {error}")
254
+
124
255
  @model_validator(mode="after")
125
256
  def validate_data(self) -> Self:
126
257
  """Validate the data.
@@ -136,14 +267,27 @@ class WaldiezSkill(WaldiezBase):
136
267
  If the skill name is not in the content.
137
268
  If the skill content is invalid.
138
269
  """
139
- search = f"def {self.name}("
140
- if self.name != SHARED_SKILL_NAME and search not in self.data.content:
141
- raise ValueError(
142
- f"The skill name '{self.name}' is not in the content."
143
- )
144
- error, tree = parse_code_string(self.data.content)
145
- if error is not None or tree is None:
146
- raise ValueError(f"Invalid skill content: {error}")
270
+ self._validate_custom_skill()
271
+ self._validate_interop_skill()
272
+ self._skill_imports = gather_code_imports(
273
+ self.data.content, self.is_interop
274
+ )
275
+ # remove the imports from the content
276
+ # we 'll place them at the top of the file
277
+ all_imports = self._skill_imports[0] + self._skill_imports[1]
278
+ code_lines = self.data.content.splitlines()
279
+ valid_lines = [
280
+ line
281
+ for line in code_lines
282
+ if not any(line.startswith(imp) for imp in all_imports)
283
+ ]
284
+ # remove empty lines at the beginning and end
285
+ # of the content
286
+ while valid_lines and not valid_lines[0].strip():
287
+ valid_lines.pop(0)
288
+ while valid_lines and not valid_lines[-1].strip():
289
+ valid_lines.pop()
290
+ self.data.content = "\n".join(valid_lines)
147
291
  return self
148
292
 
149
293
  @property
@@ -8,6 +8,7 @@ from pydantic import Field
8
8
  from typing_extensions import Annotated
9
9
 
10
10
  from ..common import WaldiezBase
11
+ from .skill_type import WaldiezSkillType
11
12
 
12
13
 
13
14
  class WaldiezSkillData(WaldiezBase):
@@ -15,12 +16,25 @@ class WaldiezSkillData(WaldiezBase):
15
16
 
16
17
  Attributes
17
18
  ----------
19
+ skill_type : WaldiezSkillType
20
+ The type of the skill: shared, custom, langchain, crewai.
18
21
  content : str
19
22
  The content (source code) of the skill.
20
23
  secrets : Dict[str, str]
21
24
  The secrets (environment variables) of the skill.
22
25
  """
23
26
 
27
+ skill_type: Annotated[
28
+ WaldiezSkillType,
29
+ Field(
30
+ "custom",
31
+ alias="skillType",
32
+ title="Skill Type",
33
+ description=(
34
+ "The type of the skill: shared, custom, langchain, crewai."
35
+ ),
36
+ ),
37
+ ] = "custom"
24
38
  content: Annotated[
25
39
  str,
26
40
  Field(
@@ -0,0 +1,8 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+ """Waldiez Skill types."""
4
+
5
+ from typing_extensions import Literal
6
+
7
+ WaldiezSkillType = Literal["shared", "custom", "langchain", "crewai"]
8
+ """Possible types of a Waldiez Skill."""