kaxil commented on code in PR #74149:
URL: https://github.com/apache/airflow/pull/74149#discussion_r4284770918
##########
providers/openai/src/airflow/providers/openai/hooks/openai.py:
##########
@@ -593,6 +619,49 @@ def create_embeddings(
:return: One embedding for a single text or token array; one embedding
per item for a batch.
"""
response = self.conn.embeddings.create(model=model, input=text,
**kwargs)
+ return self._embeddings_result(text, response)
+
+ @overload
+ async def acreate_embeddings(
+ self,
+ text: str | list[int],
+ model: str = "text-embedding-3-small",
+ **kwargs: Any,
+ ) -> list[float]: ...
+
+ @overload
+ async def acreate_embeddings(
+ self,
+ text: list[str] | list[list[int]],
+ model: str = "text-embedding-3-small",
+ **kwargs: Any,
+ ) -> list[list[float]]: ...
+
+ async def acreate_embeddings(
+ self,
+ text: str | list[str] | list[int] | list[list[int]],
+ model: str = "text-embedding-3-small",
+ **kwargs: Any,
+ ) -> list[float] | list[list[float]]:
+ """
+ Generate embeddings for the given text using the given model,
asynchronously.
+
+ The async counterpart of :meth:`create_embeddings`, on a client from
:meth:`aget_conn`
+ that is closed when the call returns.
Review Comment:
The first two sentences say the same thing. The useful fact is the per-call
client, since the reuse advice on `aget_conn` can't be applied through this
method:
```suggestion
Async version of :meth:`create_embeddings`.
Each call opens and closes its own client, so for many inputs pass
one batched ``text`` list.
```
##########
providers/openai/src/airflow/providers/openai/hooks/openai.py:
##########
@@ -207,23 +211,45 @@ def get_conn(self) -> OpenAI:
OpenAI client's workload identity support. See
:meth:`_build_workload_identity`.
"""
conn = self.get_connection(self.conn_id)
- extras = conn.extra_dejson
- openai_client_kwargs = extras.get("openai_client_kwargs", {})
+ return OpenAI(**self._client_kwargs(conn, conn.extra_dejson))
+
+ async def aget_conn(self) -> AsyncOpenAI:
+ """
+ Return an ``AsyncOpenAI`` client, for use inside an async task or
trigger.
+
+ Configured exactly as :meth:`get_conn` configures its client.
``get_connection`` and the
+ secret masking of ``extra_dejson`` send to the supervisor
synchronously, which raises
+ ``DeadlockImminentError`` on an event loop with another async call in
flight. On
+ Airflow 3.3.2+ the connection is resolved without any synchronous call
to the Task SDK.
+ On older versions, fetching the connection with ``aget_connection``
still masks its
+ secrets synchronously, so that guarantee does not hold there.
+
+ Each call builds a new client. Close it when done, with ``async with``
or
+ ``await client.close()``, to release its HTTP connections. With
``auth_type``
+ ``workload_identity`` the client caches the exchanged token, so every
new client
+ exchanges a token again: for many calls, keep one client open and
reuse it.
Review Comment:
This docstring has grown to 13 lines, and the deadlock mechanics are already
explained in `get_async_extra_dejson` in common.compat. Something like this
keeps what a caller needs:
```suggestion
Return an ``AsyncOpenAI`` client configured like :meth:`get_conn`.
From Airflow 3.3.2 the connection is resolved without a blocking
call to the
supervisor, so this is safe on an event loop with other async calls
in flight.
Each call builds a new client: close it with ``async with`` or
``await client.close()``.
With ``workload_identity`` each client exchanges its own token, so
reuse one client
for many calls.
```
##########
providers/openai/tests/unit/openai/hooks/test_openai.py:
##########
@@ -1032,3 +1034,206 @@ def
test_managed_agents_reports_sdk_upgrade_without_breaking_hook():
hook.conn = SimpleNamespace(beta=SimpleNamespace())
with pytest.raises(OpenAIAgentSessionError, match="requires
openai>=3.13.0"):
hook.create_agent_session(input="Hello", environment={"type": "none"},
agent_id="agent")
+
+
+def _async_conn(
+ extra: dict, password: str | None = None, host: str | None = None, *,
aextra_dejson: bool = True
+) -> Mock:
+ """
+ A connection whose ``extra_dejson`` raises on the event loop, where its
secret masking deadlocks.
+
+ With ``aextra_dejson=False`` it lacks ``Connection.aextra_dejson()``, like
on Airflow < 3.3.2,
+ where ``get_async_extra_dejson`` reads ``extra_dejson`` from a worker
thread instead.
+ """
Review Comment:
One line is enough here. The docstrings on
`test_aget_conn_without_aextra_dejson_reads_extra_dejson_off_the_event_loop`
and `test_acreate_embeddings_concurrent_calls_each_close_their_own_client`
repeat the test names, so they can go too.
```suggestion
"""Connection stub whose ``extra_dejson`` fails on the event loop;
``aextra_dejson=False`` mimics Airflow < 3.3.2."""
```
##########
providers/openai/src/airflow/providers/openai/hooks/openai.py:
##########
@@ -207,23 +211,45 @@ def get_conn(self) -> OpenAI:
OpenAI client's workload identity support. See
:meth:`_build_workload_identity`.
"""
conn = self.get_connection(self.conn_id)
- extras = conn.extra_dejson
- openai_client_kwargs = extras.get("openai_client_kwargs", {})
+ return OpenAI(**self._client_kwargs(conn, conn.extra_dejson))
+
+ async def aget_conn(self) -> AsyncOpenAI:
+ """
+ Return an ``AsyncOpenAI`` client, for use inside an async task or
trigger.
+
+ Configured exactly as :meth:`get_conn` configures its client.
``get_connection`` and the
+ secret masking of ``extra_dejson`` send to the supervisor
synchronously, which raises
+ ``DeadlockImminentError`` on an event loop with another async call in
flight. On
+ Airflow 3.3.2+ the connection is resolved without any synchronous call
to the Task SDK.
+ On older versions, fetching the connection with ``aget_connection``
still masks its
+ secrets synchronously, so that guarantee does not hold there.
+
+ Each call builds a new client. Close it when done, with ``async with``
or
+ ``await client.close()``, to release its HTTP connections. With
``auth_type``
+ ``workload_identity`` the client caches the exchanged token, so every
new client
+ exchanges a token again: for many calls, keep one client open and
reuse it.
+ """
+ conn = await get_async_connection(self.conn_id, hook=self)
+ return AsyncOpenAI(**self._client_kwargs(conn, await
get_async_extra_dejson(conn)))
+
+ def _client_kwargs(self, conn: Connection, extras: dict[str, Any]) ->
dict[str, Any]:
+ """Return the keyword arguments of the OpenAI client, for
:meth:`get_conn` and :meth:`aget_conn`."""
Review Comment:
Naming the callers here goes stale as soon as a third one appears.
```suggestion
"""Build the ``OpenAI`` / ``AsyncOpenAI`` constructor kwargs from
the connection."""
```
--
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]