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,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
|
+
)
|