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,634 @@
1
+ """OpenAI adapter for LLM interactions with embedding support."""
2
+
3
+ import json
4
+ import os
5
+ import time
6
+ from typing import Any, Literal
7
+
8
+ from openai import AsyncOpenAI
9
+
10
+ from hexdag.core.logging import get_logger
11
+ from hexdag.core.ports.healthcheck import HealthStatus
12
+ from hexdag.core.ports.llm import (
13
+ ImageInput,
14
+ LLMResponse,
15
+ MessageList,
16
+ SupportsEmbedding,
17
+ SupportsFunctionCalling,
18
+ SupportsGeneration,
19
+ SupportsVision,
20
+ ToolCall,
21
+ VisionMessage,
22
+ )
23
+ from hexdag.core.types import (
24
+ FrequencyPenalty,
25
+ PresencePenalty,
26
+ RetryCount,
27
+ Temperature02,
28
+ TimeoutSeconds,
29
+ TokenCount,
30
+ TopP,
31
+ )
32
+
33
+ logger = get_logger(__name__)
34
+
35
+
36
+ class OpenAIAdapter(SupportsGeneration, SupportsFunctionCalling, SupportsVision, SupportsEmbedding):
37
+ """Unified OpenAI implementation of the LLM port.
38
+
39
+ This adapter provides integration with OpenAI's models for:
40
+ - Text generation (GPT-4, GPT-3.5-turbo, etc.)
41
+ - Vision capabilities (GPT-4 Vision)
42
+ - Native tool/function calling
43
+ - Text embeddings (text-embedding-3-small, text-embedding-3-large)
44
+
45
+ It implements all optional protocols: SupportsGeneration, SupportsFunctionCalling,
46
+ SupportsVision, and SupportsEmbedding.
47
+
48
+ Secret Management
49
+ -----------------
50
+ API key resolution order:
51
+ 1. Explicit parameter: OpenAIAdapter(api_key="sk-...")
52
+ 2. Environment variable: OPENAI_API_KEY
53
+ 3. Memory port (orchestrator): secret:OPENAI_API_KEY
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ api_key: str | None = None,
59
+ model: str = "gpt-4o-mini",
60
+ temperature: Temperature02 = 0.7,
61
+ max_tokens: TokenCount | None = None,
62
+ response_format: Literal["text", "json_object"] = "text",
63
+ seed: int | None = None,
64
+ top_p: TopP = 1.0,
65
+ frequency_penalty: FrequencyPenalty = 0.0,
66
+ presence_penalty: PresencePenalty = 0.0,
67
+ system_prompt: str | None = None,
68
+ timeout: TimeoutSeconds = 60.0,
69
+ max_retries: RetryCount = 2,
70
+ embedding_model: str = "text-embedding-3-small",
71
+ embedding_dimensions: int | None = None,
72
+ **kwargs: Any, # ← For extra params like organization, base_url
73
+ ):
74
+ """Initialize OpenAI adapter.
75
+
76
+ Parameters
77
+ ----------
78
+ api_key : str | None
79
+ OpenAI API key (auto-resolved from OPENAI_API_KEY env var if not provided)
80
+ model : str, default="gpt-4o-mini"
81
+ OpenAI model to use
82
+ temperature : float, default=0.7
83
+ Sampling temperature (0-2)
84
+ max_tokens : int | None, default=None
85
+ Maximum tokens in response
86
+ response_format : Literal["text", "json_object"], default="text"
87
+ Output format
88
+ seed : int | None, default=None
89
+ Random seed for deterministic responses
90
+ top_p : float, default=1.0
91
+ Nucleus sampling parameter
92
+ frequency_penalty : float, default=0.0
93
+ Frequency penalty (-2.0 to 2.0)
94
+ presence_penalty : float, default=0.0
95
+ Presence penalty (-2.0 to 2.0)
96
+ system_prompt : str | None, default=None
97
+ System prompt to prepend to messages
98
+ timeout : float, default=60.0
99
+ Request timeout in seconds
100
+ max_retries : int, default=2
101
+ Maximum retry attempts
102
+ embedding_model : str, default="text-embedding-3-small"
103
+ OpenAI embedding model to use
104
+ embedding_dimensions : int | None, default=None
105
+ Embedding dimensionality (for text-embedding-3 models)
106
+ """
107
+ self.api_key = api_key or os.getenv("OPENAI_API_KEY")
108
+ if not self.api_key:
109
+ raise ValueError("api_key required (pass directly or set OPENAI_API_KEY)")
110
+ self.model = model
111
+ self.temperature = temperature
112
+ self.max_tokens = max_tokens
113
+ self.response_format = response_format
114
+ self.seed = seed
115
+ self.top_p = top_p
116
+ self.frequency_penalty = frequency_penalty
117
+ self.presence_penalty = presence_penalty
118
+ self.system_prompt = system_prompt
119
+ self.timeout = timeout
120
+ self.max_retries = max_retries
121
+ self.embedding_model = embedding_model
122
+ self.embedding_dimensions = embedding_dimensions
123
+ self._extra_kwargs = kwargs # Store extra params
124
+
125
+ client_kwargs: dict[str, Any] = {
126
+ "api_key": self.api_key,
127
+ "timeout": timeout,
128
+ "max_retries": max_retries,
129
+ }
130
+
131
+ if org := kwargs.get("organization"):
132
+ client_kwargs["organization"] = org
133
+ if base_url := kwargs.get("base_url"):
134
+ client_kwargs["base_url"] = base_url
135
+
136
+ self.client = AsyncOpenAI(**client_kwargs)
137
+
138
+ async def aresponse(self, messages: MessageList) -> str | None:
139
+ """Generate a response using OpenAI's modern API format.
140
+
141
+ Args
142
+ ----
143
+ messages: List of Message objects with role and content
144
+
145
+ Returns
146
+ -------
147
+ The generated response text, or None if failed
148
+ """
149
+ try:
150
+ openai_messages = [{"role": msg.role, "content": msg.content} for msg in messages]
151
+
152
+ if self.system_prompt and not any(msg["role"] == "system" for msg in openai_messages):
153
+ openai_messages.insert(0, {"role": "system", "content": self.system_prompt})
154
+
155
+ request_params: dict[str, Any] = {
156
+ "model": self.model,
157
+ "messages": openai_messages,
158
+ "temperature": self.temperature,
159
+ "top_p": self.top_p,
160
+ "frequency_penalty": self.frequency_penalty,
161
+ "presence_penalty": self.presence_penalty,
162
+ }
163
+
164
+ if self.max_tokens is not None:
165
+ request_params["max_tokens"] = self.max_tokens
166
+
167
+ if self.seed is not None:
168
+ request_params["seed"] = self.seed
169
+
170
+ # Stop sequences from extra kwargs
171
+ if stop_seq := self._extra_kwargs.get("stop_sequences"):
172
+ request_params["stop"] = stop_seq
173
+
174
+ if self.response_format == "json_object":
175
+ request_params["response_format"] = {"type": "json_object"}
176
+
177
+ # Make API call with modern format
178
+ response = await self.client.chat.completions.create(**request_params)
179
+
180
+ if response.choices and len(response.choices) > 0:
181
+ message = response.choices[0].message
182
+ if message and message.content:
183
+ content: str = str(message.content)
184
+
185
+ return content
186
+
187
+ logger.warning("No content in OpenAI response")
188
+ return None
189
+
190
+ except Exception as e:
191
+ logger.error(f"OpenAI API error: {e}", exc_info=True)
192
+ return None
193
+
194
+ async def aresponse_with_tools(
195
+ self,
196
+ messages: MessageList,
197
+ tools: list[dict[str, Any]],
198
+ tool_choice: str | dict[str, Any] = "auto",
199
+ ) -> LLMResponse:
200
+ """Generate response with native OpenAI tool calling.
201
+
202
+ Args
203
+ ----
204
+ messages: Conversation messages
205
+ tools: Tool definitions in OpenAI format
206
+ tool_choice: "auto", "none", "required", or specific tool dict
207
+
208
+ Returns
209
+ -------
210
+ LLMResponse
211
+ Response with content and tool calls
212
+
213
+ """
214
+ try:
215
+ openai_messages = [{"role": msg.role, "content": msg.content} for msg in messages]
216
+
217
+ if self.system_prompt and not any(msg["role"] == "system" for msg in openai_messages):
218
+ openai_messages.insert(0, {"role": "system", "content": self.system_prompt})
219
+
220
+ request_params: dict[str, Any] = {
221
+ "model": self.model,
222
+ "messages": openai_messages,
223
+ "temperature": self.temperature,
224
+ "top_p": self.top_p,
225
+ "frequency_penalty": self.frequency_penalty,
226
+ "presence_penalty": self.presence_penalty,
227
+ "tools": tools,
228
+ "tool_choice": tool_choice,
229
+ }
230
+
231
+ if self.max_tokens is not None:
232
+ request_params["max_tokens"] = self.max_tokens
233
+
234
+ if self.seed is not None:
235
+ request_params["seed"] = self.seed
236
+
237
+ # Stop sequences from extra kwargs
238
+ if stop_seq := self._extra_kwargs.get("stop_sequences"):
239
+ request_params["stop"] = stop_seq
240
+
241
+ if self.response_format == "json_object":
242
+ request_params["response_format"] = {"type": "json_object"}
243
+
244
+ # Make API call
245
+ response = await self.client.chat.completions.create(**request_params)
246
+
247
+ if not response.choices or len(response.choices) == 0:
248
+ logger.warning("No choices in OpenAI response")
249
+ return LLMResponse(content=None, tool_calls=None)
250
+
251
+ message = response.choices[0].message
252
+ finish_reason = response.choices[0].finish_reason
253
+
254
+ # Extract content
255
+ content = str(message.content) if message.content else None
256
+
257
+ # Extract tool calls
258
+ tool_calls = None
259
+ if message.tool_calls:
260
+ tool_calls = [
261
+ ToolCall(
262
+ id=tc.id,
263
+ name=tc.function.name,
264
+ arguments=json.loads(tc.function.arguments),
265
+ )
266
+ for tc in message.tool_calls
267
+ ]
268
+
269
+ return LLMResponse(content=content, tool_calls=tool_calls, finish_reason=finish_reason)
270
+
271
+ except Exception as e:
272
+ logger.error(f"OpenAI API error with tools: {e}", exc_info=True)
273
+ raise
274
+
275
+ async def aresponse_with_vision(
276
+ self,
277
+ messages: list[VisionMessage],
278
+ max_tokens: int | None = None,
279
+ ) -> str | None:
280
+ """Generate response from messages containing images and text.
281
+
282
+ Args
283
+ ----
284
+ messages: List of messages with optional image content
285
+ max_tokens: Optional maximum tokens in response
286
+
287
+ Returns
288
+ -------
289
+ Generated response text or None if failed
290
+
291
+ Examples
292
+ --------
293
+ Single image analysis::
294
+
295
+ messages = [
296
+ VisionMessage(
297
+ role="user",
298
+ content=[
299
+ {"type": "text", "text": "What's in this image?"},
300
+ {
301
+ "type": "image_url",
302
+ "image_url": {"url": "https://example.com/image.jpg"}
303
+ }
304
+ ]
305
+ )
306
+ ]
307
+ response = await adapter.aresponse_with_vision(messages)
308
+ """
309
+ try:
310
+ # Convert VisionMessage to OpenAI format
311
+ openai_messages: list[dict[str, Any]] = []
312
+ for msg in messages:
313
+ if isinstance(msg.content, str):
314
+ # Simple text message
315
+ openai_messages.append({"role": msg.role, "content": msg.content})
316
+ else:
317
+ # Multi-part content (text + images)
318
+ openai_messages.append({"role": msg.role, "content": msg.content})
319
+
320
+ if self.system_prompt and not any(msg["role"] == "system" for msg in openai_messages):
321
+ openai_messages.insert(0, {"role": "system", "content": self.system_prompt})
322
+
323
+ request_params: dict[str, Any] = {
324
+ "model": self.model,
325
+ "messages": openai_messages,
326
+ "temperature": self.temperature,
327
+ "top_p": self.top_p,
328
+ "frequency_penalty": self.frequency_penalty,
329
+ "presence_penalty": self.presence_penalty,
330
+ }
331
+
332
+ # Use provided max_tokens or default
333
+ if max_tokens is not None:
334
+ request_params["max_tokens"] = max_tokens
335
+ elif self.max_tokens is not None:
336
+ request_params["max_tokens"] = self.max_tokens
337
+
338
+ if self.seed is not None:
339
+ request_params["seed"] = self.seed
340
+
341
+ # Stop sequences from extra kwargs
342
+ if stop_seq := self._extra_kwargs.get("stop_sequences"):
343
+ request_params["stop"] = stop_seq
344
+
345
+ # Make API call
346
+ response = await self.client.chat.completions.create(**request_params)
347
+
348
+ if response.choices and len(response.choices) > 0:
349
+ message = response.choices[0].message
350
+ if message and message.content:
351
+ return str(message.content)
352
+
353
+ logger.warning("No content in OpenAI vision response")
354
+ return None
355
+
356
+ except Exception as e:
357
+ logger.error(f"OpenAI API error with vision: {e}", exc_info=True)
358
+ return None
359
+
360
+ async def aresponse_with_vision_and_tools(
361
+ self,
362
+ messages: list[VisionMessage],
363
+ tools: list[dict[str, Any]],
364
+ tool_choice: str | dict[str, Any] = "auto",
365
+ max_tokens: int | None = None,
366
+ ) -> LLMResponse:
367
+ """Generate response with both vision and tool calling capabilities.
368
+
369
+ Args
370
+ ----
371
+ messages: Messages with optional image content
372
+ tools: Tool definitions in OpenAI format
373
+ tool_choice: Tool selection strategy ("auto", "none", or specific tool)
374
+ max_tokens: Optional maximum tokens in response
375
+
376
+ Returns
377
+ -------
378
+ LLMResponse
379
+ Response with content and optional tool calls
380
+
381
+ Examples
382
+ --------
383
+ Image analysis with tool calls::
384
+
385
+ tools = [{
386
+ "type": "function",
387
+ "function": {
388
+ "name": "identify_product",
389
+ "description": "Look up product details",
390
+ "parameters": {
391
+ "type": "object",
392
+ "properties": {"product_name": {"type": "string"}},
393
+ "required": ["product_name"]
394
+ }
395
+ }
396
+ }]
397
+
398
+ messages = [
399
+ VisionMessage(
400
+ role="user",
401
+ content=[
402
+ {"type": "text", "text": "What product is this?"},
403
+ {"type": "image_url", "image_url": {"url": "product.jpg"}}
404
+ ]
405
+ )
406
+ ]
407
+
408
+ response = await adapter.aresponse_with_vision_and_tools(messages, tools)
409
+ """
410
+ try:
411
+ # Convert VisionMessage to OpenAI format
412
+ openai_messages: list[dict[str, Any]] = []
413
+ for msg in messages:
414
+ if isinstance(msg.content, str):
415
+ openai_messages.append({"role": msg.role, "content": msg.content})
416
+ else:
417
+ openai_messages.append({"role": msg.role, "content": msg.content})
418
+
419
+ if self.system_prompt and not any(msg["role"] == "system" for msg in openai_messages):
420
+ openai_messages.insert(0, {"role": "system", "content": self.system_prompt})
421
+
422
+ request_params: dict[str, Any] = {
423
+ "model": self.model,
424
+ "messages": openai_messages,
425
+ "temperature": self.temperature,
426
+ "top_p": self.top_p,
427
+ "frequency_penalty": self.frequency_penalty,
428
+ "presence_penalty": self.presence_penalty,
429
+ "tools": tools,
430
+ "tool_choice": tool_choice,
431
+ }
432
+
433
+ # Use provided max_tokens or default
434
+ if max_tokens is not None:
435
+ request_params["max_tokens"] = max_tokens
436
+ elif self.max_tokens is not None:
437
+ request_params["max_tokens"] = self.max_tokens
438
+
439
+ if self.seed is not None:
440
+ request_params["seed"] = self.seed
441
+
442
+ # Stop sequences from extra kwargs
443
+ if stop_seq := self._extra_kwargs.get("stop_sequences"):
444
+ request_params["stop"] = stop_seq
445
+
446
+ # Make API call
447
+ response = await self.client.chat.completions.create(**request_params)
448
+
449
+ if not response.choices or len(response.choices) == 0:
450
+ logger.warning("No choices in OpenAI vision+tools response")
451
+ return LLMResponse(content=None, tool_calls=None)
452
+
453
+ message = response.choices[0].message
454
+ finish_reason = response.choices[0].finish_reason
455
+
456
+ # Extract content
457
+ content = str(message.content) if message.content else None
458
+
459
+ # Extract tool calls
460
+ tool_calls_list = None
461
+ if message.tool_calls:
462
+ tool_calls_list = [
463
+ ToolCall(
464
+ id=tc.id,
465
+ name=tc.function.name,
466
+ arguments=json.loads(tc.function.arguments),
467
+ )
468
+ for tc in message.tool_calls
469
+ ]
470
+
471
+ return LLMResponse(
472
+ content=content, tool_calls=tool_calls_list, finish_reason=finish_reason
473
+ )
474
+
475
+ except Exception as e:
476
+ logger.error(f"OpenAI API error with vision+tools: {e}", exc_info=True)
477
+ raise
478
+
479
+ # ========== Embedding Methods (SupportsEmbedding Protocol) ==========
480
+
481
+ async def aembed(self, text: str) -> list[float]:
482
+ """Generate embedding vector for a single text input.
483
+
484
+ Args
485
+ ----
486
+ text: Text string to embed
487
+
488
+ Returns
489
+ -------
490
+ List of floats representing the embedding vector
491
+
492
+ Examples
493
+ --------
494
+ Single text embedding::
495
+
496
+ embedding = await adapter.aembed("Hello, world!")
497
+ # Returns: [0.123, -0.456, 0.789, ...]
498
+ """
499
+ try:
500
+ request_params: dict[str, Any] = {
501
+ "model": self.embedding_model,
502
+ "input": text,
503
+ }
504
+
505
+ if self.embedding_dimensions is not None:
506
+ request_params["dimensions"] = self.embedding_dimensions
507
+
508
+ response = await self.client.embeddings.create(**request_params)
509
+
510
+ if response.data and len(response.data) > 0:
511
+ embedding: list[float] = response.data[0].embedding
512
+ return embedding
513
+
514
+ logger.warning("No embedding data in OpenAI response")
515
+ return []
516
+
517
+ except Exception as e:
518
+ logger.error(f"OpenAI embedding API error: {e}", exc_info=True)
519
+ raise
520
+
521
+ async def aembed_batch(self, texts: list[str]) -> list[list[float]]:
522
+ """Generate embeddings for multiple texts efficiently.
523
+
524
+ Args
525
+ ----
526
+ texts: List of text strings to embed
527
+
528
+ Returns
529
+ -------
530
+ List of embedding vectors, one per input text
531
+
532
+ Examples
533
+ --------
534
+ Batch embedding::
535
+
536
+ texts = ["Hello", "World", "AI"]
537
+ embeddings = await adapter.aembed_batch(texts)
538
+ # Returns: [[0.1, 0.2, ...], [0.3, 0.4, ...], [0.5, 0.6, ...]]
539
+ """
540
+ try:
541
+ request_params: dict[str, Any] = {
542
+ "model": self.embedding_model,
543
+ "input": texts,
544
+ }
545
+
546
+ if self.embedding_dimensions is not None:
547
+ request_params["dimensions"] = self.embedding_dimensions
548
+
549
+ response = await self.client.embeddings.create(**request_params)
550
+
551
+ if response.data:
552
+ # Sort by index to ensure correct order
553
+ sorted_data = sorted(response.data, key=lambda x: x.index)
554
+ return [item.embedding for item in sorted_data]
555
+
556
+ logger.warning("No embedding data in OpenAI batch response")
557
+ return [[] for _ in texts]
558
+
559
+ except Exception as e:
560
+ logger.error(f"OpenAI batch embedding API error: {e}", exc_info=True)
561
+ raise
562
+
563
+ async def aembed_image(self, image: ImageInput) -> list[float]:
564
+ """Generate embedding vector for a single image input.
565
+
566
+ OpenAI does not currently support image embeddings via the embeddings API.
567
+
568
+ Args
569
+ ----
570
+ image: Image to embed
571
+
572
+ Raises
573
+ ------
574
+ NotImplementedError: OpenAI doesn't support image embeddings
575
+ """
576
+ raise NotImplementedError(
577
+ "OpenAI does not support image embeddings via the embeddings API. "
578
+ "For multimodal use cases, consider using vision models with aresponse_with_vision()."
579
+ )
580
+
581
+ async def aembed_image_batch(self, images: list[ImageInput]) -> list[list[float]]:
582
+ """Generate embeddings for multiple images efficiently.
583
+
584
+ OpenAI does not currently support image embeddings via the embeddings API.
585
+
586
+ Args
587
+ ----
588
+ images: List of images to embed
589
+
590
+ Raises
591
+ ------
592
+ NotImplementedError: OpenAI doesn't support image embeddings
593
+ """
594
+ raise NotImplementedError("OpenAI does not support image embeddings via the embeddings API")
595
+
596
+ # ========== Health Check ==========
597
+
598
+ async def ahealth_check(self) -> HealthStatus:
599
+ """Check OpenAI adapter health and connectivity.
600
+
601
+ Returns
602
+ -------
603
+ HealthStatus
604
+ Current health status with connectivity details
605
+ """
606
+ try:
607
+ # Try a minimal request to verify connectivity
608
+ start = time.time()
609
+
610
+ # Use a simple text generation request for health check
611
+ from hexdag.core.ports.llm import Message
612
+
613
+ test_messages = [Message(role="user", content="Hi")]
614
+ await self.aresponse(test_messages)
615
+
616
+ latency_ms = (time.time() - start) * 1000
617
+
618
+ return HealthStatus(
619
+ status="healthy",
620
+ adapter_name=f"OpenAI[{self.model}]",
621
+ latency_ms=latency_ms,
622
+ details={
623
+ "model": self.model,
624
+ "embedding_model": self.embedding_model,
625
+ },
626
+ )
627
+ except Exception as e:
628
+ logger.error(f"Health check failed: {e}")
629
+ return HealthStatus(
630
+ status="unhealthy",
631
+ adapter_name=f"OpenAI[{self.model}]",
632
+ latency_ms=0.0,
633
+ details={"error": str(e)},
634
+ )
@@ -0,0 +1,7 @@
1
+ """Secret management adapters."""
2
+
3
+ from hexdag.core.types import Secret
4
+
5
+ from .local_secret_adapter import LocalSecretAdapter
6
+
7
+ __all__ = ["LocalSecretAdapter", "Secret"]