hexdag 0.5.0.dev1__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 (261) hide show
  1. hexdag/__init__.py +116 -0
  2. hexdag/__main__.py +30 -0
  3. hexdag/adapters/executors/__init__.py +5 -0
  4. hexdag/adapters/executors/local_executor.py +316 -0
  5. hexdag/builtin/__init__.py +6 -0
  6. hexdag/builtin/adapters/__init__.py +51 -0
  7. hexdag/builtin/adapters/anthropic/__init__.py +5 -0
  8. hexdag/builtin/adapters/anthropic/anthropic_adapter.py +151 -0
  9. hexdag/builtin/adapters/database/__init__.py +6 -0
  10. hexdag/builtin/adapters/database/csv/csv_adapter.py +249 -0
  11. hexdag/builtin/adapters/database/pgvector/__init__.py +5 -0
  12. hexdag/builtin/adapters/database/pgvector/pgvector_adapter.py +478 -0
  13. hexdag/builtin/adapters/database/sqlalchemy/sqlalchemy_adapter.py +252 -0
  14. hexdag/builtin/adapters/database/sqlite/__init__.py +5 -0
  15. hexdag/builtin/adapters/database/sqlite/sqlite_adapter.py +410 -0
  16. hexdag/builtin/adapters/local/README.md +59 -0
  17. hexdag/builtin/adapters/local/__init__.py +7 -0
  18. hexdag/builtin/adapters/local/local_observer_manager.py +696 -0
  19. hexdag/builtin/adapters/memory/__init__.py +47 -0
  20. hexdag/builtin/adapters/memory/file_memory_adapter.py +297 -0
  21. hexdag/builtin/adapters/memory/in_memory_memory.py +216 -0
  22. hexdag/builtin/adapters/memory/schemas.py +57 -0
  23. hexdag/builtin/adapters/memory/session_memory.py +178 -0
  24. hexdag/builtin/adapters/memory/sqlite_memory_adapter.py +215 -0
  25. hexdag/builtin/adapters/memory/state_memory.py +280 -0
  26. hexdag/builtin/adapters/mock/README.md +89 -0
  27. hexdag/builtin/adapters/mock/__init__.py +15 -0
  28. hexdag/builtin/adapters/mock/hexdag.toml +50 -0
  29. hexdag/builtin/adapters/mock/mock_database.py +225 -0
  30. hexdag/builtin/adapters/mock/mock_embedding.py +223 -0
  31. hexdag/builtin/adapters/mock/mock_llm.py +177 -0
  32. hexdag/builtin/adapters/mock/mock_tool_adapter.py +192 -0
  33. hexdag/builtin/adapters/mock/mock_tool_router.py +232 -0
  34. hexdag/builtin/adapters/openai/__init__.py +5 -0
  35. hexdag/builtin/adapters/openai/openai_adapter.py +634 -0
  36. hexdag/builtin/adapters/secret/__init__.py +7 -0
  37. hexdag/builtin/adapters/secret/local_secret_adapter.py +248 -0
  38. hexdag/builtin/adapters/unified_tool_router.py +280 -0
  39. hexdag/builtin/macros/__init__.py +17 -0
  40. hexdag/builtin/macros/conversation_agent.py +390 -0
  41. hexdag/builtin/macros/llm_macro.py +151 -0
  42. hexdag/builtin/macros/reasoning_agent.py +423 -0
  43. hexdag/builtin/macros/tool_macro.py +380 -0
  44. hexdag/builtin/nodes/__init__.py +38 -0
  45. hexdag/builtin/nodes/_discovery.py +123 -0
  46. hexdag/builtin/nodes/agent_node.py +696 -0
  47. hexdag/builtin/nodes/base_node_factory.py +242 -0
  48. hexdag/builtin/nodes/composite_node.py +926 -0
  49. hexdag/builtin/nodes/data_node.py +201 -0
  50. hexdag/builtin/nodes/expression_node.py +487 -0
  51. hexdag/builtin/nodes/function_node.py +454 -0
  52. hexdag/builtin/nodes/llm_node.py +491 -0
  53. hexdag/builtin/nodes/loop_node.py +920 -0
  54. hexdag/builtin/nodes/mapped_input.py +518 -0
  55. hexdag/builtin/nodes/port_call_node.py +269 -0
  56. hexdag/builtin/nodes/tool_call_node.py +195 -0
  57. hexdag/builtin/nodes/tool_utils.py +390 -0
  58. hexdag/builtin/prompts/__init__.py +68 -0
  59. hexdag/builtin/prompts/base.py +422 -0
  60. hexdag/builtin/prompts/chat_prompts.py +303 -0
  61. hexdag/builtin/prompts/error_correction_prompts.py +320 -0
  62. hexdag/builtin/prompts/tool_prompts.py +160 -0
  63. hexdag/builtin/tools/builtin_tools.py +84 -0
  64. hexdag/builtin/tools/database_tools.py +164 -0
  65. hexdag/cli/__init__.py +17 -0
  66. hexdag/cli/__main__.py +7 -0
  67. hexdag/cli/commands/__init__.py +27 -0
  68. hexdag/cli/commands/build_cmd.py +812 -0
  69. hexdag/cli/commands/create_cmd.py +208 -0
  70. hexdag/cli/commands/docs_cmd.py +293 -0
  71. hexdag/cli/commands/generate_types_cmd.py +252 -0
  72. hexdag/cli/commands/init_cmd.py +188 -0
  73. hexdag/cli/commands/pipeline_cmd.py +494 -0
  74. hexdag/cli/commands/plugin_dev_cmd.py +529 -0
  75. hexdag/cli/commands/plugins_cmd.py +441 -0
  76. hexdag/cli/commands/studio_cmd.py +101 -0
  77. hexdag/cli/commands/validate_cmd.py +221 -0
  78. hexdag/cli/main.py +84 -0
  79. hexdag/core/__init__.py +83 -0
  80. hexdag/core/config/__init__.py +20 -0
  81. hexdag/core/config/loader.py +479 -0
  82. hexdag/core/config/models.py +150 -0
  83. hexdag/core/configurable.py +294 -0
  84. hexdag/core/context/__init__.py +37 -0
  85. hexdag/core/context/execution_context.py +378 -0
  86. hexdag/core/docs/__init__.py +26 -0
  87. hexdag/core/docs/extractors.py +678 -0
  88. hexdag/core/docs/generators.py +890 -0
  89. hexdag/core/docs/models.py +120 -0
  90. hexdag/core/domain/__init__.py +10 -0
  91. hexdag/core/domain/dag.py +1225 -0
  92. hexdag/core/exceptions.py +234 -0
  93. hexdag/core/expression_parser.py +569 -0
  94. hexdag/core/logging.py +449 -0
  95. hexdag/core/models/__init__.py +17 -0
  96. hexdag/core/models/base.py +138 -0
  97. hexdag/core/orchestration/__init__.py +46 -0
  98. hexdag/core/orchestration/body_executor.py +481 -0
  99. hexdag/core/orchestration/components/__init__.py +97 -0
  100. hexdag/core/orchestration/components/adapter_lifecycle_manager.py +113 -0
  101. hexdag/core/orchestration/components/checkpoint_manager.py +134 -0
  102. hexdag/core/orchestration/components/execution_coordinator.py +360 -0
  103. hexdag/core/orchestration/components/health_check_manager.py +176 -0
  104. hexdag/core/orchestration/components/input_mapper.py +143 -0
  105. hexdag/core/orchestration/components/lifecycle_manager.py +583 -0
  106. hexdag/core/orchestration/components/node_executor.py +377 -0
  107. hexdag/core/orchestration/components/secret_manager.py +202 -0
  108. hexdag/core/orchestration/components/wave_executor.py +158 -0
  109. hexdag/core/orchestration/constants.py +17 -0
  110. hexdag/core/orchestration/events/README.md +312 -0
  111. hexdag/core/orchestration/events/__init__.py +104 -0
  112. hexdag/core/orchestration/events/batching.py +330 -0
  113. hexdag/core/orchestration/events/decorators.py +139 -0
  114. hexdag/core/orchestration/events/events.py +573 -0
  115. hexdag/core/orchestration/events/observers/__init__.py +30 -0
  116. hexdag/core/orchestration/events/observers/core_observers.py +690 -0
  117. hexdag/core/orchestration/events/observers/models.py +111 -0
  118. hexdag/core/orchestration/events/taxonomy.py +269 -0
  119. hexdag/core/orchestration/hook_context.py +237 -0
  120. hexdag/core/orchestration/hooks.py +437 -0
  121. hexdag/core/orchestration/models.py +418 -0
  122. hexdag/core/orchestration/orchestrator.py +910 -0
  123. hexdag/core/orchestration/orchestrator_factory.py +275 -0
  124. hexdag/core/orchestration/port_wrappers.py +327 -0
  125. hexdag/core/orchestration/prompt/__init__.py +32 -0
  126. hexdag/core/orchestration/prompt/template.py +332 -0
  127. hexdag/core/pipeline_builder/__init__.py +21 -0
  128. hexdag/core/pipeline_builder/component_instantiator.py +386 -0
  129. hexdag/core/pipeline_builder/include_tag.py +265 -0
  130. hexdag/core/pipeline_builder/pipeline_config.py +133 -0
  131. hexdag/core/pipeline_builder/py_tag.py +223 -0
  132. hexdag/core/pipeline_builder/tag_discovery.py +268 -0
  133. hexdag/core/pipeline_builder/yaml_builder.py +1196 -0
  134. hexdag/core/pipeline_builder/yaml_validator.py +569 -0
  135. hexdag/core/ports/__init__.py +65 -0
  136. hexdag/core/ports/api_call.py +133 -0
  137. hexdag/core/ports/database.py +489 -0
  138. hexdag/core/ports/embedding.py +215 -0
  139. hexdag/core/ports/executor.py +237 -0
  140. hexdag/core/ports/file_storage.py +117 -0
  141. hexdag/core/ports/healthcheck.py +87 -0
  142. hexdag/core/ports/llm.py +551 -0
  143. hexdag/core/ports/memory.py +70 -0
  144. hexdag/core/ports/observer_manager.py +130 -0
  145. hexdag/core/ports/secret.py +145 -0
  146. hexdag/core/ports/tool_router.py +94 -0
  147. hexdag/core/ports_builder.py +623 -0
  148. hexdag/core/protocols.py +273 -0
  149. hexdag/core/resolver.py +304 -0
  150. hexdag/core/schema/__init__.py +9 -0
  151. hexdag/core/schema/generator.py +742 -0
  152. hexdag/core/secrets.py +242 -0
  153. hexdag/core/types.py +413 -0
  154. hexdag/core/utils/async_warnings.py +206 -0
  155. hexdag/core/utils/schema_conversion.py +78 -0
  156. hexdag/core/utils/sql_validation.py +86 -0
  157. hexdag/core/validation/secure_json.py +148 -0
  158. hexdag/core/yaml_macro.py +517 -0
  159. hexdag/mcp_server.py +3120 -0
  160. hexdag/studio/__init__.py +10 -0
  161. hexdag/studio/build_ui.py +92 -0
  162. hexdag/studio/server/__init__.py +1 -0
  163. hexdag/studio/server/main.py +100 -0
  164. hexdag/studio/server/routes/__init__.py +9 -0
  165. hexdag/studio/server/routes/execute.py +208 -0
  166. hexdag/studio/server/routes/export.py +558 -0
  167. hexdag/studio/server/routes/files.py +207 -0
  168. hexdag/studio/server/routes/plugins.py +419 -0
  169. hexdag/studio/server/routes/validate.py +220 -0
  170. hexdag/studio/ui/index.html +13 -0
  171. hexdag/studio/ui/package-lock.json +2992 -0
  172. hexdag/studio/ui/package.json +31 -0
  173. hexdag/studio/ui/postcss.config.js +6 -0
  174. hexdag/studio/ui/public/hexdag.svg +5 -0
  175. hexdag/studio/ui/src/App.tsx +251 -0
  176. hexdag/studio/ui/src/components/Canvas.tsx +408 -0
  177. hexdag/studio/ui/src/components/ContextMenu.tsx +187 -0
  178. hexdag/studio/ui/src/components/FileBrowser.tsx +123 -0
  179. hexdag/studio/ui/src/components/Header.tsx +181 -0
  180. hexdag/studio/ui/src/components/HexdagNode.tsx +193 -0
  181. hexdag/studio/ui/src/components/NodeInspector.tsx +512 -0
  182. hexdag/studio/ui/src/components/NodePalette.tsx +262 -0
  183. hexdag/studio/ui/src/components/NodePortsSection.tsx +403 -0
  184. hexdag/studio/ui/src/components/PluginManager.tsx +347 -0
  185. hexdag/studio/ui/src/components/PortsEditor.tsx +481 -0
  186. hexdag/studio/ui/src/components/PythonEditor.tsx +195 -0
  187. hexdag/studio/ui/src/components/ValidationPanel.tsx +105 -0
  188. hexdag/studio/ui/src/components/YamlEditor.tsx +196 -0
  189. hexdag/studio/ui/src/components/index.ts +8 -0
  190. hexdag/studio/ui/src/index.css +92 -0
  191. hexdag/studio/ui/src/main.tsx +10 -0
  192. hexdag/studio/ui/src/types/index.ts +123 -0
  193. hexdag/studio/ui/src/vite-env.d.ts +1 -0
  194. hexdag/studio/ui/tailwind.config.js +29 -0
  195. hexdag/studio/ui/tsconfig.json +37 -0
  196. hexdag/studio/ui/tsconfig.node.json +13 -0
  197. hexdag/studio/ui/vite.config.ts +35 -0
  198. hexdag/visualization/__init__.py +69 -0
  199. hexdag/visualization/dag_visualizer.py +1020 -0
  200. hexdag-0.5.0.dev1.dist-info/METADATA +369 -0
  201. hexdag-0.5.0.dev1.dist-info/RECORD +261 -0
  202. hexdag-0.5.0.dev1.dist-info/WHEEL +4 -0
  203. hexdag-0.5.0.dev1.dist-info/entry_points.txt +4 -0
  204. hexdag-0.5.0.dev1.dist-info/licenses/LICENSE +190 -0
  205. hexdag_plugins/.gitignore +43 -0
  206. hexdag_plugins/README.md +73 -0
  207. hexdag_plugins/__init__.py +1 -0
  208. hexdag_plugins/azure/LICENSE +21 -0
  209. hexdag_plugins/azure/README.md +414 -0
  210. hexdag_plugins/azure/__init__.py +21 -0
  211. hexdag_plugins/azure/azure_blob_adapter.py +450 -0
  212. hexdag_plugins/azure/azure_cosmos_adapter.py +383 -0
  213. hexdag_plugins/azure/azure_keyvault_adapter.py +314 -0
  214. hexdag_plugins/azure/azure_openai_adapter.py +415 -0
  215. hexdag_plugins/azure/pyproject.toml +107 -0
  216. hexdag_plugins/azure/tests/__init__.py +1 -0
  217. hexdag_plugins/azure/tests/test_azure_blob_adapter.py +350 -0
  218. hexdag_plugins/azure/tests/test_azure_cosmos_adapter.py +323 -0
  219. hexdag_plugins/azure/tests/test_azure_keyvault_adapter.py +330 -0
  220. hexdag_plugins/azure/tests/test_azure_openai_adapter.py +329 -0
  221. hexdag_plugins/hexdag_etl/README.md +168 -0
  222. hexdag_plugins/hexdag_etl/__init__.py +53 -0
  223. hexdag_plugins/hexdag_etl/examples/01_simple_pandas_transform.py +270 -0
  224. hexdag_plugins/hexdag_etl/examples/02_simple_pandas_only.py +149 -0
  225. hexdag_plugins/hexdag_etl/examples/03_file_io_pipeline.py +109 -0
  226. hexdag_plugins/hexdag_etl/examples/test_pandas_transform.py +84 -0
  227. hexdag_plugins/hexdag_etl/hexdag.toml +25 -0
  228. hexdag_plugins/hexdag_etl/hexdag_etl/__init__.py +48 -0
  229. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/__init__.py +13 -0
  230. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/api_extract.py +230 -0
  231. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/base_node_factory.py +181 -0
  232. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/file_io.py +415 -0
  233. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/outlook.py +492 -0
  234. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/pandas_transform.py +563 -0
  235. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/sql_extract_load.py +112 -0
  236. hexdag_plugins/hexdag_etl/pyproject.toml +82 -0
  237. hexdag_plugins/hexdag_etl/test_transform.py +54 -0
  238. hexdag_plugins/hexdag_etl/tests/test_plugin_integration.py +62 -0
  239. hexdag_plugins/mysql_adapter/LICENSE +21 -0
  240. hexdag_plugins/mysql_adapter/README.md +224 -0
  241. hexdag_plugins/mysql_adapter/__init__.py +6 -0
  242. hexdag_plugins/mysql_adapter/mysql_adapter.py +408 -0
  243. hexdag_plugins/mysql_adapter/pyproject.toml +93 -0
  244. hexdag_plugins/mysql_adapter/tests/test_mysql_adapter.py +259 -0
  245. hexdag_plugins/storage/README.md +184 -0
  246. hexdag_plugins/storage/__init__.py +19 -0
  247. hexdag_plugins/storage/file/__init__.py +5 -0
  248. hexdag_plugins/storage/file/local.py +325 -0
  249. hexdag_plugins/storage/ports/__init__.py +5 -0
  250. hexdag_plugins/storage/ports/vector_store.py +236 -0
  251. hexdag_plugins/storage/sql/__init__.py +7 -0
  252. hexdag_plugins/storage/sql/base.py +187 -0
  253. hexdag_plugins/storage/sql/mysql.py +27 -0
  254. hexdag_plugins/storage/sql/postgresql.py +27 -0
  255. hexdag_plugins/storage/tests/__init__.py +1 -0
  256. hexdag_plugins/storage/tests/test_local_file_storage.py +161 -0
  257. hexdag_plugins/storage/tests/test_sql_adapters.py +212 -0
  258. hexdag_plugins/storage/vector/__init__.py +7 -0
  259. hexdag_plugins/storage/vector/chromadb.py +223 -0
  260. hexdag_plugins/storage/vector/in_memory.py +285 -0
  261. hexdag_plugins/storage/vector/pgvector.py +502 -0
