kaxil commented on code in PR #70441:
URL: https://github.com/apache/airflow/pull/70441#discussion_r3661375927


##########
providers/common/ai/src/airflow/providers/common/ai/hooks/langchain.py:
##########
@@ -171,3 +170,53 @@ def get_embedding_model(self) -> Embeddings:
             kind="embedding",
         )
         return init_embeddings(model_id, **self._connection_kwargs(conn))
+
+
+class LangChainAzureHook(LangChainHook):
+    """
+    Hook for Azure OpenAI via LangChain.
+
+    ``LangChainHook``'s ``api_key`` + ``base_url`` credential surface doesn't
+    match what Azure OpenAI's LangChain classes (``AzureChatOpenAI``,
+    ``AzureOpenAIEmbeddings``) expect: an ``azure_endpoint`` and an
+    ``api_version`` rather than a generic ``base_url``. This subclass maps
+    the connection to those fields instead, mirroring
+    
:class:`~airflow.providers.common.ai.hooks.pydantic_ai.PydanticAIAzureHook`.
+
+    Connection fields:
+        - **password**: Azure API key
+        - **host**: Azure endpoint (e.g. 
``https://<resource>.openai.azure.com``)
+        - **extra** JSON::
+
+            {"model": "azure_openai:gpt-4o", "api_version": 
"2024-07-01-preview"}
+
+    :param llm_conn_id: Airflow connection ID.
+    """
+
+    conn_type = "langchain-azure"
+    default_conn_name = "langchain_azure_default"
+    hook_name = "LangChain (Azure OpenAI)"
+
+    @staticmethod
+    def get_ui_field_behaviour() -> dict[str, Any]:
+        """Return custom field behaviour for the Airflow connection form."""
+        return {
+            "hidden_fields": ["schema", "port", "login"],
+            "relabeling": {"password": "API Key", "host": "Azure Endpoint"},
+            "placeholders": {
+                "host": "https://<resource>.openai.azure.com",
+                "extra": '{"model": "azure_openai:gpt-4o", "api_version": 
"2024-07-01-preview"}',
+            },
+        }
+
+    def _connection_kwargs(self, conn: Any) -> dict[str, Any]:
+        """Map connection fields to Azure OpenAI's azure_endpoint/api_version 
shape."""
+        kwargs: dict[str, Any] = {}
+        if conn.password:
+            kwargs["api_key"] = conn.password

Review Comment:
   `conn.password` is the only auth path here, but `AzureChatOpenAI` also 
accepts `azure_ad_token` and `azure_ad_token_provider`, and managed identity is 
how most enterprise Azure OpenAI deployments authenticate (AKS workload 
identity, no key stored in the connection at all).
   
   A hook that exists specifically to handle Azure credentials, but only reads 
a static key, misses those deployments. Deliberate scope cut for a follow-up? 
`PydanticAIAzureHook` has the same limitation, so this is not something you 
introduced.



##########
providers/common/ai/src/airflow/providers/common/ai/hooks/langchain.py:
##########
@@ -171,3 +170,53 @@ def get_embedding_model(self) -> Embeddings:
             kind="embedding",
         )
         return init_embeddings(model_id, **self._connection_kwargs(conn))
+
+
+class LangChainAzureHook(LangChainHook):

Review Comment:
   `LangChainHook` has no `test_connection`, unlike `PydanticAIHook`. This PR 
registers a new conn type in the UI, so users get a Test button that will not 
tell them whether the endpoint or the api_version is right. Given how many 
Azure-specific fields have to line up, worth adding one here?



##########
providers/common/ai/provider.yaml:
##########
@@ -404,6 +404,41 @@ connection-types:
           type:
             - string
             - 'null'
+  - hook-class-name: 
airflow.providers.common.ai.hooks.langchain.LangChainAzureHook
+    hook-name: "LangChain (Azure OpenAI)"
+    connection-type: langchain-azure
+    ui-field-behaviour:
+      hidden-fields:
+        - schema
+        - port
+        - login
+      relabeling:
+        password: API Key
+        host: Azure Endpoint
+      placeholders:
+        host: "https://<resource>.openai.azure.com"
+    conn-fields:
+      model:
+        label: Chat Model
+        description: "Chat model in azure_openai:name format (e.g. 
azure_openai:gpt-4o)."

Review Comment:
   Nothing enforces this format. Putting `openai:gpt-4o` on a `langchain-azure` 
