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.
- hexdag/__init__.py +116 -0
- hexdag/__main__.py +30 -0
- hexdag/adapters/executors/__init__.py +5 -0
- hexdag/adapters/executors/local_executor.py +316 -0
- hexdag/builtin/__init__.py +6 -0
- hexdag/builtin/adapters/__init__.py +51 -0
- hexdag/builtin/adapters/anthropic/__init__.py +5 -0
- hexdag/builtin/adapters/anthropic/anthropic_adapter.py +151 -0
- hexdag/builtin/adapters/database/__init__.py +6 -0
- hexdag/builtin/adapters/database/csv/csv_adapter.py +249 -0
- hexdag/builtin/adapters/database/pgvector/__init__.py +5 -0
- hexdag/builtin/adapters/database/pgvector/pgvector_adapter.py +478 -0
- hexdag/builtin/adapters/database/sqlalchemy/sqlalchemy_adapter.py +252 -0
- hexdag/builtin/adapters/database/sqlite/__init__.py +5 -0
- hexdag/builtin/adapters/database/sqlite/sqlite_adapter.py +410 -0
- hexdag/builtin/adapters/local/README.md +59 -0
- hexdag/builtin/adapters/local/__init__.py +7 -0
- hexdag/builtin/adapters/local/local_observer_manager.py +696 -0
- hexdag/builtin/adapters/memory/__init__.py +47 -0
- hexdag/builtin/adapters/memory/file_memory_adapter.py +297 -0
- hexdag/builtin/adapters/memory/in_memory_memory.py +216 -0
- hexdag/builtin/adapters/memory/schemas.py +57 -0
- hexdag/builtin/adapters/memory/session_memory.py +178 -0
- hexdag/builtin/adapters/memory/sqlite_memory_adapter.py +215 -0
- hexdag/builtin/adapters/memory/state_memory.py +280 -0
- hexdag/builtin/adapters/mock/README.md +89 -0
- hexdag/builtin/adapters/mock/__init__.py +15 -0
- hexdag/builtin/adapters/mock/hexdag.toml +50 -0
- hexdag/builtin/adapters/mock/mock_database.py +225 -0
- hexdag/builtin/adapters/mock/mock_embedding.py +223 -0
- hexdag/builtin/adapters/mock/mock_llm.py +177 -0
- hexdag/builtin/adapters/mock/mock_tool_adapter.py +192 -0
- hexdag/builtin/adapters/mock/mock_tool_router.py +232 -0
- hexdag/builtin/adapters/openai/__init__.py +5 -0
- hexdag/builtin/adapters/openai/openai_adapter.py +634 -0
- hexdag/builtin/adapters/secret/__init__.py +7 -0
- hexdag/builtin/adapters/secret/local_secret_adapter.py +248 -0
- hexdag/builtin/adapters/unified_tool_router.py +280 -0
- hexdag/builtin/macros/__init__.py +17 -0
- hexdag/builtin/macros/conversation_agent.py +390 -0
- hexdag/builtin/macros/llm_macro.py +151 -0
- hexdag/builtin/macros/reasoning_agent.py +423 -0
- hexdag/builtin/macros/tool_macro.py +380 -0
- hexdag/builtin/nodes/__init__.py +38 -0
- hexdag/builtin/nodes/_discovery.py +123 -0
- hexdag/builtin/nodes/agent_node.py +696 -0
- hexdag/builtin/nodes/base_node_factory.py +242 -0
- hexdag/builtin/nodes/composite_node.py +926 -0
- hexdag/builtin/nodes/data_node.py +201 -0
- hexdag/builtin/nodes/expression_node.py +487 -0
- hexdag/builtin/nodes/function_node.py +454 -0
- hexdag/builtin/nodes/llm_node.py +491 -0
- hexdag/builtin/nodes/loop_node.py +920 -0
- hexdag/builtin/nodes/mapped_input.py +518 -0
- hexdag/builtin/nodes/port_call_node.py +269 -0
- hexdag/builtin/nodes/tool_call_node.py +195 -0
- hexdag/builtin/nodes/tool_utils.py +390 -0
- hexdag/builtin/prompts/__init__.py +68 -0
- hexdag/builtin/prompts/base.py +422 -0
- hexdag/builtin/prompts/chat_prompts.py +303 -0
- hexdag/builtin/prompts/error_correction_prompts.py +320 -0
- hexdag/builtin/prompts/tool_prompts.py +160 -0
- hexdag/builtin/tools/builtin_tools.py +84 -0
- hexdag/builtin/tools/database_tools.py +164 -0
- hexdag/cli/__init__.py +17 -0
- hexdag/cli/__main__.py +7 -0
- hexdag/cli/commands/__init__.py +27 -0
- hexdag/cli/commands/build_cmd.py +812 -0
- hexdag/cli/commands/create_cmd.py +208 -0
- hexdag/cli/commands/docs_cmd.py +293 -0
- hexdag/cli/commands/generate_types_cmd.py +252 -0
- hexdag/cli/commands/init_cmd.py +188 -0
- hexdag/cli/commands/pipeline_cmd.py +494 -0
- hexdag/cli/commands/plugin_dev_cmd.py +529 -0
- hexdag/cli/commands/plugins_cmd.py +441 -0
- hexdag/cli/commands/studio_cmd.py +101 -0
- hexdag/cli/commands/validate_cmd.py +221 -0
- hexdag/cli/main.py +84 -0
- hexdag/core/__init__.py +83 -0
- hexdag/core/config/__init__.py +20 -0
- hexdag/core/config/loader.py +479 -0
- hexdag/core/config/models.py +150 -0
- hexdag/core/configurable.py +294 -0
- hexdag/core/context/__init__.py +37 -0
- hexdag/core/context/execution_context.py +378 -0
- hexdag/core/docs/__init__.py +26 -0
- hexdag/core/docs/extractors.py +678 -0
- hexdag/core/docs/generators.py +890 -0
- hexdag/core/docs/models.py +120 -0
- hexdag/core/domain/__init__.py +10 -0
- hexdag/core/domain/dag.py +1225 -0
- hexdag/core/exceptions.py +234 -0
- hexdag/core/expression_parser.py +569 -0
- hexdag/core/logging.py +449 -0
- hexdag/core/models/__init__.py +17 -0
- hexdag/core/models/base.py +138 -0
- hexdag/core/orchestration/__init__.py +46 -0
- hexdag/core/orchestration/body_executor.py +481 -0
- hexdag/core/orchestration/components/__init__.py +97 -0
- hexdag/core/orchestration/components/adapter_lifecycle_manager.py +113 -0
- hexdag/core/orchestration/components/checkpoint_manager.py +134 -0
- hexdag/core/orchestration/components/execution_coordinator.py +360 -0
- hexdag/core/orchestration/components/health_check_manager.py +176 -0
- hexdag/core/orchestration/components/input_mapper.py +143 -0
- hexdag/core/orchestration/components/lifecycle_manager.py +583 -0
- hexdag/core/orchestration/components/node_executor.py +377 -0
- hexdag/core/orchestration/components/secret_manager.py +202 -0
- hexdag/core/orchestration/components/wave_executor.py +158 -0
- hexdag/core/orchestration/constants.py +17 -0
- hexdag/core/orchestration/events/README.md +312 -0
- hexdag/core/orchestration/events/__init__.py +104 -0
- hexdag/core/orchestration/events/batching.py +330 -0
- hexdag/core/orchestration/events/decorators.py +139 -0
- hexdag/core/orchestration/events/events.py +573 -0
- hexdag/core/orchestration/events/observers/__init__.py +30 -0
- hexdag/core/orchestration/events/observers/core_observers.py +690 -0
- hexdag/core/orchestration/events/observers/models.py +111 -0
- hexdag/core/orchestration/events/taxonomy.py +269 -0
- hexdag/core/orchestration/hook_context.py +237 -0
- hexdag/core/orchestration/hooks.py +437 -0
- hexdag/core/orchestration/models.py +418 -0
- hexdag/core/orchestration/orchestrator.py +910 -0
- hexdag/core/orchestration/orchestrator_factory.py +275 -0
- hexdag/core/orchestration/port_wrappers.py +327 -0
- hexdag/core/orchestration/prompt/__init__.py +32 -0
- hexdag/core/orchestration/prompt/template.py +332 -0
- hexdag/core/pipeline_builder/__init__.py +21 -0
- hexdag/core/pipeline_builder/component_instantiator.py +386 -0
- hexdag/core/pipeline_builder/include_tag.py +265 -0
- hexdag/core/pipeline_builder/pipeline_config.py +133 -0
- hexdag/core/pipeline_builder/py_tag.py +223 -0
- hexdag/core/pipeline_builder/tag_discovery.py +268 -0
- hexdag/core/pipeline_builder/yaml_builder.py +1196 -0
- hexdag/core/pipeline_builder/yaml_validator.py +569 -0
- hexdag/core/ports/__init__.py +65 -0
- hexdag/core/ports/api_call.py +133 -0
- hexdag/core/ports/database.py +489 -0
- hexdag/core/ports/embedding.py +215 -0
- hexdag/core/ports/executor.py +237 -0
- hexdag/core/ports/file_storage.py +117 -0
- hexdag/core/ports/healthcheck.py +87 -0
- hexdag/core/ports/llm.py +551 -0
- hexdag/core/ports/memory.py +70 -0
- hexdag/core/ports/observer_manager.py +130 -0
- hexdag/core/ports/secret.py +145 -0
- hexdag/core/ports/tool_router.py +94 -0
- hexdag/core/ports_builder.py +623 -0
- hexdag/core/protocols.py +273 -0
- hexdag/core/resolver.py +304 -0
- hexdag/core/schema/__init__.py +9 -0
- hexdag/core/schema/generator.py +742 -0
- hexdag/core/secrets.py +242 -0
- hexdag/core/types.py +413 -0
- hexdag/core/utils/async_warnings.py +206 -0
- hexdag/core/utils/schema_conversion.py +78 -0
- hexdag/core/utils/sql_validation.py +86 -0
- hexdag/core/validation/secure_json.py +148 -0
- hexdag/core/yaml_macro.py +517 -0
- hexdag/mcp_server.py +3120 -0
- hexdag/studio/__init__.py +10 -0
- hexdag/studio/build_ui.py +92 -0
- hexdag/studio/server/__init__.py +1 -0
- hexdag/studio/server/main.py +100 -0
- hexdag/studio/server/routes/__init__.py +9 -0
- hexdag/studio/server/routes/execute.py +208 -0
- hexdag/studio/server/routes/export.py +558 -0
- hexdag/studio/server/routes/files.py +207 -0
- hexdag/studio/server/routes/plugins.py +419 -0
- hexdag/studio/server/routes/validate.py +220 -0
- hexdag/studio/ui/index.html +13 -0
- hexdag/studio/ui/package-lock.json +2992 -0
- hexdag/studio/ui/package.json +31 -0
- hexdag/studio/ui/postcss.config.js +6 -0
- hexdag/studio/ui/public/hexdag.svg +5 -0
- hexdag/studio/ui/src/App.tsx +251 -0
- hexdag/studio/ui/src/components/Canvas.tsx +408 -0
- hexdag/studio/ui/src/components/ContextMenu.tsx +187 -0
- hexdag/studio/ui/src/components/FileBrowser.tsx +123 -0
- hexdag/studio/ui/src/components/Header.tsx +181 -0
- hexdag/studio/ui/src/components/HexdagNode.tsx +193 -0
- hexdag/studio/ui/src/components/NodeInspector.tsx +512 -0
- hexdag/studio/ui/src/components/NodePalette.tsx +262 -0
- hexdag/studio/ui/src/components/NodePortsSection.tsx +403 -0
- hexdag/studio/ui/src/components/PluginManager.tsx +347 -0
- hexdag/studio/ui/src/components/PortsEditor.tsx +481 -0
- hexdag/studio/ui/src/components/PythonEditor.tsx +195 -0
- hexdag/studio/ui/src/components/ValidationPanel.tsx +105 -0
- hexdag/studio/ui/src/components/YamlEditor.tsx +196 -0
- hexdag/studio/ui/src/components/index.ts +8 -0
- hexdag/studio/ui/src/index.css +92 -0
- hexdag/studio/ui/src/main.tsx +10 -0
- hexdag/studio/ui/src/types/index.ts +123 -0
- hexdag/studio/ui/src/vite-env.d.ts +1 -0
- hexdag/studio/ui/tailwind.config.js +29 -0
- hexdag/studio/ui/tsconfig.json +37 -0
- hexdag/studio/ui/tsconfig.node.json +13 -0
- hexdag/studio/ui/vite.config.ts +35 -0
- hexdag/visualization/__init__.py +69 -0
- hexdag/visualization/dag_visualizer.py +1020 -0
- hexdag-0.5.0.dev1.dist-info/METADATA +369 -0
- hexdag-0.5.0.dev1.dist-info/RECORD +261 -0
- hexdag-0.5.0.dev1.dist-info/WHEEL +4 -0
- hexdag-0.5.0.dev1.dist-info/entry_points.txt +4 -0
- hexdag-0.5.0.dev1.dist-info/licenses/LICENSE +190 -0
- hexdag_plugins/.gitignore +43 -0
- hexdag_plugins/README.md +73 -0
- hexdag_plugins/__init__.py +1 -0
- hexdag_plugins/azure/LICENSE +21 -0
- hexdag_plugins/azure/README.md +414 -0
- hexdag_plugins/azure/__init__.py +21 -0
- hexdag_plugins/azure/azure_blob_adapter.py +450 -0
- hexdag_plugins/azure/azure_cosmos_adapter.py +383 -0
- hexdag_plugins/azure/azure_keyvault_adapter.py +314 -0
- hexdag_plugins/azure/azure_openai_adapter.py +415 -0
- hexdag_plugins/azure/pyproject.toml +107 -0
- hexdag_plugins/azure/tests/__init__.py +1 -0
- hexdag_plugins/azure/tests/test_azure_blob_adapter.py +350 -0
- hexdag_plugins/azure/tests/test_azure_cosmos_adapter.py +323 -0
- hexdag_plugins/azure/tests/test_azure_keyvault_adapter.py +330 -0
- hexdag_plugins/azure/tests/test_azure_openai_adapter.py +329 -0
- hexdag_plugins/hexdag_etl/README.md +168 -0
- hexdag_plugins/hexdag_etl/__init__.py +53 -0
- hexdag_plugins/hexdag_etl/examples/01_simple_pandas_transform.py +270 -0
- hexdag_plugins/hexdag_etl/examples/02_simple_pandas_only.py +149 -0
- hexdag_plugins/hexdag_etl/examples/03_file_io_pipeline.py +109 -0
- hexdag_plugins/hexdag_etl/examples/test_pandas_transform.py +84 -0
- hexdag_plugins/hexdag_etl/hexdag.toml +25 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/__init__.py +48 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/__init__.py +13 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/api_extract.py +230 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/base_node_factory.py +181 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/file_io.py +415 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/outlook.py +492 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/pandas_transform.py +563 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/sql_extract_load.py +112 -0
- hexdag_plugins/hexdag_etl/pyproject.toml +82 -0
- hexdag_plugins/hexdag_etl/test_transform.py +54 -0
- hexdag_plugins/hexdag_etl/tests/test_plugin_integration.py +62 -0
- hexdag_plugins/mysql_adapter/LICENSE +21 -0
- hexdag_plugins/mysql_adapter/README.md +224 -0
- hexdag_plugins/mysql_adapter/__init__.py +6 -0
- hexdag_plugins/mysql_adapter/mysql_adapter.py +408 -0
- hexdag_plugins/mysql_adapter/pyproject.toml +93 -0
- hexdag_plugins/mysql_adapter/tests/test_mysql_adapter.py +259 -0
- hexdag_plugins/storage/README.md +184 -0
- hexdag_plugins/storage/__init__.py +19 -0
- hexdag_plugins/storage/file/__init__.py +5 -0
- hexdag_plugins/storage/file/local.py +325 -0
- hexdag_plugins/storage/ports/__init__.py +5 -0
- hexdag_plugins/storage/ports/vector_store.py +236 -0
- hexdag_plugins/storage/sql/__init__.py +7 -0
- hexdag_plugins/storage/sql/base.py +187 -0
- hexdag_plugins/storage/sql/mysql.py +27 -0
- hexdag_plugins/storage/sql/postgresql.py +27 -0
- hexdag_plugins/storage/tests/__init__.py +1 -0
- hexdag_plugins/storage/tests/test_local_file_storage.py +161 -0
- hexdag_plugins/storage/tests/test_sql_adapters.py +212 -0
- hexdag_plugins/storage/vector/__init__.py +7 -0
- hexdag_plugins/storage/vector/chromadb.py +223 -0
- hexdag_plugins/storage/vector/in_memory.py +285 -0
- hexdag_plugins/storage/vector/pgvector.py +502 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""hexdag studio - Local-first visual editor for hexdag pipelines.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
hexdag studio ./pipelines/
|
|
5
|
+
|
|
6
|
+
Opens a browser-based editor that reads/writes YAML files directly.
|
|
7
|
+
No cloud, no accounts - just local files that work with git.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Build script for hexdag studio UI.
|
|
3
|
+
|
|
4
|
+
This script builds the React UI and bundles it into the dist folder
|
|
5
|
+
which is served by the FastAPI server.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python -m hexdag.studio.build_ui
|
|
9
|
+
|
|
10
|
+
Requirements:
|
|
11
|
+
- Node.js 18+
|
|
12
|
+
- npm or pnpm
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_ui() -> int:
|
|
21
|
+
"""Build the React UI."""
|
|
22
|
+
ui_dir = Path(__file__).parent / "ui"
|
|
23
|
+
|
|
24
|
+
if not ui_dir.exists():
|
|
25
|
+
print(f"Error: UI directory not found: {ui_dir}")
|
|
26
|
+
return 1
|
|
27
|
+
|
|
28
|
+
print("Building hexdag studio UI...")
|
|
29
|
+
print(f" Directory: {ui_dir}")
|
|
30
|
+
|
|
31
|
+
# Check for package.json
|
|
32
|
+
package_json = ui_dir / "package.json"
|
|
33
|
+
if not package_json.exists():
|
|
34
|
+
print(f"Error: package.json not found: {package_json}")
|
|
35
|
+
return 1
|
|
36
|
+
|
|
37
|
+
# Try npm first, then pnpm
|
|
38
|
+
npm_cmd = "npm"
|
|
39
|
+
try:
|
|
40
|
+
subprocess.run([npm_cmd, "--version"], capture_output=True, check=True)
|
|
41
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
42
|
+
npm_cmd = "pnpm"
|
|
43
|
+
try:
|
|
44
|
+
subprocess.run([npm_cmd, "--version"], capture_output=True, check=True)
|
|
45
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
46
|
+
print("Error: Neither npm nor pnpm found. Please install Node.js.")
|
|
47
|
+
return 1
|
|
48
|
+
|
|
49
|
+
print(f" Using: {npm_cmd}")
|
|
50
|
+
|
|
51
|
+
# Install dependencies
|
|
52
|
+
print("\n[1/2] Installing dependencies...")
|
|
53
|
+
result = subprocess.run(
|
|
54
|
+
[npm_cmd, "install"],
|
|
55
|
+
cwd=ui_dir,
|
|
56
|
+
capture_output=False,
|
|
57
|
+
)
|
|
58
|
+
if result.returncode != 0:
|
|
59
|
+
print("Error: Failed to install dependencies")
|
|
60
|
+
return result.returncode
|
|
61
|
+
|
|
62
|
+
# Build
|
|
63
|
+
print("\n[2/2] Building production bundle...")
|
|
64
|
+
result = subprocess.run(
|
|
65
|
+
[npm_cmd, "run", "build"],
|
|
66
|
+
cwd=ui_dir,
|
|
67
|
+
capture_output=False,
|
|
68
|
+
)
|
|
69
|
+
if result.returncode != 0:
|
|
70
|
+
print("Error: Failed to build")
|
|
71
|
+
return result.returncode
|
|
72
|
+
|
|
73
|
+
dist_dir = ui_dir / "dist"
|
|
74
|
+
if dist_dir.exists():
|
|
75
|
+
print("\nBuild complete!")
|
|
76
|
+
print(f" Output: {dist_dir}")
|
|
77
|
+
|
|
78
|
+
# List built files
|
|
79
|
+
print("\nBuilt files:")
|
|
80
|
+
for f in dist_dir.rglob("*"):
|
|
81
|
+
if f.is_file():
|
|
82
|
+
size = f.stat().st_size
|
|
83
|
+
rel_path = f.relative_to(dist_dir)
|
|
84
|
+
print(f" {rel_path} ({size:,} bytes)")
|
|
85
|
+
else:
|
|
86
|
+
print("Warning: dist directory not created")
|
|
87
|
+
|
|
88
|
+
return 0
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
sys.exit(build_ui())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""FastAPI server for hexdag studio."""
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""FastAPI server for hexdag studio.
|
|
2
|
+
|
|
3
|
+
Local-first visual editor for hexdag pipelines.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from collections.abc import AsyncGenerator
|
|
7
|
+
from contextlib import asynccontextmanager
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from fastapi import FastAPI
|
|
11
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
12
|
+
from fastapi.responses import FileResponse
|
|
13
|
+
from fastapi.staticfiles import StaticFiles
|
|
14
|
+
|
|
15
|
+
from hexdag.studio.server.routes import (
|
|
16
|
+
execute_router,
|
|
17
|
+
export_router,
|
|
18
|
+
files_router,
|
|
19
|
+
plugins_router,
|
|
20
|
+
validate_router,
|
|
21
|
+
)
|
|
22
|
+
from hexdag.studio.server.routes.files import set_workspace_root
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def create_app(workspace_path: Path) -> FastAPI:
|
|
26
|
+
"""Create FastAPI application for hexdag studio.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
workspace_path: Root directory for pipeline files
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
@asynccontextmanager
|
|
33
|
+
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
34
|
+
# Startup
|
|
35
|
+
set_workspace_root(workspace_path)
|
|
36
|
+
print(f"Workspace: {workspace_path}")
|
|
37
|
+
yield
|
|
38
|
+
# Shutdown
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
app = FastAPI(
|
|
42
|
+
title="hexdag studio",
|
|
43
|
+
description="Local-first visual editor for hexdag pipelines",
|
|
44
|
+
version="0.1.0",
|
|
45
|
+
lifespan=lifespan,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# CORS for local development
|
|
49
|
+
app.add_middleware(
|
|
50
|
+
CORSMiddleware,
|
|
51
|
+
allow_origins=["http://localhost:3141", "http://127.0.0.1:3141", "http://localhost:5173"],
|
|
52
|
+
allow_credentials=True,
|
|
53
|
+
allow_methods=["*"],
|
|
54
|
+
allow_headers=["*"],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# API routes
|
|
58
|
+
app.include_router(files_router, prefix="/api")
|
|
59
|
+
app.include_router(validate_router, prefix="/api")
|
|
60
|
+
app.include_router(execute_router, prefix="/api")
|
|
61
|
+
app.include_router(export_router, prefix="/api")
|
|
62
|
+
app.include_router(plugins_router, prefix="/api")
|
|
63
|
+
|
|
64
|
+
# Health check
|
|
65
|
+
@app.get("/api/health")
|
|
66
|
+
async def health() -> dict[str, str]:
|
|
67
|
+
return {"status": "ok", "workspace": str(workspace_path)}
|
|
68
|
+
|
|
69
|
+
# Serve static UI files
|
|
70
|
+
ui_path = Path(__file__).parent.parent / "ui" / "dist"
|
|
71
|
+
if ui_path.exists():
|
|
72
|
+
app.mount("/assets", StaticFiles(directory=ui_path / "assets"), name="assets")
|
|
73
|
+
|
|
74
|
+
@app.get("/")
|
|
75
|
+
async def serve_index() -> FileResponse:
|
|
76
|
+
return FileResponse(ui_path / "index.html")
|
|
77
|
+
|
|
78
|
+
@app.get("/{path:path}")
|
|
79
|
+
async def serve_spa(path: str) -> FileResponse:
|
|
80
|
+
# SPA fallback - serve index.html for all non-API routes
|
|
81
|
+
file_path = ui_path / path
|
|
82
|
+
if file_path.exists() and file_path.is_file():
|
|
83
|
+
return FileResponse(file_path)
|
|
84
|
+
return FileResponse(ui_path / "index.html")
|
|
85
|
+
|
|
86
|
+
return app
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def run_server(workspace_path: Path, host: str = "127.0.0.1", port: int = 3141) -> None:
|
|
90
|
+
"""Run the studio server.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
workspace_path: Root directory for pipeline files
|
|
94
|
+
host: Host to bind to
|
|
95
|
+
port: Port to bind to
|
|
96
|
+
"""
|
|
97
|
+
import uvicorn
|
|
98
|
+
|
|
99
|
+
app = create_app(workspace_path)
|
|
100
|
+
uvicorn.run(app, host=host, port=port, log_level="info")
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""API routes for hexdag studio."""
|
|
2
|
+
|
|
3
|
+
from hexdag.studio.server.routes.execute import router as execute_router
|
|
4
|
+
from hexdag.studio.server.routes.export import router as export_router
|
|
5
|
+
from hexdag.studio.server.routes.files import router as files_router
|
|
6
|
+
from hexdag.studio.server.routes.plugins import router as plugins_router
|
|
7
|
+
from hexdag.studio.server.routes.validate import router as validate_router
|
|
8
|
+
|
|
9
|
+
__all__ = ["files_router", "validate_router", "execute_router", "export_router", "plugins_router"]
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Execution API for hexdag studio.
|
|
2
|
+
|
|
3
|
+
Provides test execution of pipelines with mock adapters.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from fastapi import APIRouter
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from hexdag.studio.server.routes.files import get_workspace_root
|
|
14
|
+
|
|
15
|
+
router = APIRouter(prefix="/execute", tags=["execute"])
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ExecuteRequest(BaseModel):
|
|
19
|
+
"""Request to execute a pipeline."""
|
|
20
|
+
|
|
21
|
+
content: str
|
|
22
|
+
inputs: dict[str, Any] = {}
|
|
23
|
+
use_mocks: bool = True
|
|
24
|
+
timeout: float = 30.0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NodeResult(BaseModel):
|
|
28
|
+
"""Result from a single node execution."""
|
|
29
|
+
|
|
30
|
+
name: str
|
|
31
|
+
status: str # completed, failed, skipped
|
|
32
|
+
output: Any | None = None
|
|
33
|
+
error: str | None = None
|
|
34
|
+
duration_ms: float | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ExecuteResponse(BaseModel):
|
|
38
|
+
"""Pipeline execution result."""
|
|
39
|
+
|
|
40
|
+
success: bool
|
|
41
|
+
nodes: list[NodeResult]
|
|
42
|
+
final_output: Any | None = None
|
|
43
|
+
error: str | None = None
|
|
44
|
+
duration_ms: float
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@router.post("", response_model=ExecuteResponse)
|
|
48
|
+
async def execute_pipeline(request: ExecuteRequest) -> ExecuteResponse:
|
|
49
|
+
"""Execute a pipeline with optional mock adapters.
|
|
50
|
+
|
|
51
|
+
This is for testing/preview purposes. Production execution
|
|
52
|
+
should use the CLI or programmatic API.
|
|
53
|
+
"""
|
|
54
|
+
import time
|
|
55
|
+
|
|
56
|
+
start = time.perf_counter()
|
|
57
|
+
node_results: list[NodeResult] = []
|
|
58
|
+
|
|
59
|
+
# Add workspace root to sys.path so local modules (like tools/) can be imported
|
|
60
|
+
try:
|
|
61
|
+
workspace_root = get_workspace_root()
|
|
62
|
+
# Check if workspace has a parent with tools/adapters (project root pattern)
|
|
63
|
+
# e.g., workspace is "examples/raven/pipelines", project root is "examples/raven"
|
|
64
|
+
project_root = (
|
|
65
|
+
workspace_root.parent if workspace_root.name == "pipelines" else workspace_root
|
|
66
|
+
)
|
|
67
|
+
project_root_str = str(project_root)
|
|
68
|
+
if project_root_str not in sys.path:
|
|
69
|
+
sys.path.insert(0, project_root_str)
|
|
70
|
+
except Exception:
|
|
71
|
+
pass # Workspace not set, continue without modification
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
from hexdag.core.orchestration.orchestrator import Orchestrator
|
|
75
|
+
from hexdag.core.pipeline_builder import YamlPipelineBuilder
|
|
76
|
+
|
|
77
|
+
# Build pipeline
|
|
78
|
+
builder = YamlPipelineBuilder()
|
|
79
|
+
graph, config = builder.build_from_yaml_string(request.content)
|
|
80
|
+
|
|
81
|
+
# Create orchestrator with mocks if requested
|
|
82
|
+
if request.use_mocks:
|
|
83
|
+
# Use mock adapters for safe testing
|
|
84
|
+
from hexdag.builtin.adapters.memory import InMemoryMemory
|
|
85
|
+
from hexdag.builtin.adapters.mock import MockLLM
|
|
86
|
+
|
|
87
|
+
orchestrator = Orchestrator(
|
|
88
|
+
ports={
|
|
89
|
+
"llm": MockLLM(),
|
|
90
|
+
"memory": InMemoryMemory(),
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
else:
|
|
94
|
+
orchestrator = Orchestrator()
|
|
95
|
+
|
|
96
|
+
# Track node results via events
|
|
97
|
+
node_timings: dict[str, float] = {}
|
|
98
|
+
|
|
99
|
+
def on_node_started(event: Any) -> None:
|
|
100
|
+
node_timings[event.node_id] = time.perf_counter()
|
|
101
|
+
|
|
102
|
+
def on_node_completed(event: Any) -> None:
|
|
103
|
+
start_time = node_timings.get(event.node_id, start)
|
|
104
|
+
duration = (time.perf_counter() - start_time) * 1000
|
|
105
|
+
node_results.append(
|
|
106
|
+
NodeResult(
|
|
107
|
+
name=event.node_id,
|
|
108
|
+
status="completed",
|
|
109
|
+
output=event.output if hasattr(event, "output") else None,
|
|
110
|
+
duration_ms=duration,
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def on_node_failed(event: Any) -> None:
|
|
115
|
+
start_time = node_timings.get(event.node_id, start)
|
|
116
|
+
duration = (time.perf_counter() - start_time) * 1000
|
|
117
|
+
node_results.append(
|
|
118
|
+
NodeResult(
|
|
119
|
+
name=event.node_id,
|
|
120
|
+
status="failed",
|
|
121
|
+
error=str(event.error) if hasattr(event, "error") else "Unknown error",
|
|
122
|
+
duration_ms=duration,
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Subscribe to events
|
|
127
|
+
orchestrator.on("node_started", on_node_started)
|
|
128
|
+
orchestrator.on("node_completed", on_node_completed)
|
|
129
|
+
orchestrator.on("node_failed", on_node_failed)
|
|
130
|
+
|
|
131
|
+
# Execute with timeout
|
|
132
|
+
try:
|
|
133
|
+
result = await asyncio.wait_for(
|
|
134
|
+
orchestrator.run(graph, request.inputs),
|
|
135
|
+
timeout=request.timeout,
|
|
136
|
+
)
|
|
137
|
+
success = True
|
|
138
|
+
final_output = result
|
|
139
|
+
error = None
|
|
140
|
+
except TimeoutError:
|
|
141
|
+
success = False
|
|
142
|
+
final_output = None
|
|
143
|
+
error = f"Execution timed out after {request.timeout}s"
|
|
144
|
+
|
|
145
|
+
except Exception as e:
|
|
146
|
+
success = False
|
|
147
|
+
final_output = None
|
|
148
|
+
error = str(e)
|
|
149
|
+
|
|
150
|
+
duration = (time.perf_counter() - start) * 1000
|
|
151
|
+
|
|
152
|
+
return ExecuteResponse(
|
|
153
|
+
success=success,
|
|
154
|
+
nodes=node_results,
|
|
155
|
+
final_output=final_output,
|
|
156
|
+
error=error,
|
|
157
|
+
duration_ms=duration,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@router.post("/dry-run")
|
|
162
|
+
async def dry_run(request: ExecuteRequest) -> dict[str, Any]:
|
|
163
|
+
"""Analyze pipeline without executing.
|
|
164
|
+
|
|
165
|
+
Returns execution plan, dependency order, and estimated complexity.
|
|
166
|
+
"""
|
|
167
|
+
# Add workspace root to sys.path for local module imports
|
|
168
|
+
try:
|
|
169
|
+
workspace_root = get_workspace_root()
|
|
170
|
+
project_root = (
|
|
171
|
+
workspace_root.parent if workspace_root.name == "pipelines" else workspace_root
|
|
172
|
+
)
|
|
173
|
+
project_root_str = str(project_root)
|
|
174
|
+
if project_root_str not in sys.path:
|
|
175
|
+
sys.path.insert(0, project_root_str)
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
from hexdag.core.pipeline_builder import YamlPipelineBuilder
|
|
181
|
+
|
|
182
|
+
builder = YamlPipelineBuilder()
|
|
183
|
+
graph, config = builder.build_from_yaml_string(request.content)
|
|
184
|
+
|
|
185
|
+
# Get execution order
|
|
186
|
+
execution_order = list(graph.topological_sort())
|
|
187
|
+
|
|
188
|
+
# Analyze dependencies
|
|
189
|
+
dependency_map = {}
|
|
190
|
+
for node_id in execution_order:
|
|
191
|
+
node = graph.get_node(node_id)
|
|
192
|
+
dependency_map[node_id] = {
|
|
193
|
+
"dependencies": list(node.dependencies) if node else [],
|
|
194
|
+
"kind": node.node_type if node and hasattr(node, "node_type") else "unknown",
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
"valid": True,
|
|
199
|
+
"execution_order": execution_order,
|
|
200
|
+
"node_count": len(execution_order),
|
|
201
|
+
"dependency_map": dependency_map,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
except Exception as e:
|
|
205
|
+
return {
|
|
206
|
+
"valid": False,
|
|
207
|
+
"error": str(e),
|
|
208
|
+
}
|