@@ -0,0 +1,26 @@
1
+ """Documentation generation framework for hexDAG.
2
+
3
+ This module provides tools to extract documentation from code artifacts
4
+ (decorators, signatures, docstrings) and generate up-to-date documentation
5
+ for the MCP server and other consumers.
6
+ """
7
+
8
+ from hexdag.core.docs.extractors import DocExtractor
9
+ from hexdag.core.docs.generators import GuideGenerator
10
+ from hexdag.core.docs.models import (
11
+ AdapterDoc,
12
+ ComponentDoc,
13
+ NodeDoc,
14
+ ParameterDoc,
15
+ ToolDoc,
16
+ )
17
+
18
+ __all__ = [
19
+ "AdapterDoc",
20
+ "ComponentDoc",
21
+ "DocExtractor",
22
+ "GuideGenerator",
23
+ "NodeDoc",
24
+ "ParameterDoc",
25
+ "ToolDoc",
26
+ ]
@@ -0,0 +1,678 @@
1
+ """Documentation extractors for hexDAG components.
2
+
3
+ This module provides utilities to extract documentation from code artifacts
4
+ including signatures, docstrings, decorators, and explicit schema attributes.
5
+ """
6
+
7
+ import inspect
8
+ import re
9
+ from collections.abc import Callable
10
+ from typing import Any, get_type_hints
11
+
12
+ from hexdag.core.docs.models import (
13
+ AdapterDoc,
14
+ NodeDoc,
15
+ ParameterDoc,
16
+ ToolDoc,
17
+ )
18
+ from hexdag.core.logging import get_logger
19
+ from hexdag.core.schema import SchemaGenerator
20
+ from hexdag.core.secrets import SecretDescriptor
21
+
22
+ logger = get_logger(__name__)
23
+
24
+
25
+ class DocExtractor:
26
+ """Extract documentation from Python code artifacts.
27
+
28
+ This class provides static methods to extract structured documentation
29
+ from adapters, nodes, tools, and other callables by inspecting their
30
+ signatures, docstrings, and special attributes.
31
+ """
32
+
33
+ @staticmethod
34
+ def extract_parameters(obj: Callable | type) -> list[ParameterDoc]:
35
+ """Extract parameter documentation from a callable or class __init__.
36
+
37
+ Parameters
38
+ ----------
39
+ obj : Callable | type
40
+ Function, method, or class to extract parameters from
41
+
42
+ Returns
43
+ -------
44
+ list[ParameterDoc]
45
+ List of documented parameters
46
+ """
47
+ # Get the target to inspect
48
+ if isinstance(obj, type):
49
+ target = obj.__init__
50
+ elif callable(obj) and not inspect.isfunction(obj):
51
+ target = obj.__call__
52
+ else:
53
+ target = obj
54
+
55
+ try:
56
+ sig = inspect.signature(target)
57
+ except (ValueError, TypeError) as e:
58
+ logger.warning(f"Could not get signature for {obj}: {e}")
59
+ return []
60
+
61
+ # Extract docstring parameter descriptions
62
+ param_docs = SchemaGenerator._extract_param_docs(target)
63
+
64
+ # Try to get type hints
65
+ try:
66
+ hints = get_type_hints(target)
67
+ except Exception:
68
+ hints = {}
69
+
70
+ parameters = []
71
+ for param_name, param in sig.parameters.items():
72
+ if param_name in ("self", "cls", "args", "kwargs"):
73
+ continue
74
+
75
+ # Skip VAR_POSITIONAL and VAR_KEYWORD
76
+ if param.kind in (
77
+ inspect.Parameter.VAR_POSITIONAL,
78
+ inspect.Parameter.VAR_KEYWORD,
79
+ ):
80
+ continue
81
+
82
+ # Get type hint
83
+ type_hint = hints.get(param_name, param.annotation)
84
+ if type_hint == inspect.Parameter.empty:
85
+ type_hint_str = "Any"
86
+ else:
87
+ type_hint_str = DocExtractor._format_type_hint(type_hint)
88
+
89
+ # Check for secret descriptor
90
+ default_value = param.default
91
+ is_secret = isinstance(default_value, SecretDescriptor)
92
+
93
+ # Get default value
94
+ if default_value == inspect.Parameter.empty:
95
+ default_str = None
96
+ required = True
97
+ elif is_secret:
98
+ default_str = f"secret(env='{default_value.env_var}')"
99
+ required = default_value.required
100
+ else:
101
+ default_str = repr(default_value)
102
+ required = False
103
+
104
+ # Get description from docstring
105
+ description = param_docs.get(param_name, "")
106
+
107
+ # Check for enum/Literal values
108
+ enum_values = DocExtractor._extract_enum_values(type_hint)
109
+
110
+ parameters.append(
111
+ ParameterDoc(
112
+ name=param_name,
113
+ type_hint=type_hint_str,
114
+ description=description,
115
+ required=required,
116
+ default=default_str,
117
+ enum_values=enum_values,
118
+ )
119
+ )
120
+
121
+ return parameters
122
+
123
+ @staticmethod
124
+ def _format_type_hint(hint: Any) -> str:
125
+ """Format a type hint as a readable string.
126
+
127
+ Parameters
128
+ ----------
129
+ hint : Any
130
+ Type annotation
131
+
132
+ Returns
133
+ -------
134
+ str
135
+ Human-readable type string
136
+ """
137
+ if hint is None or hint is type(None):
138
+ return "None"
139
+
140
+ # Handle string annotations
141
+ if isinstance(hint, str):
142
+ return hint
143
+
144
+ # Get origin and args for generic types
145
+ origin = getattr(hint, "__origin__", None)
146
+ args = getattr(hint, "__args__", ())
147
+
148
+ # Handle Union types (including | syntax)
149
+ if origin is type(int | str): # UnionType
150
+ parts = [DocExtractor._format_type_hint(arg) for arg in args]
151
+ return " | ".join(parts)
152
+
153
+ # Handle Optional (Union with None)
154
+ try:
155
+ from typing import Union
156
+
157
+ if origin is Union:
158
+ non_none = [arg for arg in args if arg is not type(None)]
159
+ if len(non_none) == 1 and type(None) in args:
160
+ return f"{DocExtractor._format_type_hint(non_none[0])} | None"
161
+ parts = [DocExtractor._format_type_hint(arg) for arg in args]
162
+ return " | ".join(parts)
163
+ except ImportError:
164
+ pass
165
+
166
+ # Handle Literal
167
+ try:
168
+ from typing import Literal, get_origin
169
+
170
+ if get_origin(hint) is Literal:
171
+ values = ", ".join(repr(v) for v in args)
172
+ return f"Literal[{values}]"
173
+ except (ImportError, TypeError):
174
+ pass
175
+
176
+ # Handle list, dict, etc.
177
+ if origin is list:
178
+ if args:
179
+ return f"list[{DocExtractor._format_type_hint(args[0])}]"
180
+ return "list"
181
+ if origin is dict:
182
+ if len(args) == 2:
183
+ key_type = DocExtractor._format_type_hint(args[0])
184
+ val_type = DocExtractor._format_type_hint(args[1])
185
+ return f"dict[{key_type}, {val_type}]"
186
+ return "dict"
187
+ if origin is set:
188
+ if args:
189
+ return f"set[{DocExtractor._format_type_hint(args[0])}]"
190
+ return "set"
191
+ if origin is tuple:
192
+ if args:
193
+ parts = [DocExtractor._format_type_hint(arg) for arg in args]
194
+ return f"tuple[{', '.join(parts)}]"
195
+ return "tuple"
196
+
197
+ # Handle basic types
198
+ if hasattr(hint, "__name__"):
199
+ return hint.__name__
200
+
201
+ # Fallback
202
+ return str(hint)
203
+
204
+ @staticmethod
205
+ def _extract_enum_values(hint: Any) -> list[str] | None:
206
+ """Extract enum values from Literal type hints.
207
+
208
+ Parameters
209
+ ----------
210
+ hint : Any
211
+ Type annotation to check
212
+
213
+ Returns
214
+ -------
215
+ list[str] | None
216
+ List of allowed values or None
217
+ """
218
+ try:
219
+ from typing import Literal, get_args, get_origin
220
+
221
+ if get_origin(hint) is Literal:
222
+ return [str(v) for v in get_args(hint)]
223
+ except (ImportError, TypeError):
224
+ pass
225
+ return None
226
+
227
+ @staticmethod
228
+ def extract_docstring_parts(obj: Any) -> tuple[str, str, list[str]]:
229
+ """Extract description, full docstring, and examples from docstring.
230
+
231
+ Parameters
232
+ ----------
233
+ obj : Any
234
+ Object with __doc__ attribute
235
+
236
+ Returns
237
+ -------
238
+ tuple[str, str, list[str]]
239
+ (first_line_description, full_docstring, examples)
240
+ """
241
+ docstring = inspect.getdoc(obj) or ""
242
+ if not docstring:
243
+ return "", "", []
244
+
245
+ # First line as description
246
+ lines = docstring.split("\n")
247
+ description = lines[0].strip() if lines else ""
248
+
249
+ # Extract examples
250
+ examples = DocExtractor._extract_examples(docstring)
251
+
252
+ return description, docstring, examples
253
+
254
+ @staticmethod
255
+ def _extract_examples(docstring: str) -> list[str]:
256
+ """Extract code examples from docstring.
257
+
258
+ Parameters
259
+ ----------
260
+ docstring : str
261
+ Full docstring text
262
+
263
+ Returns
264
+ -------
265
+ list[str]
266
+ List of code examples
267
+ """
268
+ examples = []
269
+
270
+ # Find Examples section
271
+ patterns = [
272
+ r"Examples?\s*[-=]*\s*\n(.*?)(?=\n\s*[A-Z][a-z]+\s*[-=]*\s*\n|\Z)",
273
+ r"Examples?\s*\n\s*-+\s*\n(.*?)(?=\n\s*[A-Z][a-z]+\s*\n\s*-+|\Z)",
274
+ ]
275
+
276
+ for pattern in patterns:
277
+ match = re.search(pattern, docstring, re.DOTALL | re.IGNORECASE)
278
+ if match:
279
+ examples_text = match.group(1)
280
+ # Extract code blocks
281
+ code_blocks = re.findall(
282
+ r"```(?:python)?\n(.*?)```|>>> (.*?)(?=\n(?!\.\.\.|\s)|\Z)",
283
+ examples_text,
284
+ re.DOTALL,
285
+ )
286
+ for block in code_blocks:
287
+ code = block[0] or block[1]
288
+ if code.strip():
289
+ examples.append(code.strip())
290
+ break
291
+
292
+ return examples
293
+
294
+ @staticmethod
295
+ def extract_adapter_doc(cls: type) -> AdapterDoc:
296
+ """Extract documentation from an adapter class.
297
+
298
+ Parameters
299
+ ----------
300
+ cls : type
301
+ Adapter class to document
302
+
303
+ Returns
304
+ -------
305
+ AdapterDoc
306
+ Extracted adapter documentation
307
+ """
308
+ description, full_docstring, examples = DocExtractor.extract_docstring_parts(cls)
309
+ parameters = DocExtractor.extract_parameters(cls)
310
+
311
+ # Determine port type from class name or protocol
312
+ port_type = DocExtractor._guess_port_type(cls)
313
+
314
+ # Extract secrets from signature
315
+ secrets = DocExtractor._extract_secrets(cls)
316
+
317
+ # Generate module path
318
+ module_path = f"{cls.__module__}.{cls.__name__}"
319
+
320
+ # Generate decorator example
321
+ decorator_example = DocExtractor._generate_adapter_decorator_example(
322
+ cls, port_type, secrets
323
+ )
324
+
325
+ # Generate YAML example
326
+ yaml_example = DocExtractor._generate_adapter_yaml_example(cls, port_type)
327
+
328
+ return AdapterDoc(
329
+ name=cls.__name__,
330
+ module_path=module_path,
331
+ description=description,
332
+ full_docstring=full_docstring,
333
+ parameters=parameters,
334
+ examples=examples,
335
+ yaml_example=yaml_example,
336
+ port_type=port_type,
337
+ secrets=secrets,
338
+ decorator_example=decorator_example,
339
+ )
340
+
341
+ @staticmethod
342
+ def _guess_port_type(cls: type) -> str:
343
+ """Guess port type from adapter class name or implemented protocols.
344
+
345
+ Parameters
346
+ ----------
347
+ cls : type
348
+ Adapter class
349
+
350
+ Returns
351
+ -------
352
+ str
353
+ Guessed port type
354
+ """
355
+ name_lower = cls.__name__.lower()
356
+
357
+ # Check class name patterns
358
+ if "llm" in name_lower or "openai" in name_lower or "anthropic" in name_lower:
359
+ return "llm"
360
+ if "memory" in name_lower:
361
+ return "memory"
362
+ if "database" in name_lower or "sql" in name_lower:
363
+ return "database"
364
+ if "secret" in name_lower or "keyvault" in name_lower:
365
+ return "secret"
366
+ if "storage" in name_lower or "blob" in name_lower:
367
+ return "storage"
368
+ if "tool" in name_lower and "router" in name_lower:
369
+ return "tool_router"
370
+ if "embedding" in name_lower:
371
+ return "embedding"
372
+ if "observer" in name_lower:
373
+ return "observer_manager"
374
+
375
+ # Check implemented protocols via base classes
376
+ for base in cls.__mro__:
377
+ base_name = base.__name__.lower()
378
+ if "generation" in base_name or "llm" in base_name:
379
+ return "llm"
380
+ if "memory" in base_name:
381
+ return "memory"
382
+ if "database" in base_name:
383
+ return "database"
384
+
385
+ return "unknown"
386
+
387
+ @staticmethod
388
+ def _extract_secrets(cls: type) -> dict[str, str]:
389
+ """Extract secret declarations from __init__ signature.
390
+
391
+ Parameters
392
+ ----------
393
+ cls : type
394
+ Class to inspect
395
+
396
+ Returns
397
+ -------
398
+ dict[str, str]
399
+ Mapping of parameter name to environment variable name
400
+ """
401
+ try:
402
+ sig = inspect.signature(cls.__init__)
403
+ except (ValueError, TypeError):
404
+ return {}
405
+
406
+ secrets = {}
407
+ for param_name, param in sig.parameters.items():
408
+ if param_name in ("self", "cls"):
409
+ continue
410
+
411
+ if isinstance(param.default, SecretDescriptor):
412
+ secrets[param_name] = param.default.env_var
413
+
414
+ return secrets
415
+
416
+ @staticmethod
417
+ def _generate_adapter_decorator_example(
418
+ cls: type, port_type: str, secrets: dict[str, str]
419
+ ) -> str:
420
+ """Generate example @adapter decorator usage.
421
+
422
+ Parameters
423
+ ----------
424
+ cls : type
425
+ Adapter class
426
+ port_type : str
427
+ Port type
428
+ secrets : dict[str, str]
429
+ Secret mappings
430
+
431
+ Returns
432
+ -------
433
+ str
434
+ Example decorator code
435
+ """
436
+ name = cls.__name__.replace("Adapter", "").lower()
437
+
438
+ if secrets:
439
+ secrets_str = ", ".join(f'"{k}": "{v}"' for k, v in secrets.items())
440
+ return f'@adapter("{port_type}", name="{name}", secrets={{{secrets_str}}})'
441
+ return f'@adapter("{port_type}", name="{name}")'
442
+
443
+ @staticmethod
444
+ def _generate_adapter_yaml_example(cls: type, port_type: str) -> str:
445
+ """Generate YAML usage example for adapter.
446
+
447
+ Parameters
448
+ ----------
449
+ cls : type
450
+ Adapter class
451
+ port_type : str
452
+ Port type
453
+
454
+ Returns
455
+ -------
456
+ str
457
+ YAML example
458
+ """
459
+ module_path = f"{cls.__module__}.{cls.__name__}"
460
+ return f"""ports:
461
+ {port_type}:
462
+ adapter: {module_path}
463
+ config:
464
+ # Add configuration here"""
465
+
466
+ @staticmethod
467
+ def extract_node_doc(cls: type) -> NodeDoc:
468
+ """Extract documentation from a node factory class.
469
+
470
+ Parameters
471
+ ----------
472
+ cls : type
473
+ Node factory class to document
474
+
475
+ Returns
476
+ -------
477
+ NodeDoc
478
+ Extracted node documentation
479
+ """
480
+ description, full_docstring, examples = DocExtractor.extract_docstring_parts(cls)
481
+
482
+ # Check for _yaml_schema
483
+ yaml_schema = getattr(cls, "_yaml_schema", None)
484
+
485
+ # If has explicit schema, extract parameters from it
486
+ if yaml_schema and isinstance(yaml_schema, dict):
487
+ parameters = DocExtractor._extract_params_from_schema(yaml_schema)
488
+ if "description" in yaml_schema:
489
+ description = yaml_schema["description"]
490
+ else:
491
+ # Extract from __call__ method
492
+ try:
493
+ instance = cls()
494
+ parameters = DocExtractor.extract_parameters(instance)
495
+ except Exception:
496
+ parameters = DocExtractor.extract_parameters(cls)
497
+
498
+ # Generate module path
499
+ module_path = f"{cls.__module__}.{cls.__name__}"
500
+
501
+ # Determine kind from class name
502
+ kind = DocExtractor._class_name_to_kind(cls.__name__)
503
+
504
+ # Generate YAML example
505
+ yaml_example = DocExtractor._generate_node_yaml_example(cls, kind, yaml_schema)
506
+
507
+ return NodeDoc(
508
+ name=cls.__name__,
509
+ module_path=module_path,
510
+ description=description,
511
+ full_docstring=full_docstring,
512
+ parameters=parameters,
513
+ examples=examples,
514
+ yaml_example=yaml_example,
515
+ namespace="core",
516
+ yaml_schema=yaml_schema,
517
+ kind=kind,
518
+ )
519
+
520
+ @staticmethod
521
+ def _class_name_to_kind(class_name: str) -> str:
522
+ """Convert class name to YAML kind.
523
+
524
+ Parameters
525
+ ----------
526
+ class_name : str
527
+ Class name (e.g., "LLMNode", "FunctionNode")
528
+
529
+ Returns
530
+ -------
531
+ str
532
+ YAML kind (e.g., "llm_node", "function_node")
533
+ """
534
+ # Split on uppercase letters and join with underscore
535
+ words = re.findall(r"[A-Z][a-z]*", class_name)
536
+ return "_".join(word.lower() for word in words)
537
+
538
+ @staticmethod
539
+ def _extract_params_from_schema(schema: dict[str, Any]) -> list[ParameterDoc]:
540
+ """Extract parameters from _yaml_schema.
541
+
542
+ Parameters
543
+ ----------
544
+ schema : dict[str, Any]
545
+ JSON Schema dict
546
+
547
+ Returns
548
+ -------
549
+ list[ParameterDoc]
550
+ Extracted parameters
551
+ """
552
+ parameters = []
553
+ properties = schema.get("properties", {})
554
+ required = set(schema.get("required", []))
555
+
556
+ for prop_name, prop_schema in properties.items():
557
+ param_type = prop_schema.get("type", "any")
558
+ description = prop_schema.get("description", "")
559
+ default = prop_schema.get("default")
560
+
561
+ # Handle enum
562
+ enum_values = prop_schema.get("enum")
563
+ if enum_values:
564
+ enum_values = [str(v) for v in enum_values]
565
+
566
+ parameters.append(
567
+ ParameterDoc(
568
+ name=prop_name,
569
+ type_hint=param_type,
570
+ description=description,
571
+ required=prop_name in required,
572
+ default=repr(default) if default is not None else None,
573
+ enum_values=enum_values,
574
+ )
575
+ )
576
+
577
+ return parameters
578
+
579
+ @staticmethod
580
+ def _generate_node_yaml_example(
581
+ cls: type, kind: str, yaml_schema: dict[str, Any] | None
582
+ ) -> str:
583
+ """Generate YAML usage example for node.
584
+
585
+ Parameters
586
+ ----------
587
+ cls : type
588
+ Node class
589
+ kind : str
590
+ Node kind
591
+ yaml_schema : dict[str, Any] | None
592
+ Explicit schema if available
593
+
594
+ Returns
595
+ -------
596
+ str
597
+ YAML example
598
+ """
599
+ if yaml_schema:
600
+ return SchemaGenerator.generate_example_yaml(kind, yaml_schema)
601
+
602
+ # Fallback basic example
603
+ return f"""- kind: {kind}
604
+ metadata:
605
+ name: my_{kind.replace("_node", "")}
606
+ spec:
607
+ # Add configuration here
608
+ dependencies: []"""
609
+
610
+ @staticmethod
611
+ def extract_tool_doc(func: Callable) -> ToolDoc:
612
+ """Extract documentation from a tool function.
613
+
614
+ Parameters
615
+ ----------
616
+ func : Callable
617
+ Tool function to document
618
+
619
+ Returns
620
+ -------
621
+ ToolDoc
622
+ Extracted tool documentation
623
+ """
624
+ description, full_docstring, examples = DocExtractor.extract_docstring_parts(func)
625
+ parameters = DocExtractor.extract_parameters(func)
626
+
627
+ # Get return type
628
+ try:
629
+ hints = get_type_hints(func)
630
+ return_type = hints.get("return")
631
+ return_type_str = DocExtractor._format_type_hint(return_type) if return_type else "Any"
632
+ except Exception:
633
+ return_type_str = "Any"
634
+
635
+ # Check if async
636
+ is_async = inspect.iscoroutinefunction(func)
637
+
638
+ # Generate module path
639
+ module_path = f"{func.__module__}.{func.__name__}"
640
+
641
+ # Generate YAML example for agent usage
642
+ yaml_example = DocExtractor._generate_tool_yaml_example(func)
643
+
644
+ return ToolDoc(
645
+ name=func.__name__,
646
+ module_path=module_path,
647
+ description=description,
648
+ full_docstring=full_docstring,
649
+ parameters=parameters,
650
+ examples=examples,
651
+ yaml_example=yaml_example,
652
+ namespace="core",
653
+ return_type=return_type_str,
654
+ is_async=is_async,
655
+ )
656
+
657
+ @staticmethod
658
+ def _generate_tool_yaml_example(func: Callable) -> str:
659
+ """Generate YAML example showing tool usage with agents.
660
+
661
+ Parameters
662
+ ----------
663
+ func : Callable
664
+ Tool function
665
+
666
+ Returns
667
+ -------
668
+ str
669
+ YAML example
670
+ """
671
+ module_path = f"{func.__module__}.{func.__name__}"
672
+ return f"""- kind: agent_node
673
+ metadata:
674
+ name: my_agent
675
+ spec:
676
+ tools:
677
+ - {module_path}
678
+ # ... other config"""