connection does not raise: `init_chat_model` transfers `azure_endpoint` and 
`api_version` into `model_kwargs` behind a `UserWarning` and hands back a plain 
`ChatOpenAI`, so it fails later at request time with an error that points 
nowhere useful. Worth defaulting the prefix, or validating it in the hook?



##########
providers/common/ai/src/airflow/providers/common/ai/hooks/langchain.py:
##########
@@ -171,3 +170,53 @@ def get_embedding_model(self) -> Embeddings:
             kind="embedding",
         )
         return init_embeddings(model_id, **self._connection_kwargs(conn))
+
+
+class LangChainAzureHook(LangChainHook):
+    """
+    Hook for Azure OpenAI via LangChain.
+
+    ``LangChainHook``'s ``api_key`` + ``base_url`` credential surface doesn't
+    match what Azure OpenAI's LangChain classes (``AzureChatOpenAI``,
+    ``AzureOpenAIEmbeddings``) expect: an ``azure_endpoint`` and an
+    ``api_version`` rather than a generic ``base_url``. This subclass maps
+    the connection to those fields instead, mirroring
+    
:class:`~airflow.providers.common.ai.hooks.pydantic_ai.PydanticAIAzureHook`.
+
+    Connection fields:
+        - **password**: Azure API key
+        - **host**: Azure endpoint (e.g. 
``https://<resource>.openai.azure.com``)
+        - **extra** JSON::
+
+            {"model": "azure_openai:gpt-4o", "api_version": 
"2024-07-01-preview"}
+
+    :param llm_conn_id: Airflow connection ID.
+    """
+
+    conn_type = "langchain-azure"

Review Comment:
   LangChain registers two Azure providers in `_BUILTIN_PROVIDERS`: 
`azure_openai` (langchain-openai) and `azure_ai` (langchain-azure-ai, AI 
Foundry). Different classes, different kwargs.
   
   `langchain-azure` does not say which one this is, and it is the name you 
would want if Foundry support is ever added. Worth making it 
`langchain-azure-openai` while the conn type is still unreleased?



##########
providers/common/ai/src/airflow/providers/common/ai/hooks/langchain.py:
##########
@@ -171,3 +170,53 @@ def get_embedding_model(self) -> Embeddings:
             kind="embedding",
         )
         return init_embeddings(model_id, **self._connection_kwargs(conn))
+
+
+class LangChainAzureHook(LangChainHook):
+    """
+    Hook for Azure OpenAI via LangChain.
+
+    ``LangChainHook``'s ``api_key`` + ``base_url`` credential surface doesn't
+    match what Azure OpenAI's LangChain classes (``AzureChatOpenAI``,
+    ``AzureOpenAIEmbeddings``) expect: an ``azure_endpoint`` and an
+    ``api_version`` rather than a generic ``base_url``. This subclass maps
+    the connection to those fields instead, mirroring
+    
:class:`~airflow.providers.common.ai.hooks.pydantic_ai.PydanticAIAzureHook`.
+
+    Connection fields:
+        - **password**: Azure API key
+        - **host**: Azure endpoint (e.g. 
``https://<resource>.openai.azure.com``)
+        - **extra** JSON::
+
+            {"model": "azure_openai:gpt-4o", "api_version": 
"2024-07-01-preview"}
+
+    :param llm_conn_id: Airflow connection ID.
+    """
+
+    conn_type = "langchain-azure"
+    default_conn_name = "langchain_azure_default"
+    hook_name = "LangChain (Azure OpenAI)"
+
+    @staticmethod
+    def get_ui_field_behaviour() -> dict[str, Any]:
+        """Return custom field behaviour for the Airflow connection form."""
+        return {
+            "hidden_fields": ["schema", "port", "login"],
+            "relabeling": {"password": "API Key", "host": "Azure Endpoint"},
+            "placeholders": {
+                "host": "https://<resource>.openai.azure.com",
+                "extra": '{"model": "azure_openai:gpt-4o", "api_version": 
"2024-07-01-preview"}',
+            },
+        }
+
+    def _connection_kwargs(self, conn: Any) -> dict[str, Any]:
+        """Map connection fields to Azure OpenAI's azure_endpoint/api_version 
shape."""
+        kwargs: dict[str, Any] = {}
+        if conn.password:
+            kwargs["api_key"] = conn.password
+        if conn.host:
+            kwargs["azure_endpoint"] = conn.host
+        api_version = conn.extra_dejson.get("api_version")
+        if api_version:
+            kwargs["api_version"] = api_version

Review Comment:
   Treating `api_version` as optional behaves differently for chat and 
