alita-sdk 0.3.379__py3-none-any.whl → 0.3.627__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.
Files changed (278) hide show
  1. alita_sdk/cli/__init__.py +10 -0
  2. alita_sdk/cli/__main__.py +17 -0
  3. alita_sdk/cli/agent/__init__.py +5 -0
  4. alita_sdk/cli/agent/default.py +258 -0
  5. alita_sdk/cli/agent_executor.py +156 -0
  6. alita_sdk/cli/agent_loader.py +245 -0
  7. alita_sdk/cli/agent_ui.py +228 -0
  8. alita_sdk/cli/agents.py +3113 -0
  9. alita_sdk/cli/callbacks.py +647 -0
  10. alita_sdk/cli/cli.py +168 -0
  11. alita_sdk/cli/config.py +306 -0
  12. alita_sdk/cli/context/__init__.py +30 -0
  13. alita_sdk/cli/context/cleanup.py +198 -0
  14. alita_sdk/cli/context/manager.py +731 -0
  15. alita_sdk/cli/context/message.py +285 -0
  16. alita_sdk/cli/context/strategies.py +289 -0
  17. alita_sdk/cli/context/token_estimation.py +127 -0
  18. alita_sdk/cli/formatting.py +182 -0
  19. alita_sdk/cli/input_handler.py +419 -0
  20. alita_sdk/cli/inventory.py +1073 -0
  21. alita_sdk/cli/mcp_loader.py +315 -0
  22. alita_sdk/cli/testcases/__init__.py +94 -0
  23. alita_sdk/cli/testcases/data_generation.py +119 -0
  24. alita_sdk/cli/testcases/discovery.py +96 -0
  25. alita_sdk/cli/testcases/executor.py +84 -0
  26. alita_sdk/cli/testcases/logger.py +85 -0
  27. alita_sdk/cli/testcases/parser.py +172 -0
  28. alita_sdk/cli/testcases/prompts.py +91 -0
  29. alita_sdk/cli/testcases/reporting.py +125 -0
  30. alita_sdk/cli/testcases/setup.py +108 -0
  31. alita_sdk/cli/testcases/test_runner.py +282 -0
  32. alita_sdk/cli/testcases/utils.py +39 -0
  33. alita_sdk/cli/testcases/validation.py +90 -0
  34. alita_sdk/cli/testcases/workflow.py +196 -0
  35. alita_sdk/cli/toolkit.py +327 -0
  36. alita_sdk/cli/toolkit_loader.py +85 -0
  37. alita_sdk/cli/tools/__init__.py +43 -0
  38. alita_sdk/cli/tools/approval.py +224 -0
  39. alita_sdk/cli/tools/filesystem.py +1751 -0
  40. alita_sdk/cli/tools/planning.py +389 -0
  41. alita_sdk/cli/tools/terminal.py +414 -0
  42. alita_sdk/community/__init__.py +72 -12
  43. alita_sdk/community/inventory/__init__.py +236 -0
  44. alita_sdk/community/inventory/config.py +257 -0
  45. alita_sdk/community/inventory/enrichment.py +2137 -0
  46. alita_sdk/community/inventory/extractors.py +1469 -0
  47. alita_sdk/community/inventory/ingestion.py +3172 -0
  48. alita_sdk/community/inventory/knowledge_graph.py +1457 -0
  49. alita_sdk/community/inventory/parsers/__init__.py +218 -0
  50. alita_sdk/community/inventory/parsers/base.py +295 -0
  51. alita_sdk/community/inventory/parsers/csharp_parser.py +907 -0
  52. alita_sdk/community/inventory/parsers/go_parser.py +851 -0
  53. alita_sdk/community/inventory/parsers/html_parser.py +389 -0
  54. alita_sdk/community/inventory/parsers/java_parser.py +593 -0
  55. alita_sdk/community/inventory/parsers/javascript_parser.py +629 -0
  56. alita_sdk/community/inventory/parsers/kotlin_parser.py +768 -0
  57. alita_sdk/community/inventory/parsers/markdown_parser.py +362 -0
  58. alita_sdk/community/inventory/parsers/python_parser.py +604 -0
  59. alita_sdk/community/inventory/parsers/rust_parser.py +858 -0
  60. alita_sdk/community/inventory/parsers/swift_parser.py +832 -0
  61. alita_sdk/community/inventory/parsers/text_parser.py +322 -0
  62. alita_sdk/community/inventory/parsers/yaml_parser.py +370 -0
  63. alita_sdk/community/inventory/patterns/__init__.py +61 -0
  64. alita_sdk/community/inventory/patterns/ast_adapter.py +380 -0
  65. alita_sdk/community/inventory/patterns/loader.py +348 -0
  66. alita_sdk/community/inventory/patterns/registry.py +198 -0
  67. alita_sdk/community/inventory/presets.py +535 -0
  68. alita_sdk/community/inventory/retrieval.py +1403 -0
  69. alita_sdk/community/inventory/toolkit.py +173 -0
  70. alita_sdk/community/inventory/toolkit_utils.py +176 -0
  71. alita_sdk/community/inventory/visualize.py +1370 -0
  72. alita_sdk/configurations/__init__.py +1 -1
  73. alita_sdk/configurations/ado.py +141 -20
  74. alita_sdk/configurations/bitbucket.py +94 -2
  75. alita_sdk/configurations/confluence.py +130 -1
  76. alita_sdk/configurations/figma.py +76 -0
  77. alita_sdk/configurations/gitlab.py +91 -0
  78. alita_sdk/configurations/jira.py +103 -0
  79. alita_sdk/configurations/openapi.py +329 -0
  80. alita_sdk/configurations/qtest.py +72 -1
  81. alita_sdk/configurations/report_portal.py +96 -0
  82. alita_sdk/configurations/sharepoint.py +148 -0
  83. alita_sdk/configurations/testio.py +83 -0
  84. alita_sdk/configurations/testrail.py +88 -0
  85. alita_sdk/configurations/xray.py +93 -0
  86. alita_sdk/configurations/zephyr_enterprise.py +93 -0
  87. alita_sdk/configurations/zephyr_essential.py +75 -0
  88. alita_sdk/runtime/clients/artifact.py +3 -3
  89. alita_sdk/runtime/clients/client.py +388 -46
  90. alita_sdk/runtime/clients/mcp_discovery.py +342 -0
  91. alita_sdk/runtime/clients/mcp_manager.py +262 -0
  92. alita_sdk/runtime/clients/sandbox_client.py +8 -21
  93. alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
  94. alita_sdk/runtime/langchain/assistant.py +157 -39
  95. alita_sdk/runtime/langchain/constants.py +647 -1
  96. alita_sdk/runtime/langchain/document_loaders/AlitaDocxMammothLoader.py +315 -3
  97. alita_sdk/runtime/langchain/document_loaders/AlitaExcelLoader.py +103 -60
  98. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLinesLoader.py +77 -0
  99. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +10 -4
  100. alita_sdk/runtime/langchain/document_loaders/AlitaPowerPointLoader.py +226 -7
  101. alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +5 -2
  102. alita_sdk/runtime/langchain/document_loaders/constants.py +40 -19
  103. alita_sdk/runtime/langchain/langraph_agent.py +405 -84
  104. alita_sdk/runtime/langchain/utils.py +106 -7
  105. alita_sdk/runtime/llms/preloaded.py +2 -6
  106. alita_sdk/runtime/models/mcp_models.py +61 -0
  107. alita_sdk/runtime/skills/__init__.py +91 -0
  108. alita_sdk/runtime/skills/callbacks.py +498 -0
  109. alita_sdk/runtime/skills/discovery.py +540 -0
  110. alita_sdk/runtime/skills/executor.py +610 -0
  111. alita_sdk/runtime/skills/input_builder.py +371 -0
  112. alita_sdk/runtime/skills/models.py +330 -0
  113. alita_sdk/runtime/skills/registry.py +355 -0
  114. alita_sdk/runtime/skills/skill_runner.py +330 -0
  115. alita_sdk/runtime/toolkits/__init__.py +31 -0
  116. alita_sdk/runtime/toolkits/application.py +29 -10
  117. alita_sdk/runtime/toolkits/artifact.py +20 -11
  118. alita_sdk/runtime/toolkits/datasource.py +13 -6
  119. alita_sdk/runtime/toolkits/mcp.py +783 -0
  120. alita_sdk/runtime/toolkits/mcp_config.py +1048 -0
  121. alita_sdk/runtime/toolkits/planning.py +178 -0
  122. alita_sdk/runtime/toolkits/skill_router.py +238 -0
  123. alita_sdk/runtime/toolkits/subgraph.py +251 -6
  124. alita_sdk/runtime/toolkits/tools.py +356 -69
  125. alita_sdk/runtime/toolkits/vectorstore.py +11 -5
  126. alita_sdk/runtime/tools/__init__.py +10 -3
  127. alita_sdk/runtime/tools/application.py +27 -6
  128. alita_sdk/runtime/tools/artifact.py +511 -28
  129. alita_sdk/runtime/tools/data_analysis.py +183 -0
  130. alita_sdk/runtime/tools/function.py +67 -35
  131. alita_sdk/runtime/tools/graph.py +10 -4
  132. alita_sdk/runtime/tools/image_generation.py +148 -46
  133. alita_sdk/runtime/tools/llm.py +1003 -128
  134. alita_sdk/runtime/tools/loop.py +3 -1
  135. alita_sdk/runtime/tools/loop_output.py +3 -1
  136. alita_sdk/runtime/tools/mcp_inspect_tool.py +284 -0
  137. alita_sdk/runtime/tools/mcp_remote_tool.py +181 -0
  138. alita_sdk/runtime/tools/mcp_server_tool.py +8 -5
  139. alita_sdk/runtime/tools/planning/__init__.py +36 -0
  140. alita_sdk/runtime/tools/planning/models.py +246 -0
  141. alita_sdk/runtime/tools/planning/wrapper.py +607 -0
  142. alita_sdk/runtime/tools/router.py +2 -4
  143. alita_sdk/runtime/tools/sandbox.py +65 -48
  144. alita_sdk/runtime/tools/skill_router.py +776 -0
  145. alita_sdk/runtime/tools/tool.py +3 -1
  146. alita_sdk/runtime/tools/vectorstore.py +9 -3
  147. alita_sdk/runtime/tools/vectorstore_base.py +70 -14
  148. alita_sdk/runtime/utils/AlitaCallback.py +137 -21
  149. alita_sdk/runtime/utils/constants.py +5 -1
  150. alita_sdk/runtime/utils/mcp_client.py +492 -0
  151. alita_sdk/runtime/utils/mcp_oauth.py +361 -0
  152. alita_sdk/runtime/utils/mcp_sse_client.py +434 -0
  153. alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
  154. alita_sdk/runtime/utils/serialization.py +155 -0
  155. alita_sdk/runtime/utils/streamlit.py +40 -13
  156. alita_sdk/runtime/utils/toolkit_utils.py +30 -9
  157. alita_sdk/runtime/utils/utils.py +36 -0
  158. alita_sdk/tools/__init__.py +134 -35
  159. alita_sdk/tools/ado/repos/__init__.py +51 -32
  160. alita_sdk/tools/ado/repos/repos_wrapper.py +148 -89
  161. alita_sdk/tools/ado/test_plan/__init__.py +25 -9
  162. alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +23 -1
  163. alita_sdk/tools/ado/utils.py +1 -18
  164. alita_sdk/tools/ado/wiki/__init__.py +25 -12
  165. alita_sdk/tools/ado/wiki/ado_wrapper.py +291 -22
  166. alita_sdk/tools/ado/work_item/__init__.py +26 -13
  167. alita_sdk/tools/ado/work_item/ado_wrapper.py +73 -11
  168. alita_sdk/tools/advanced_jira_mining/__init__.py +11 -8
  169. alita_sdk/tools/aws/delta_lake/__init__.py +13 -9
  170. alita_sdk/tools/aws/delta_lake/tool.py +5 -1
  171. alita_sdk/tools/azure_ai/search/__init__.py +11 -8
  172. alita_sdk/tools/azure_ai/search/api_wrapper.py +1 -1
  173. alita_sdk/tools/base/tool.py +5 -1
  174. alita_sdk/tools/base_indexer_toolkit.py +271 -84
  175. alita_sdk/tools/bitbucket/__init__.py +17 -11
  176. alita_sdk/tools/bitbucket/api_wrapper.py +59 -11
  177. alita_sdk/tools/bitbucket/cloud_api_wrapper.py +49 -35
  178. alita_sdk/tools/browser/__init__.py +5 -4
  179. alita_sdk/tools/carrier/__init__.py +5 -6
  180. alita_sdk/tools/carrier/backend_reports_tool.py +6 -6
  181. alita_sdk/tools/carrier/run_ui_test_tool.py +6 -6
  182. alita_sdk/tools/carrier/ui_reports_tool.py +5 -5
  183. alita_sdk/tools/chunkers/__init__.py +3 -1
  184. alita_sdk/tools/chunkers/code/treesitter/treesitter.py +37 -13
  185. alita_sdk/tools/chunkers/sematic/json_chunker.py +1 -0
  186. alita_sdk/tools/chunkers/sematic/markdown_chunker.py +97 -6
  187. alita_sdk/tools/chunkers/sematic/proposal_chunker.py +1 -1
  188. alita_sdk/tools/chunkers/universal_chunker.py +270 -0
  189. alita_sdk/tools/cloud/aws/__init__.py +10 -7
  190. alita_sdk/tools/cloud/azure/__init__.py +10 -7
  191. alita_sdk/tools/cloud/gcp/__init__.py +10 -7
  192. alita_sdk/tools/cloud/k8s/__init__.py +10 -7
  193. alita_sdk/tools/code/linter/__init__.py +10 -8
  194. alita_sdk/tools/code/loaders/codesearcher.py +3 -2
  195. alita_sdk/tools/code/sonar/__init__.py +11 -8
  196. alita_sdk/tools/code_indexer_toolkit.py +82 -22
  197. alita_sdk/tools/confluence/__init__.py +22 -16
  198. alita_sdk/tools/confluence/api_wrapper.py +107 -30
  199. alita_sdk/tools/confluence/loader.py +14 -2
  200. alita_sdk/tools/custom_open_api/__init__.py +12 -5
  201. alita_sdk/tools/elastic/__init__.py +11 -8
  202. alita_sdk/tools/elitea_base.py +493 -30
  203. alita_sdk/tools/figma/__init__.py +58 -11
  204. alita_sdk/tools/figma/api_wrapper.py +1235 -143
  205. alita_sdk/tools/figma/figma_client.py +73 -0
  206. alita_sdk/tools/figma/toon_tools.py +2748 -0
  207. alita_sdk/tools/github/__init__.py +14 -15
  208. alita_sdk/tools/github/github_client.py +224 -100
  209. alita_sdk/tools/github/graphql_client_wrapper.py +119 -33
  210. alita_sdk/tools/github/schemas.py +14 -5
  211. alita_sdk/tools/github/tool.py +5 -1
  212. alita_sdk/tools/github/tool_prompts.py +9 -22
  213. alita_sdk/tools/gitlab/__init__.py +16 -11
  214. alita_sdk/tools/gitlab/api_wrapper.py +218 -48
  215. alita_sdk/tools/gitlab_org/__init__.py +10 -9
  216. alita_sdk/tools/gitlab_org/api_wrapper.py +63 -64
  217. alita_sdk/tools/google/bigquery/__init__.py +13 -12
  218. alita_sdk/tools/google/bigquery/tool.py +5 -1
  219. alita_sdk/tools/google_places/__init__.py +11 -8
  220. alita_sdk/tools/google_places/api_wrapper.py +1 -1
  221. alita_sdk/tools/jira/__init__.py +17 -10
  222. alita_sdk/tools/jira/api_wrapper.py +92 -41
  223. alita_sdk/tools/keycloak/__init__.py +11 -8
  224. alita_sdk/tools/localgit/__init__.py +9 -3
  225. alita_sdk/tools/localgit/local_git.py +62 -54
  226. alita_sdk/tools/localgit/tool.py +5 -1
  227. alita_sdk/tools/memory/__init__.py +12 -4
  228. alita_sdk/tools/non_code_indexer_toolkit.py +1 -0
  229. alita_sdk/tools/ocr/__init__.py +11 -8
  230. alita_sdk/tools/openapi/__init__.py +491 -106
  231. alita_sdk/tools/openapi/api_wrapper.py +1368 -0
  232. alita_sdk/tools/openapi/tool.py +20 -0
  233. alita_sdk/tools/pandas/__init__.py +20 -12
  234. alita_sdk/tools/pandas/api_wrapper.py +38 -25
  235. alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
  236. alita_sdk/tools/postman/__init__.py +10 -9
  237. alita_sdk/tools/pptx/__init__.py +11 -10
  238. alita_sdk/tools/pptx/pptx_wrapper.py +1 -1
  239. alita_sdk/tools/qtest/__init__.py +31 -11
  240. alita_sdk/tools/qtest/api_wrapper.py +2135 -86
  241. alita_sdk/tools/rally/__init__.py +10 -9
  242. alita_sdk/tools/rally/api_wrapper.py +1 -1
  243. alita_sdk/tools/report_portal/__init__.py +12 -8
  244. alita_sdk/tools/salesforce/__init__.py +10 -8
  245. alita_sdk/tools/servicenow/__init__.py +17 -15
  246. alita_sdk/tools/servicenow/api_wrapper.py +1 -1
  247. alita_sdk/tools/sharepoint/__init__.py +10 -7
  248. alita_sdk/tools/sharepoint/api_wrapper.py +129 -38
  249. alita_sdk/tools/sharepoint/authorization_helper.py +191 -1
  250. alita_sdk/tools/sharepoint/utils.py +8 -2
  251. alita_sdk/tools/slack/__init__.py +10 -7
  252. alita_sdk/tools/slack/api_wrapper.py +2 -2
  253. alita_sdk/tools/sql/__init__.py +12 -9
  254. alita_sdk/tools/testio/__init__.py +10 -7
  255. alita_sdk/tools/testrail/__init__.py +11 -10
  256. alita_sdk/tools/testrail/api_wrapper.py +1 -1
  257. alita_sdk/tools/utils/__init__.py +9 -4
  258. alita_sdk/tools/utils/content_parser.py +103 -18
  259. alita_sdk/tools/utils/text_operations.py +410 -0
  260. alita_sdk/tools/utils/tool_prompts.py +79 -0
  261. alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +30 -13
  262. alita_sdk/tools/xray/__init__.py +13 -9
  263. alita_sdk/tools/yagmail/__init__.py +9 -3
  264. alita_sdk/tools/zephyr/__init__.py +10 -7
  265. alita_sdk/tools/zephyr_enterprise/__init__.py +11 -7
  266. alita_sdk/tools/zephyr_essential/__init__.py +10 -7
  267. alita_sdk/tools/zephyr_essential/api_wrapper.py +30 -13
  268. alita_sdk/tools/zephyr_essential/client.py +2 -2
  269. alita_sdk/tools/zephyr_scale/__init__.py +11 -8
  270. alita_sdk/tools/zephyr_scale/api_wrapper.py +2 -2
  271. alita_sdk/tools/zephyr_squad/__init__.py +10 -7
  272. {alita_sdk-0.3.379.dist-info → alita_sdk-0.3.627.dist-info}/METADATA +154 -8
  273. alita_sdk-0.3.627.dist-info/RECORD +468 -0
  274. alita_sdk-0.3.627.dist-info/entry_points.txt +2 -0
  275. alita_sdk-0.3.379.dist-info/RECORD +0 -360
  276. {alita_sdk-0.3.379.dist-info → alita_sdk-0.3.627.dist-info}/WHEEL +0 -0
  277. {alita_sdk-0.3.379.dist-info → alita_sdk-0.3.627.dist-info}/licenses/LICENSE +0 -0
  278. {alita_sdk-0.3.379.dist-info → alita_sdk-0.3.627.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,540 @@
1
+ """
2
+ Skill discovery service for finding and validating skills from filesystem.
3
+
4
+ This module handles the discovery of skill definitions from configurable
5
+ directories, parsing agent.md files, and creating validated SkillMetadata objects.
6
+ """
7
+
8
+ import logging
9
+ import os
10
+ import re
11
+ import yaml
12
+ from pathlib import Path
13
+ from typing import Dict, List, Optional, Tuple
14
+
15
+ from .models import (
16
+ SkillMetadata, SkillType, ExecutionConfig, ResultsConfig,
17
+ SkillInputSchema, SkillOutputSchema, SkillValidationError
18
+ )
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class SkillDiscovery:
24
+ """Service for discovering and validating skills from filesystem."""
25
+
26
+ def __init__(self, search_paths: Optional[List[str]] = None):
27
+ """
28
+ Initialize skill discovery service.
29
+
30
+ Args:
31
+ search_paths: Custom search paths. If None, uses default paths.
32
+ """
33
+ self.search_paths = search_paths or self._get_default_search_paths()
34
+ self.cache: Dict[str, SkillMetadata] = {}
35
+ logger.info(f"Initialized skill discovery with paths: {self.search_paths}")
36
+
37
+ @staticmethod
38
+ def _get_default_search_paths() -> List[str]:
39
+ """Get default search paths for skills."""
40
+ return [
41
+ ".alita/agents/skills",
42
+ "./skills",
43
+ os.path.expanduser("~/.alita/skills")
44
+ ]
45
+
46
+ def discover(self, refresh: bool = False) -> Dict[str, SkillMetadata]:
47
+ """
48
+ Discover skills from configured search paths.
49
+
50
+ Args:
51
+ refresh: If True, clear cache and rescan all directories.
52
+
53
+ Returns:
54
+ Dictionary mapping skill names to SkillMetadata objects.
55
+
56
+ Raises:
57
+ Exception: If discovery fails critically.
58
+ """
59
+ if not refresh and self.cache:
60
+ logger.debug(f"Returning cached skills: {len(self.cache)} found")
61
+ return self.cache
62
+
63
+ if refresh:
64
+ logger.info("Refreshing skill discovery cache")
65
+ self.cache.clear()
66
+
67
+ discovered_skills = {}
68
+
69
+ for search_path in self.search_paths:
70
+ try:
71
+ path_skills = self._discover_in_path(search_path)
72
+ logger.info(f"Found {len(path_skills)} skills in {search_path}")
73
+
74
+ # Handle name collisions
75
+ for name, skill in path_skills.items():
76
+ if name in discovered_skills:
77
+ logger.warning(
78
+ f"Skill name collision: '{name}' found in both "
79
+ f"{discovered_skills[name].path} and {skill.path}. "
80
+ f"Using the latter."
81
+ )
82
+ discovered_skills[name] = skill
83
+
84
+ except Exception as e:
85
+ logger.error(f"Error discovering skills in {search_path}: {e}")
86
+ # Continue with other paths rather than failing completely
87
+
88
+ logger.info(f"Discovery complete: {len(discovered_skills)} skills found")
89
+ self.cache = discovered_skills
90
+ return discovered_skills
91
+
92
+ def _discover_in_path(self, search_path: str) -> Dict[str, SkillMetadata]:
93
+ """
94
+ Discover skills in a specific path.
95
+
96
+ Args:
97
+ search_path: Path to search for skills.
98
+
99
+ Returns:
100
+ Dictionary of discovered skills.
101
+ """
102
+ path = Path(search_path).expanduser().resolve()
103
+
104
+ if not path.exists():
105
+ logger.debug(f"Search path does not exist: {path}")
106
+ return {}
107
+
108
+ if not path.is_dir():
109
+ logger.warning(f"Search path is not a directory: {path}")
110
+ return {}
111
+
112
+ skills = {}
113
+
114
+ # Recursively find all agent.md files, including through symlinks
115
+ for agent_file in self._find_agent_files(path):
116
+ try:
117
+ skill_metadata = self._parse_skill_file(agent_file)
118
+ if skill_metadata:
119
+ skills[skill_metadata.name] = skill_metadata
120
+ logger.debug(f"Discovered skill: {skill_metadata.name} at {agent_file.parent}")
121
+ except Exception as e:
122
+ logger.error(f"Error parsing skill file {agent_file}: {e}")
123
+ continue
124
+
125
+ return skills
126
+
127
+ def _find_agent_files(self, path: Path) -> list[Path]:
128
+ """
129
+ Find all agent.md files, including through symlinks.
130
+
131
+ Python's rglob doesn't follow symlinks by default, so we need
132
+ to handle them manually for skills discovery.
133
+
134
+ Args:
135
+ path: Root path to search from.
136
+
137
+ Returns:
138
+ List of agent.md file paths found.
139
+ """
140
+ agent_files = []
141
+
142
+ def _walk_directory(current_path: Path, visited: set = None):
143
+ if visited is None:
144
+ visited = set()
145
+
146
+ # Avoid infinite loops from circular symlinks
147
+ resolved_path = current_path.resolve()
148
+ if resolved_path in visited:
149
+ return
150
+ visited.add(resolved_path)
151
+
152
+ try:
153
+ if not current_path.exists() or not current_path.is_dir():
154
+ return
155
+
156
+ for item in current_path.iterdir():
157
+ if item.name == "agent.md" and item.is_file():
158
+ agent_files.append(item)
159
+ elif item.is_dir():
160
+ # Recurse into subdirectories (including symlinked ones)
161
+ _walk_directory(item, visited.copy())
162
+
163
+ except (PermissionError, OSError) as e:
164
+ logger.debug(f"Cannot access {current_path}: {e}")
165
+
166
+ _walk_directory(path)
167
+ logger.debug(f"Found {len(agent_files)} agent.md files in {path}")
168
+ return agent_files
169
+
170
+ def _parse_skill_file(self, agent_file: Path) -> Optional[SkillMetadata]:
171
+ """
172
+ Parse an agent.md file and create SkillMetadata.
173
+
174
+ Args:
175
+ agent_file: Path to agent.md file.
176
+
177
+ Returns:
178
+ SkillMetadata object if valid, None otherwise.
179
+
180
+ Raises:
181
+ SkillValidationError: If skill definition is invalid.
182
+ """
183
+ logger.debug(f"Parsing skill file: {agent_file}")
184
+
185
+ try:
186
+ content = agent_file.read_text(encoding='utf-8')
187
+ logger.debug(f"Read {len(content)} characters from {agent_file}")
188
+ except Exception as e:
189
+ logger.error(f"Cannot read file {agent_file}: {e}")
190
+ raise SkillValidationError(f"Cannot read file {agent_file}: {e}")
191
+
192
+ # Extract YAML frontmatter
193
+ frontmatter, body = self._extract_frontmatter(content)
194
+
195
+ if not frontmatter:
196
+ logger.debug(f"No frontmatter found in {agent_file}")
197
+ return None
198
+
199
+ logger.debug(f"Extracted frontmatter from {agent_file}: {list(frontmatter.keys())}")
200
+
201
+ # Validate agent_type
202
+ agent_type = frontmatter.get('agent_type')
203
+ if agent_type != 'skill':
204
+ logger.debug(f"File {agent_file} is not a skill (agent_type: {agent_type})")
205
+ return None
206
+
207
+ logger.debug(f"Agent type validation passed for {agent_file}")
208
+
209
+ # Validate and create metadata
210
+ try:
211
+ metadata = self._create_skill_metadata(frontmatter, body, agent_file.parent)
212
+ logger.debug(f"Successfully created metadata for skill: {metadata.name}")
213
+ return metadata
214
+ except Exception as e:
215
+ logger.error(f"Invalid skill definition in {agent_file}: {e}")
216
+ raise SkillValidationError(f"Invalid skill definition in {agent_file}: {e}")
217
+
218
+ def _extract_frontmatter(self, content: str) -> Tuple[Optional[Dict], str]:
219
+ """
220
+ Extract YAML frontmatter from markdown content.
221
+
222
+ Args:
223
+ content: Full file content.
224
+
225
+ Returns:
226
+ Tuple of (frontmatter_dict, remaining_body).
227
+ """
228
+ # Match YAML frontmatter pattern
229
+ pattern = r'^---\s*\n(.*?)\n---\s*\n(.*)'
230
+ match = re.match(pattern, content, re.DOTALL)
231
+
232
+ if not match:
233
+ return None, content
234
+
235
+ try:
236
+ frontmatter = yaml.safe_load(match.group(1))
237
+ body = match.group(2).strip()
238
+ return frontmatter, body
239
+ except yaml.YAMLError as e:
240
+ logger.error(f"Invalid YAML frontmatter: {e}")
241
+ return None, content
242
+
243
+ def _create_skill_metadata(
244
+ self,
245
+ frontmatter: Dict,
246
+ body: str,
247
+ skill_path: Path
248
+ ) -> SkillMetadata:
249
+ """
250
+ Create SkillMetadata from parsed frontmatter.
251
+
252
+ Args:
253
+ frontmatter: Parsed YAML frontmatter.
254
+ body: Markdown body content.
255
+ skill_path: Path to skill directory.
256
+
257
+ Returns:
258
+ Validated SkillMetadata object.
259
+
260
+ Raises:
261
+ SkillValidationError: If validation fails.
262
+ """
263
+ logger.debug(f"Creating skill metadata from frontmatter keys: {list(frontmatter.keys())}")
264
+
265
+ # Validate required fields
266
+ name = frontmatter.get('name')
267
+ if not name:
268
+ raise SkillValidationError("Missing required field: name")
269
+
270
+ logger.debug(f"Skill name: {name}")
271
+
272
+ description = frontmatter.get('description', '')
273
+ skill_type_str = frontmatter.get('skill_type', 'agent')
274
+
275
+ logger.debug(f"Skill type string: {skill_type_str}")
276
+
277
+ try:
278
+ skill_type = SkillType(skill_type_str)
279
+ logger.debug(f"Parsed skill type: {skill_type}")
280
+ except ValueError:
281
+ raise SkillValidationError(f"Invalid skill_type: {skill_type_str}. Must be 'graph' or 'agent'")
282
+
283
+ # Parse execution configuration
284
+ logger.debug(f"Parsing execution config: {frontmatter.get('execution', {})}")
285
+ try:
286
+ execution_config = self._parse_execution_config(frontmatter.get('execution', {}))
287
+ logger.debug(f"Created execution config: {execution_config}")
288
+ except Exception as e:
289
+ logger.error(f"Failed to parse execution config: {e}")
290
+ raise SkillValidationError(f"Invalid execution configuration: {e}")
291
+
292
+ # Parse results configuration
293
+ logger.debug(f"Parsing results config: {frontmatter.get('results', {})}")
294
+ try:
295
+ results_config = self._parse_results_config(frontmatter.get('results', {}))
296
+ logger.debug(f"Created results config: {results_config}")
297
+ except Exception as e:
298
+ logger.error(f"Failed to parse results config: {e}")
299
+ raise SkillValidationError(f"Invalid results configuration: {e}")
300
+
301
+ # Parse input/output schemas
302
+ logger.debug(f"Parsing input schema: {frontmatter.get('inputs', {})}")
303
+ try:
304
+ inputs = self._parse_input_schema(frontmatter.get('inputs', {}), skill_type)
305
+ logger.debug(f"Created input schema: {inputs}")
306
+ except Exception as e:
307
+ logger.error(f"Failed to parse input schema: {e}")
308
+ raise SkillValidationError(f"Invalid input schema: {e}")
309
+
310
+ logger.debug(f"Parsing output schema: {frontmatter.get('outputs', {})}")
311
+ try:
312
+ outputs = self._parse_output_schema(frontmatter.get('outputs', {}))
313
+ logger.debug(f"Created output schema: {outputs}")
314
+ except Exception as e:
315
+ logger.error(f"Failed to parse output schema: {e}")
316
+ raise SkillValidationError(f"Invalid output schema: {e}")
317
+
318
+ # Create base metadata
319
+ logger.debug("Creating SkillMetadata object...")
320
+ try:
321
+ from .models import SkillSource
322
+ metadata = SkillMetadata(
323
+ name=name,
324
+ skill_type=skill_type,
325
+ source=SkillSource.FILESYSTEM,
326
+ path=str(skill_path),
327
+ description=description,
328
+ capabilities=frontmatter.get('capabilities', []),
329
+ tags=frontmatter.get('tags', []),
330
+ version=frontmatter.get('version', '1.0.0'),
331
+ execution=execution_config,
332
+ results=results_config,
333
+ inputs=inputs,
334
+ outputs=outputs,
335
+ model=frontmatter.get('model'),
336
+ temperature=frontmatter.get('temperature'),
337
+ max_tokens=frontmatter.get('max_tokens')
338
+ )
339
+ logger.debug("Base metadata created successfully")
340
+ except Exception as e:
341
+ logger.error(f"Failed to create base metadata: {e}")
342
+ raise SkillValidationError(f"Failed to create skill metadata: {e}")
343
+
344
+ # Add type-specific fields
345
+ try:
346
+ if skill_type == SkillType.GRAPH:
347
+ metadata.state_schema = frontmatter.get('state', {})
348
+ metadata.nodes = frontmatter.get('nodes', [])
349
+ # Store complete YAML for graph reconstruction
350
+ metadata.graph_yaml = self._build_graph_yaml(frontmatter)
351
+ logger.debug("Added graph-specific fields")
352
+ else: # AGENT
353
+ metadata.system_prompt = body if body else frontmatter.get('system_prompt')
354
+ metadata.agent_type = frontmatter.get('agent_subtype', 'react') # Use different field name
355
+ metadata.toolkits = frontmatter.get('toolkits', [])
356
+ logger.debug("Added agent-specific fields")
357
+
358
+ # Validate type-specific requirements
359
+ self._validate_skill_type_requirements(metadata)
360
+
361
+ logger.debug(f"Successfully created skill metadata: {name} ({skill_type.value})")
362
+ return metadata
363
+
364
+ except Exception as e:
365
+ logger.error(f"Failed to add type-specific fields: {e}")
366
+ raise SkillValidationError(f"Failed to complete skill metadata: {e}")
367
+
368
+ def _parse_execution_config(self, execution_data: Dict) -> ExecutionConfig:
369
+ """Parse execution configuration from frontmatter."""
370
+ return ExecutionConfig(
371
+ mode=execution_data.get('mode', 'subprocess'),
372
+ timeout=execution_data.get('timeout', 300),
373
+ working_directory=execution_data.get('working_directory'),
374
+ environment=execution_data.get('environment', {}),
375
+ max_retries=execution_data.get('max_retries', 0)
376
+ )
377
+
378
+ def _parse_results_config(self, results_data: Dict) -> ResultsConfig:
379
+ """Parse results configuration from frontmatter."""
380
+ return ResultsConfig(
381
+ format=results_data.get('format', 'text_with_links'),
382
+ output_files=results_data.get('output_files', []),
383
+ cleanup_policy=results_data.get('cleanup_policy', 'preserve')
384
+ )
385
+
386
+ def _parse_input_schema(self, inputs_data: Dict, skill_type: SkillType) -> SkillInputSchema:
387
+ """Parse input schema based on skill type."""
388
+ schema = SkillInputSchema()
389
+
390
+ if skill_type == SkillType.AGENT:
391
+ schema.variables = inputs_data.get('variables', {})
392
+ schema.chat_history = inputs_data.get('chat_history')
393
+ schema.user_input = inputs_data.get('user_input')
394
+ else: # GRAPH
395
+ schema.state_variables = inputs_data.get('state_variables', {})
396
+
397
+ return schema
398
+
399
+ def _parse_output_schema(self, outputs_data: Dict) -> SkillOutputSchema:
400
+ """Parse output schema from frontmatter."""
401
+ return SkillOutputSchema(
402
+ primary_output=outputs_data.get('primary_output', {
403
+ "type": "text",
404
+ "description": "Main result text"
405
+ }),
406
+ generated_files=outputs_data.get('generated_files', {
407
+ "type": "list[file_reference]",
408
+ "description": "Created files"
409
+ }),
410
+ additional_outputs=outputs_data.get('additional_outputs')
411
+ )
412
+
413
+ def _build_graph_yaml(self, frontmatter: Dict) -> str:
414
+ """Build complete YAML for graph skills."""
415
+ graph_definition = {
416
+ 'name': frontmatter.get('name'),
417
+ 'description': frontmatter.get('description'),
418
+ 'state': frontmatter.get('state', {}),
419
+ 'nodes': frontmatter.get('nodes', []),
420
+ 'entry_point': frontmatter.get('entry_point'),
421
+ 'interrupt_before': frontmatter.get('interrupt_before', []),
422
+ 'interrupt_after': frontmatter.get('interrupt_after', [])
423
+ }
424
+
425
+ # Remove None values
426
+ graph_definition = {k: v for k, v in graph_definition.items() if v is not None}
427
+
428
+ return yaml.dump(graph_definition, default_flow_style=False)
429
+
430
+ def _validate_skill_type_requirements(self, metadata: SkillMetadata) -> None:
431
+ """
432
+ Validate type-specific requirements for skills.
433
+
434
+ Args:
435
+ metadata: SkillMetadata to validate.
436
+
437
+ Raises:
438
+ SkillValidationError: If validation fails.
439
+ """
440
+ if metadata.skill_type == SkillType.GRAPH:
441
+ if not metadata.state_schema:
442
+ logger.warning(f"Graph skill {metadata.name} has no state schema")
443
+ if not metadata.nodes:
444
+ raise SkillValidationError(f"Graph skill {metadata.name} must have nodes defined")
445
+
446
+ elif metadata.skill_type == SkillType.AGENT:
447
+ if not metadata.system_prompt:
448
+ logger.warning(f"Agent skill {metadata.name} has no system prompt")
449
+ if not metadata.toolkits:
450
+ logger.warning(f"Agent skill {metadata.name} has no toolkits defined")
451
+
452
+ def get_skill_by_name(self, name: str) -> Optional[SkillMetadata]:
453
+ """
454
+ Get a specific skill by name.
455
+
456
+ Args:
457
+ name: Name of the skill to retrieve.
458
+
459
+ Returns:
460
+ SkillMetadata if found, None otherwise.
461
+ """
462
+ if not self.cache:
463
+ self.discover()
464
+
465
+ return self.cache.get(name)
466
+
467
+ def find_skills_by_capability(self, capability: str) -> List[SkillMetadata]:
468
+ """
469
+ Find skills that provide a specific capability.
470
+
471
+ Args:
472
+ capability: Capability to search for.
473
+
474
+ Returns:
475
+ List of matching SkillMetadata objects.
476
+ """
477
+ if not self.cache:
478
+ self.discover()
479
+
480
+ return [
481
+ skill for skill in self.cache.values()
482
+ if capability in skill.capabilities
483
+ ]
484
+
485
+ def find_skills_by_tag(self, tag: str) -> List[SkillMetadata]:
486
+ """
487
+ Find skills with a specific tag.
488
+
489
+ Args:
490
+ tag: Tag to search for.
491
+
492
+ Returns:
493
+ List of matching SkillMetadata objects.
494
+ """
495
+ if not self.cache:
496
+ self.discover()
497
+
498
+ return [
499
+ skill for skill in self.cache.values()
500
+ if tag in skill.tags
501
+ ]
502
+
503
+ def find_skills_by_type(self, skill_type: SkillType) -> List[SkillMetadata]:
504
+ """
505
+ Find skills of a specific type.
506
+
507
+ Args:
508
+ skill_type: SkillType to filter by.
509
+
510
+ Returns:
511
+ List of matching SkillMetadata objects.
512
+ """
513
+ if not self.cache:
514
+ self.discover()
515
+
516
+ return [
517
+ skill for skill in self.cache.values()
518
+ if skill.skill_type == skill_type
519
+ ]
520
+
521
+ def validate_skill_definition(self, skill_path: Path) -> Tuple[bool, Optional[str]]:
522
+ """
523
+ Validate a skill definition without adding it to cache.
524
+
525
+ Args:
526
+ skill_path: Path to skill directory containing agent.md.
527
+
528
+ Returns:
529
+ Tuple of (is_valid, error_message).
530
+ """
531
+ agent_file = skill_path / "agent.md"
532
+
533
+ if not agent_file.exists():
534
+ return False, f"agent.md not found in {skill_path}"
535
+
536
+ try:
537
+ self._parse_skill_file(agent_file)
538
+ return True, None
539
+ except Exception as e:
540
+ return False, str(e)