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
hexdag/__init__.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Hex-DAG Agent Framework.
|
|
2
|
+
|
|
3
|
+
A modular, deterministic, and extensible architecture for orchestrating LLM-powered agents alongside
|
|
4
|
+
traditional code with YAML pipeline configuration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
# Version is defined in pyproject.toml and read dynamically
|
|
10
|
+
try:
|
|
11
|
+
from importlib.metadata import version
|
|
12
|
+
|
|
13
|
+
__version__ = version("hexdag")
|
|
14
|
+
except Exception:
|
|
15
|
+
__version__ = "0.0.0.dev0" # Fallback for development installs
|
|
16
|
+
|
|
17
|
+
# Core framework exports
|
|
18
|
+
from hexdag.builtin.nodes import (
|
|
19
|
+
ConditionalNode,
|
|
20
|
+
FunctionNode,
|
|
21
|
+
LLMNode,
|
|
22
|
+
LoopNode,
|
|
23
|
+
ReActAgentNode,
|
|
24
|
+
)
|
|
25
|
+
from hexdag.core.domain import DirectedGraph, NodeSpec
|
|
26
|
+
from hexdag.core.orchestration.orchestrator import Orchestrator
|
|
27
|
+
from hexdag.core.orchestration.prompt import FewShotPromptTemplate, PromptTemplate
|
|
28
|
+
|
|
29
|
+
# YAML Workflow Builder
|
|
30
|
+
from hexdag.core.pipeline_builder.yaml_builder import YamlPipelineBuilder
|
|
31
|
+
|
|
32
|
+
# Port interfaces
|
|
33
|
+
from hexdag.core.ports import LLM, APICall, DatabasePort, ToolRouter
|
|
34
|
+
|
|
35
|
+
# Import resolver for component resolution
|
|
36
|
+
from hexdag.core.resolver import resolve, resolve_function
|
|
37
|
+
|
|
38
|
+
# Define placeholders for lazy-loaded adapters to satisfy __all__ checking
|
|
39
|
+
# These will be replaced by __getattr__ when accessed
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from hexdag.builtin.adapters.memory import InMemoryMemory
|
|
42
|
+
from hexdag.builtin.adapters.mock import MockDatabaseAdapter, MockLLM
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Lazy loading for adapters and optional modules to avoid circular imports
|
|
46
|
+
def __getattr__(name: str) -> Any:
|
|
47
|
+
"""Lazy import for adapters and optional components.
|
|
48
|
+
|
|
49
|
+
Raises
|
|
50
|
+
------
|
|
51
|
+
ImportError
|
|
52
|
+
If visualization module is not available
|
|
53
|
+
AttributeError
|
|
54
|
+
If the requested attribute does not exist
|
|
55
|
+
"""
|
|
56
|
+
# Mock adapters
|
|
57
|
+
if name == "MockLLM":
|
|
58
|
+
from hexdag.builtin.adapters.mock import MockLLM as _MockLLM
|
|
59
|
+
|
|
60
|
+
return _MockLLM
|
|
61
|
+
if name == "MockDatabaseAdapter":
|
|
62
|
+
from hexdag.builtin.adapters.mock import MockDatabaseAdapter as _MockDatabaseAdapter
|
|
63
|
+
|
|
64
|
+
return _MockDatabaseAdapter
|
|
65
|
+
if name == "MockToolRouter":
|
|
66
|
+
from hexdag.builtin.adapters.mock import MockToolRouter as _MockToolRouter
|
|
67
|
+
|
|
68
|
+
return _MockToolRouter
|
|
69
|
+
|
|
70
|
+
# Visualization components (optional)
|
|
71
|
+
if name == "DAGVisualizer":
|
|
72
|
+
try:
|
|
73
|
+
from hexdag.visualization import DAGVisualizer as _DAGVisualizer
|
|
74
|
+
|
|
75
|
+
return _DAGVisualizer
|
|
76
|
+
except ImportError as e:
|
|
77
|
+
raise ImportError(
|
|
78
|
+
"Visualization module not available. Install with:\n"
|
|
79
|
+
" pip install hexdag[viz]\n"
|
|
80
|
+
" or\n"
|
|
81
|
+
" uv pip install hexdag[viz]"
|
|
82
|
+
) from e
|
|
83
|
+
|
|
84
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
__all__ = [
|
|
88
|
+
# Version
|
|
89
|
+
"__version__",
|
|
90
|
+
# Module Resolution
|
|
91
|
+
"resolve",
|
|
92
|
+
"resolve_function",
|
|
93
|
+
# Core Framework - DAG Building and Execution
|
|
94
|
+
"Orchestrator",
|
|
95
|
+
"DirectedGraph",
|
|
96
|
+
"NodeSpec",
|
|
97
|
+
"YamlPipelineBuilder",
|
|
98
|
+
# Node Factories
|
|
99
|
+
"FunctionNode",
|
|
100
|
+
"LLMNode",
|
|
101
|
+
"ReActAgentNode",
|
|
102
|
+
"LoopNode",
|
|
103
|
+
"ConditionalNode",
|
|
104
|
+
# Templating System
|
|
105
|
+
"PromptTemplate",
|
|
106
|
+
"FewShotPromptTemplate",
|
|
107
|
+
# Port Interfaces
|
|
108
|
+
"LLM",
|
|
109
|
+
"APICall",
|
|
110
|
+
"ToolRouter",
|
|
111
|
+
"DatabasePort",
|
|
112
|
+
# Testing and Development Adapters
|
|
113
|
+
"InMemoryMemory",
|
|
114
|
+
"MockLLM",
|
|
115
|
+
"MockDatabaseAdapter",
|
|
116
|
+
]
|
hexdag/__main__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Entry point for running hexDAG as a module.
|
|
2
|
+
|
|
3
|
+
Supports both CLI and MCP server modes:
|
|
4
|
+
- python -m hexdag [CLI command]
|
|
5
|
+
- python -m hexdag --mcp (starts MCP server)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main() -> None:
|
|
14
|
+
"""Main entry point for module execution."""
|
|
15
|
+
# Check if --mcp flag is present
|
|
16
|
+
if "--mcp" in sys.argv:
|
|
17
|
+
# Remove --mcp flag and run MCP server
|
|
18
|
+
sys.argv.remove("--mcp")
|
|
19
|
+
from hexdag.mcp_server import mcp
|
|
20
|
+
|
|
21
|
+
mcp.run()
|
|
22
|
+
else:
|
|
23
|
+
# Run regular CLI
|
|
24
|
+
from hexdag.cli import main as cli_main
|
|
25
|
+
|
|
26
|
+
cli_main()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
main()
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""Local in-process executor adapter.
|
|
2
|
+
|
|
3
|
+
This executor runs nodes in the same process using asyncio, providing
|
|
4
|
+
the same behavior as the built-in orchestrator execution but through
|
|
5
|
+
the ExecutorPort interface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import time
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from hexdag.core.context import get_port
|
|
13
|
+
from hexdag.core.logging import get_logger
|
|
14
|
+
from hexdag.core.orchestration.components import (
|
|
15
|
+
ExecutionCoordinator,
|
|
16
|
+
NodeExecutionError,
|
|
17
|
+
NodeExecutor,
|
|
18
|
+
)
|
|
19
|
+
from hexdag.core.orchestration.constants import (
|
|
20
|
+
EXECUTOR_CONTEXT_GRAPH,
|
|
21
|
+
EXECUTOR_CONTEXT_INITIAL_INPUT,
|
|
22
|
+
EXECUTOR_CONTEXT_NODE_RESULTS,
|
|
23
|
+
)
|
|
24
|
+
from hexdag.core.orchestration.hook_context import PipelineStatus
|
|
25
|
+
from hexdag.core.orchestration.models import NodeExecutionContext
|
|
26
|
+
from hexdag.core.ports.executor import (
|
|
27
|
+
ExecutionResult,
|
|
28
|
+
ExecutionTask,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from hexdag.core.domain.dag import DirectedGraph, NodeSpec
|
|
33
|
+
|
|
34
|
+
logger = get_logger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _calculate_duration_ms(start_time: float) -> float:
|
|
38
|
+
"""Calculate duration in milliseconds from start time.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
start_time : float
|
|
43
|
+
Start time from time.time()
|
|
44
|
+
|
|
45
|
+
Returns
|
|
46
|
+
-------
|
|
47
|
+
float
|
|
48
|
+
Duration in milliseconds
|
|
49
|
+
"""
|
|
50
|
+
return (time.time() - start_time) * 1000
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Local:
|
|
54
|
+
"""Configuration for LocalExecutor.
|
|
55
|
+
|
|
56
|
+
Attributes
|
|
57
|
+
----------
|
|
58
|
+
max_concurrent_nodes : int
|
|
59
|
+
Maximum number of nodes to execute concurrently (default: 10)
|
|
60
|
+
strict_validation : bool
|
|
61
|
+
If True, raise errors on validation failure (default: False)
|
|
62
|
+
default_node_timeout : float | None
|
|
63
|
+
Default timeout in seconds for each node (default: None, no timeout)
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
max_concurrent_nodes: int = 10
|
|
67
|
+
strict_validation: bool = False
|
|
68
|
+
default_node_timeout: float | None = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class LocalExecutor:
|
|
72
|
+
"""Local in-process executor using asyncio.
|
|
73
|
+
|
|
74
|
+
This executor wraps the core NodeExecutor and WaveExecutor to provide
|
|
75
|
+
the same in-process execution behavior but through the ExecutorPort
|
|
76
|
+
interface. It's useful for:
|
|
77
|
+
|
|
78
|
+
1. Testing the executor abstraction
|
|
79
|
+
2. Providing a consistent interface across execution strategies
|
|
80
|
+
3. Serving as a reference implementation for other executors
|
|
81
|
+
|
|
82
|
+
The LocalExecutor maintains the same features as built-in execution:
|
|
83
|
+
- Async/sync function support
|
|
84
|
+
- Validation and type checking
|
|
85
|
+
- Timeout handling
|
|
86
|
+
- Event emission
|
|
87
|
+
- Policy evaluation
|
|
88
|
+
|
|
89
|
+
Examples
|
|
90
|
+
--------
|
|
91
|
+
Basic usage::
|
|
92
|
+
|
|
93
|
+
executor = LocalExecutor(max_concurrent_nodes=5)
|
|
94
|
+
orchestrator = Orchestrator(executor=executor)
|
|
95
|
+
results = await orchestrator.run(graph, input_data)
|
|
96
|
+
|
|
97
|
+
With validation and timeout::
|
|
98
|
+
|
|
99
|
+
executor = LocalExecutor(
|
|
100
|
+
strict_validation=True,
|
|
101
|
+
default_node_timeout=30.0
|
|
102
|
+
)
|
|
103
|
+
orchestrator = Orchestrator(executor=executor)
|
|
104
|
+
|
|
105
|
+
From YAML::
|
|
106
|
+
|
|
107
|
+
executor:
|
|
108
|
+
type: local
|
|
109
|
+
namespace: core
|
|
110
|
+
params:
|
|
111
|
+
max_concurrent_nodes: 20
|
|
112
|
+
strict_validation: true
|
|
113
|
+
default_node_timeout: 60.0
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self, strict_validation: bool = True, default_node_timeout: float = 60.0, **kwargs: Any
|
|
118
|
+
) -> None:
|
|
119
|
+
"""Initialize local executor.
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
strict_validation : bool, default=True
|
|
124
|
+
Enable strict validation
|
|
125
|
+
default_node_timeout : float, default=60.0
|
|
126
|
+
Default timeout for nodes in seconds
|
|
127
|
+
"""
|
|
128
|
+
self.strict_validation = strict_validation
|
|
129
|
+
self.default_node_timeout = default_node_timeout
|
|
130
|
+
self.max_concurrent_nodes = kwargs.get("max_concurrent_nodes", 10)
|
|
131
|
+
|
|
132
|
+
# Core execution components
|
|
133
|
+
self._node_executor = NodeExecutor(
|
|
134
|
+
strict_validation=strict_validation,
|
|
135
|
+
default_node_timeout=default_node_timeout,
|
|
136
|
+
)
|
|
137
|
+
self._execution_coordinator = ExecutionCoordinator()
|
|
138
|
+
|
|
139
|
+
# State for tracking
|
|
140
|
+
self._initialized = False
|
|
141
|
+
|
|
142
|
+
async def aexecute_node(self, task: ExecutionTask) -> ExecutionResult:
|
|
143
|
+
"""Execute a single node in-process.
|
|
144
|
+
|
|
145
|
+
Parameters
|
|
146
|
+
----------
|
|
147
|
+
task : ExecutionTask
|
|
148
|
+
Task containing node information and input data
|
|
149
|
+
|
|
150
|
+
Returns
|
|
151
|
+
-------
|
|
152
|
+
ExecutionResult
|
|
153
|
+
Result of the execution with output or error information
|
|
154
|
+
|
|
155
|
+
Raises
|
|
156
|
+
------
|
|
157
|
+
RuntimeError
|
|
158
|
+
If executor not initialized via asetup()
|
|
159
|
+
|
|
160
|
+
Notes
|
|
161
|
+
-----
|
|
162
|
+
This method expects that the orchestrator has set up the execution
|
|
163
|
+
context with the graph and ports. The task.context_data should contain
|
|
164
|
+
the necessary execution context information.
|
|
165
|
+
"""
|
|
166
|
+
if not self._initialized:
|
|
167
|
+
raise RuntimeError("LocalExecutor not initialized - call asetup() first")
|
|
168
|
+
|
|
169
|
+
start_time = time.time()
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
# Note: Uses ContextVar pattern consistent with rest of codebase
|
|
173
|
+
graph: DirectedGraph = get_port(EXECUTOR_CONTEXT_GRAPH)
|
|
174
|
+
node_results: dict[str, Any] = get_port(EXECUTOR_CONTEXT_NODE_RESULTS)
|
|
175
|
+
initial_input: Any = get_port(EXECUTOR_CONTEXT_INITIAL_INPUT)
|
|
176
|
+
|
|
177
|
+
if graph is None:
|
|
178
|
+
raise RuntimeError(
|
|
179
|
+
"LocalExecutor requires execution context with graph. "
|
|
180
|
+
"Ensure orchestrator has set up context properly."
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if task.node_name not in graph.nodes:
|
|
184
|
+
return ExecutionResult(
|
|
185
|
+
node_name=task.node_name,
|
|
186
|
+
status=PipelineStatus.FAILED,
|
|
187
|
+
error=f"Node '{task.node_name}' not found in graph",
|
|
188
|
+
error_type="KeyError",
|
|
189
|
+
duration_ms=_calculate_duration_ms(start_time),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
node_spec: NodeSpec = graph.nodes[task.node_name]
|
|
193
|
+
|
|
194
|
+
# Prepare input using ExecutionCoordinator
|
|
195
|
+
node_input = self._execution_coordinator.prepare_node_input(
|
|
196
|
+
node_spec, node_results, initial_input
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
execution_context = NodeExecutionContext(
|
|
200
|
+
dag_id=task.context_data.get("dag_id", "unnamed"),
|
|
201
|
+
node_id=task.node_name,
|
|
202
|
+
wave_index=task.wave_index,
|
|
203
|
+
attempt=task.context_data.get("attempt", 1),
|
|
204
|
+
metadata=task.context_data,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Execute using NodeExecutor
|
|
208
|
+
output = await self._node_executor.execute_node(
|
|
209
|
+
node_name=task.node_name,
|
|
210
|
+
node_spec=node_spec,
|
|
211
|
+
node_input=node_input,
|
|
212
|
+
context=execution_context,
|
|
213
|
+
coordinator=self._execution_coordinator,
|
|
214
|
+
wave_index=task.wave_index,
|
|
215
|
+
validate=task.should_validate,
|
|
216
|
+
**task.params,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
return ExecutionResult(
|
|
220
|
+
node_name=task.node_name,
|
|
221
|
+
output=output,
|
|
222
|
+
status=PipelineStatus.SUCCESS,
|
|
223
|
+
duration_ms=_calculate_duration_ms(start_time),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
except NodeExecutionError:
|
|
227
|
+
# NodeExecutionError should propagate directly (orchestrator will handle)
|
|
228
|
+
raise
|
|
229
|
+
except (ValueError, TypeError, KeyError, AttributeError, RuntimeError) as e:
|
|
230
|
+
# Catch expected execution errors (validation, type issues, missing data)
|
|
231
|
+
logger.error(f"LocalExecutor: Node '{task.node_name}' failed: {e}")
|
|
232
|
+
return ExecutionResult(
|
|
233
|
+
node_name=task.node_name,
|
|
234
|
+
status=PipelineStatus.FAILED,
|
|
235
|
+
error=str(e),
|
|
236
|
+
error_type=type(e).__name__,
|
|
237
|
+
duration_ms=_calculate_duration_ms(start_time),
|
|
238
|
+
)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
# Catch unexpected errors but log with higher severity
|
|
241
|
+
logger.exception(f"LocalExecutor: Unexpected error in node '{task.node_name}': {e}")
|
|
242
|
+
return ExecutionResult(
|
|
243
|
+
node_name=task.node_name,
|
|
244
|
+
status=PipelineStatus.FAILED,
|
|
245
|
+
error=str(e),
|
|
246
|
+
error_type=type(e).__name__,
|
|
247
|
+
duration_ms=_calculate_duration_ms(start_time),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
async def aexecute_wave(self, tasks: list[ExecutionTask]) -> dict[str, ExecutionResult]:
|
|
251
|
+
"""Execute multiple nodes concurrently within a wave.
|
|
252
|
+
|
|
253
|
+
Parameters
|
|
254
|
+
----------
|
|
255
|
+
tasks : list[ExecutionTask]
|
|
256
|
+
List of tasks to execute in parallel
|
|
257
|
+
|
|
258
|
+
Returns
|
|
259
|
+
-------
|
|
260
|
+
dict[str, ExecutionResult]
|
|
261
|
+
Map of node_name -> execution result
|
|
262
|
+
|
|
263
|
+
Raises
|
|
264
|
+
------
|
|
265
|
+
RuntimeError
|
|
266
|
+
If executor not initialized via asetup()
|
|
267
|
+
|
|
268
|
+
Notes
|
|
269
|
+
-----
|
|
270
|
+
This method uses asyncio.gather to execute all tasks concurrently,
|
|
271
|
+
respecting the max_concurrent_nodes limit via semaphore.
|
|
272
|
+
"""
|
|
273
|
+
if not self._initialized:
|
|
274
|
+
raise RuntimeError("LocalExecutor not initialized - call asetup() first")
|
|
275
|
+
|
|
276
|
+
semaphore = asyncio.Semaphore(self.max_concurrent_nodes)
|
|
277
|
+
|
|
278
|
+
async def execute_with_limit(task: ExecutionTask) -> ExecutionResult:
|
|
279
|
+
"""Execute task with semaphore-based concurrency control."""
|
|
280
|
+
async with semaphore:
|
|
281
|
+
return await self.aexecute_node(task)
|
|
282
|
+
|
|
283
|
+
# Execute all tasks concurrently with semaphore limit
|
|
284
|
+
results_list = await asyncio.gather(
|
|
285
|
+
*[execute_with_limit(task) for task in tasks],
|
|
286
|
+
return_exceptions=True,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
results = {}
|
|
290
|
+
for result in results_list:
|
|
291
|
+
if isinstance(result, ExecutionResult):
|
|
292
|
+
results[result.node_name] = result
|
|
293
|
+
elif isinstance(result, Exception):
|
|
294
|
+
# Can happen if task is cancelled during gather()
|
|
295
|
+
logger.error(f"Exception during wave execution: {result}")
|
|
296
|
+
raise result
|
|
297
|
+
|
|
298
|
+
return results
|
|
299
|
+
|
|
300
|
+
async def asetup(self) -> None:
|
|
301
|
+
"""Initialize executor resources.
|
|
302
|
+
|
|
303
|
+
For LocalExecutor, this is a no-op as all resources are
|
|
304
|
+
in-process and don't require setup.
|
|
305
|
+
"""
|
|
306
|
+
self._initialized = True
|
|
307
|
+
logger.debug("LocalExecutor initialized")
|
|
308
|
+
|
|
309
|
+
async def aclose(self) -> None:
|
|
310
|
+
"""Cleanup executor resources.
|
|
311
|
+
|
|
312
|
+
For LocalExecutor, this is a no-op as all resources are
|
|
313
|
+
in-process and don't require cleanup.
|
|
314
|
+
"""
|
|
315
|
+
self._initialized = False
|
|
316
|
+
logger.debug("LocalExecutor closed")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Initalize the hexdag adapter package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .unified_tool_router import UnifiedToolRouter # noqa: F401
|
|
10
|
+
|
|
11
|
+
_LAZY_MAP: dict[str, tuple[str, str]] = {
|
|
12
|
+
"UnifiedToolRouter": (
|
|
13
|
+
"hexdag.adapters.unified_tool_router",
|
|
14
|
+
"UnifiedToolRouter",
|
|
15
|
+
),
|
|
16
|
+
# Backward compatibility alias
|
|
17
|
+
"FunctionBasedToolRouter": (
|
|
18
|
+
"hexdag.adapters.unified_tool_router",
|
|
19
|
+
"UnifiedToolRouter",
|
|
20
|
+
),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
__all__: tuple[str, ...] = tuple(_LAZY_MAP)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def __getattr__(name: str) -> Any:
|
|
27
|
+
"""Lazy import for adapters.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
Any
|
|
32
|
+
The imported adapter class or module.
|
|
33
|
+
|
|
34
|
+
Raises
|
|
35
|
+
------
|
|
36
|
+
AttributeError
|
|
37
|
+
If the requested name is not found in the lazy import map.
|
|
38
|
+
"""
|
|
39
|
+
if name in _LAZY_MAP:
|
|
40
|
+
module_name, attr = _LAZY_MAP[name]
|
|
41
|
+
module = importlib.import_module(module_name)
|
|
42
|
+
value = getattr(module, attr)
|
|
43
|
+
|
|
44
|
+
# Register the port dynamically if necessary
|
|
45
|
+
if hasattr(value, "register_port"):
|
|
46
|
+
value.register_port()
|
|
47
|
+
|
|
48
|
+
globals()[name] = value # Cache the imported module in the globals for future access
|
|
49
|
+
return value
|
|
50
|
+
|
|
51
|
+
raise AttributeError(f"module {__name__} has no attribute {name}")
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Anthropic adapter for LLM interactions."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from anthropic import AsyncAnthropic
|
|
7
|
+
|
|
8
|
+
from hexdag.core.logging import get_logger
|
|
9
|
+
from hexdag.core.ports.llm import MessageList
|
|
10
|
+
from hexdag.core.types import (
|
|
11
|
+
PositiveInt,
|
|
12
|
+
RetryCount,
|
|
13
|
+
Temperature01,
|
|
14
|
+
TimeoutSeconds,
|
|
15
|
+
TopP,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = get_logger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AnthropicAdapter:
|
|
22
|
+
"""Anthropic implementation of the LLM port.
|
|
23
|
+
|
|
24
|
+
This adapter provides integration with Anthropic's Claude models through
|
|
25
|
+
their API. It supports async operations and handles message conversion
|
|
26
|
+
between hexDAG's format and Anthropic's format.
|
|
27
|
+
|
|
28
|
+
Secret Management
|
|
29
|
+
-----------------
|
|
30
|
+
API key resolution order:
|
|
31
|
+
1. Explicit parameter: AnthropicAdapter(api_key="sk-...")
|
|
32
|
+
2. Environment variable: ANTHROPIC_API_KEY
|
|
33
|
+
3. Memory port (orchestrator): secret:ANTHROPIC_API_KEY
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
api_key: str | None = None,
|
|
39
|
+
model: str = "claude-3-5-sonnet-20241022",
|
|
40
|
+
temperature: Temperature01 = 0.7,
|
|
41
|
+
max_tokens: PositiveInt = 4096,
|
|
42
|
+
top_p: TopP = 1.0,
|
|
43
|
+
top_k: PositiveInt | None = None,
|
|
44
|
+
system_prompt: str | None = None,
|
|
45
|
+
timeout: TimeoutSeconds = 60.0,
|
|
46
|
+
max_retries: RetryCount = 2,
|
|
47
|
+
**kwargs: Any, # ← For extra params like base_url
|
|
48
|
+
):
|
|
49
|
+
"""Initialize Anthropic adapter.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
api_key : str | None
|
|
54
|
+
Anthropic API key (auto-resolved from ANTHROPIC_API_KEY env var if not provided)
|
|
55
|
+
model : str, default="claude-3-5-sonnet-20241022"
|
|
56
|
+
Claude model to use
|
|
57
|
+
temperature : float, default=0.7
|
|
58
|
+
Sampling temperature (0-1)
|
|
59
|
+
max_tokens : int, default=4096
|
|
60
|
+
Maximum tokens in response
|
|
61
|
+
top_p : float, default=1.0
|
|
62
|
+
Nucleus sampling parameter
|
|
63
|
+
top_k : int | None, default=None
|
|
64
|
+
Top-k sampling parameter
|
|
65
|
+
system_prompt : str | None, default=None
|
|
66
|
+
System prompt to use
|
|
67
|
+
timeout : float, default=60.0
|
|
68
|
+
Request timeout in seconds
|
|
69
|
+
max_retries : int, default=2
|
|
70
|
+
Maximum retry attempts
|
|
71
|
+
"""
|
|
72
|
+
self.api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
|
|
73
|
+
if not self.api_key:
|
|
74
|
+
raise ValueError("api_key required (pass directly or set ANTHROPIC_API_KEY)")
|
|
75
|
+
self.model = model
|
|
76
|
+
self.temperature = temperature
|
|
77
|
+
self.max_tokens = max_tokens
|
|
78
|
+
self.top_p = top_p
|
|
79
|
+
self.top_k = top_k
|
|
80
|
+
self.system_prompt = system_prompt
|
|
81
|
+
self.timeout = timeout
|
|
82
|
+
self.max_retries = max_retries
|
|
83
|
+
self._extra_kwargs = kwargs # Store extra params
|
|
84
|
+
|
|
85
|
+
client_kwargs: dict[str, Any] = {
|
|
86
|
+
"api_key": self.api_key,
|
|
87
|
+
"timeout": timeout,
|
|
88
|
+
"max_retries": max_retries,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if base_url := kwargs.get("base_url"):
|
|
92
|
+
client_kwargs["base_url"] = base_url
|
|
93
|
+
|
|
94
|
+
self.client = AsyncAnthropic(**client_kwargs)
|
|
95
|
+
|
|
96
|
+
async def aresponse(self, messages: MessageList) -> str | None:
|
|
97
|
+
"""Generate a response using Anthropic's API.
|
|
98
|
+
|
|
99
|
+
Args
|
|
100
|
+
----
|
|
101
|
+
messages: List of Message objects with role and content
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
The generated response text, or None if failed
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
# Anthropic requires system messages to be separate
|
|
109
|
+
system_message = self.system_prompt
|
|
110
|
+
anthropic_messages = []
|
|
111
|
+
|
|
112
|
+
for msg in messages:
|
|
113
|
+
if msg.role == "system":
|
|
114
|
+
# Concatenate multiple system messages if present
|
|
115
|
+
if system_message:
|
|
116
|
+
system_message += "\n" + msg.content
|
|
117
|
+
else:
|
|
118
|
+
system_message = msg.content
|
|
119
|
+
else:
|
|
120
|
+
anthropic_messages.append({"role": msg.role, "content": msg.content})
|
|
121
|
+
|
|
122
|
+
request_params: dict[str, Any] = {
|
|
123
|
+
"model": self.model,
|
|
124
|
+
"messages": anthropic_messages,
|
|
125
|
+
"temperature": self.temperature,
|
|
126
|
+
"max_tokens": self.max_tokens,
|
|
127
|
+
"top_p": self.top_p,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if system_message is not None:
|
|
131
|
+
request_params["system"] = system_message
|
|
132
|
+
|
|
133
|
+
if self.top_k is not None:
|
|
134
|
+
request_params["top_k"] = self.top_k
|
|
135
|
+
|
|
136
|
+
if stop_sequences := self._extra_kwargs.get("stop_sequences"):
|
|
137
|
+
request_params["stop_sequences"] = stop_sequences
|
|
138
|
+
|
|
139
|
+
response = await self.client.messages.create(**request_params)
|
|
140
|
+
|
|
141
|
+
if response.content and len(response.content) > 0:
|
|
142
|
+
first_content = response.content[0]
|
|
143
|
+
if hasattr(first_content, "text"):
|
|
144
|
+
return str(first_content.text)
|
|
145
|
+
|
|
146
|
+
logger.warning("No text content in Anthropic response")
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.error(f"Anthropic API error: {e}", exc_info=True)
|
|
151
|
+
return None
|