embeddings, and neither case is good (checked against langchain-openai 1.4.1):
   
   - `AzureChatOpenAI` has no default, so omitting it raises a raw pydantic 
`ValidationError` from inside the constructor rather than an Airflow-side 
message naming the connection field.
   - `AzureOpenAIEmbeddings` defaults to `2023-05-15`, so the same omission 
silently pins a three-year-old API version instead of failing.
   
   Given that carrying this field is much of the reason the subclass exists, 
should it be required with a clear error, the way `_resolve_model_id` already 
does for the model id?



##########
providers/common/ai/src/airflow/providers/common/ai/hooks/langchain.py:
##########
@@ -171,3 +170,53 @@ def get_embedding_model(self) -> Embeddings:
             kind="embedding",
         )
         return init_embeddings(model_id, **self._connection_kwargs(conn))
+
+
+class LangChainAzureHook(LangChainHook):
+    """
+    Hook for Azure OpenAI via LangChain.
+
+    ``LangChainHook``'s ``api_key`` + ``base_url`` credential surface doesn't
+    match what Azure OpenAI's LangChain classes (``AzureChatOpenAI``,
+    ``AzureOpenAIEmbeddings``) expect: an ``azure_endpoint`` and an

Review Comment:
   Both of these classes come from `langchain-openai`, which nothing installs. 
The provider's extra is just `"langchain" = ["langchain>=1.0.0"]`, so 
installing `apache-airflow-providers-common-ai[langchain]` and creating this 
connection fails inside `init_chat_model` with an ImportError. Should the extra 
grow, or should the hook raise something that names the missing package?



##########
providers/common/ai/src/airflow/providers/common/ai/hooks/langchain.py:
##########
@@ -171,3 +170,53 @@ def get_embedding_model(self) -> Embeddings:
             kind="embedding",
         )
         return init_embeddings(model_id, **self._connection_kwargs(conn))
+
+
+class LangChainAzureHook(LangChainHook):
+    """
+    Hook for Azure OpenAI via LangChain.
+
+    ``LangChainHook``'s ``api_key`` + ``base_url`` credential surface doesn't
+    match what Azure OpenAI's LangChain classes (``AzureChatOpenAI``,
+    ``AzureOpenAIEmbeddings``) expect: an ``azure_endpoint`` and an
+    ``api_version`` rather than a generic ``base_url``. This subclass maps
+    the connection to those fields instead, mirroring
+    
:class:`~airflow.providers.common.ai.hooks.pydantic_ai.PydanticAIAzureHook`.
+
+    Connection fields:
+        - **password**: Azure API key
+        - **host**: Azure endpoint (e.g. 
``https://<resource>.openai.azure.com``)
+        - **extra** JSON::
+
+            {"model": "azure_openai:gpt-4o", "api_version": 
"2024-07-01-preview"}
+
+    :param llm_conn_id: Airflow connection ID.
+    """
+
+    conn_type = "langchain-azure"
+    default_conn_name = "langchain_azure_default"
+    hook_name = "LangChain (Azure OpenAI)"
+
+    @staticmethod
+    def get_ui_field_behaviour() -> dict[str, Any]:
+        """Return custom field behaviour for the Airflow connection form."""
+        return {
+            "hidden_fields": ["schema", "port", "login"],
+            "relabeling": {"password": "API Key", "host": "Azure Endpoint"},
+            "placeholders": {
+                "host": "https://<resource>.openai.azure.com",
+                "extra": '{"model": "azure_openai:gpt-4o", "api_version": 
"2024-07-01-preview"}',
+            },
+        }
+
+    def _connection_kwargs(self, conn: Any) -> dict[str, Any]:
+        """Map connection fields to Azure OpenAI's azure_endpoint/api_version 
shape."""
+        kwargs: dict[str, Any] = {}
+        if conn.password:
+            kwargs["api_key"] = conn.password
+        if conn.host:
+            kwargs["azure_endpoint"] = conn.host
+        api_version = conn.extra_dejson.get("api_version")
+        if api_version:
+            kwargs["api_version"] = api_version
+        return kwargs

Review Comment:
   Nothing here can set `azure_deployment`. Azure deployment names are 
user-chosen, so they routinely differ from the model name, and the SDK routes 
on it:
   
   ```
   azure_deployment=None            -> 
.../openai/deployments/gpt-4o/chat/completions
   azure_deployment='gpt4o-prod-eu' -> 
.../openai/deployments/gpt4o-prod-eu/chat/completions
   ```
   
   So anyone whose deployment is not named exactly after the model cannot use 
this hook. Deliberate?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to