This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new fd896f247fe Add rerank support to Cohere provider (#72501)
fd896f247fe is described below

commit fd896f247fe9cdc2b15026b2385ff2c878b4e844
Author: Jeff(Wei-Hao) Lu <[email protected]>
AuthorDate: Wed Sep 9 19:04:07 2026 +0800

    Add rerank support to Cohere provider (#72501)
    
    * Add CohereRerankOperator for cohere provider
    
    * add rerank, reranking to wordlist
    
    * Fix Cohere rerank parameters and fix tests
---
 docs/spelling_wordlist.txt                         |  2 +
 providers/cohere/docs/index.rst                    |  7 +-
 providers/cohere/docs/operators/rerank.rst         | 71 ++++++++++++++++
 providers/cohere/provider.yaml                     |  2 +
 .../airflow/providers/cohere/get_provider_info.py  | 13 ++-
 .../src/airflow/providers/cohere/hooks/cohere.py   | 26 ++++++
 .../airflow/providers/cohere/operators/rerank.py   | 95 ++++++++++++++++++++++
 .../cohere/example_cohere_rerank_operator.py}      | 44 +++++-----
 .../cohere/tests/unit/cohere/hooks/test_cohere.py  | 49 ++++++++++-
 .../tests/unit/cohere/operators/test_rerank.py     | 86 ++++++++++++++++++++
 10 files changed, 366 insertions(+), 29 deletions(-)

diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
index f5d77467181..140ca2e93aa 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -1447,6 +1447,8 @@ requeue
 requeued
 requeues
 requeuing
+rerank
+reranking
 reserialize
 Reserializing
 resetdb
diff --git a/providers/cohere/docs/index.rst b/providers/cohere/docs/index.rst
index e3b1570d93c..262b709ff9a 100644
--- a/providers/cohere/docs/index.rst
+++ b/providers/cohere/docs/index.rst
@@ -19,16 +19,18 @@
 ``apache-airflow-providers-cohere``
 ======================================
 
-The ``cohere`` provider gives Dags direct access to Cohere's own Embed API — 
this page
+The ``cohere`` provider gives Dags direct access to Cohere's Embed and Rerank 
APIs — this page
 compares that choice against ``common.ai``.
 
 When to use this provider
 --------------------------
 
-Use ``cohere`` when a Dag needs Cohere's native embedding models specifically:
+Use ``cohere`` when a Dag needs Cohere's native embedding or reranking models:
 
 * ``CohereEmbeddingOperator`` — call Cohere's
   `Embed API <https://docs.cohere.com/docs/embeddings>`__ directly via 
``CohereHook``.
+* ``CohereRerankOperator`` — reorder documents with Cohere's
+  `Rerank API <https://docs.cohere.com/docs/rerank-overview>`__.
 
 Use :doc:`apache-airflow-providers-common-ai:index` instead when the embedding 
step should
 stay vendor-neutral:
@@ -53,6 +55,7 @@ stay vendor-neutral:
 
     Connection types <connections>
     Operators <operators/embedding>
+    Rerank operator <operators/rerank>
 
 .. toctree::
     :hidden:
diff --git a/providers/cohere/docs/operators/rerank.rst 
b/providers/cohere/docs/operators/rerank.rst
new file mode 100644
index 00000000000..44ae215098e
--- /dev/null
+++ b/providers/cohere/docs/operators/rerank.rst
@@ -0,0 +1,71 @@
+ .. Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+ ..   http://www.apache.org/licenses/LICENSE-2.0
+
+ .. Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+.. _howto/operator:CohereRerankOperator:
+
+CohereRerankOperator
+====================
+
+Use :class:`~airflow.providers.cohere.operators.rerank.CohereRerankOperator` 
to reorder
+documents by their relevance to a query with Cohere's
+`Rerank API <https://docs.cohere.com/docs/rerank-overview>`__.
+
+Before you begin
+^^^^^^^^^^^^^^^^
+
+Configure a :ref:`Cohere connection <howto/connection:cohere>`. The operator 
uses the
+``cohere_default`` connection unless another ``conn_id`` is provided.
+
+The operator requires:
+
+* ``query``: The search query used to evaluate relevance.
+* ``documents``: A list of text documents to rank.
+
+The operator uses ``rerank-v3.5`` by default. Set ``model`` to use another 
Cohere model or
+an endpoint-specific deployment name. Use ``top_n`` to limit the number of 
results and
+``max_tokens_per_doc`` to control how much of each document Cohere processes. 
The query,
+documents, and both limits are templated fields; rendered limit values are 
converted to integers
+before they are sent to Cohere.
+
+Using the operator
+^^^^^^^^^^^^^^^^^^
+
+.. exampleinclude:: 
/../../cohere/tests/system/cohere/example_cohere_rerank_operator.py
+    :language: python
+    :dedent: 4
+    :start-after: [START howto_operator_cohere_rerank]
+    :end-before: [END howto_operator_cohere_rerank]
+
+Output
+^^^^^^
+
+The operator converts the Cohere response to an XCom-serializable dictionary. 
Its ``results``
+list is ordered from most to least relevant. Each result contains the original 
document's
+zero-based ``index`` and its ``relevance_score``. Use the index to associate a 
result with the
+corresponding item in the input ``documents`` list.
+
+An abbreviated response looks like this:
+
+.. code-block:: json
+
+    {
+      "id": "rerank-request-id",
+      "results": [
+        {"index": 1, "relevance_score": 0.99},
+        {"index": 0, "relevance_score": 0.12}
+      ]
+    }
diff --git a/providers/cohere/provider.yaml b/providers/cohere/provider.yaml
index 97e057f6f69..8e6d67ca638 100644
--- a/providers/cohere/provider.yaml
+++ b/providers/cohere/provider.yaml
@@ -62,6 +62,7 @@ integrations:
     external-doc-url: https://docs.cohere.com/docs
     how-to-guide:
       - /docs/apache-airflow-providers-cohere/operators/embedding.rst
+      - /docs/apache-airflow-providers-cohere/operators/rerank.rst
     tags: [software]
 
 hooks:
@@ -73,6 +74,7 @@ operators:
   - integration-name: Cohere
     python-modules:
       - airflow.providers.cohere.operators.embedding
+      - airflow.providers.cohere.operators.rerank
 
 connection-types:
   - hook-class-name: airflow.providers.cohere.hooks.cohere.CohereHook
diff --git a/providers/cohere/src/airflow/providers/cohere/get_provider_info.py 
b/providers/cohere/src/airflow/providers/cohere/get_provider_info.py
index ad445d1afbd..a4f9eef9a91 100644
--- a/providers/cohere/src/airflow/providers/cohere/get_provider_info.py
+++ b/providers/cohere/src/airflow/providers/cohere/get_provider_info.py
@@ -30,7 +30,10 @@ def get_provider_info():
             {
                 "integration-name": "Cohere",
                 "external-doc-url": "https://docs.cohere.com/docs";,
-                "how-to-guide": 
["/docs/apache-airflow-providers-cohere/operators/embedding.rst"],
+                "how-to-guide": [
+                    
"/docs/apache-airflow-providers-cohere/operators/embedding.rst",
+                    
"/docs/apache-airflow-providers-cohere/operators/rerank.rst",
+                ],
                 "tags": ["software"],
             }
         ],
@@ -38,7 +41,13 @@ def get_provider_info():
             {"integration-name": "Cohere", "python-modules": 
["airflow.providers.cohere.hooks.cohere"]}
         ],
         "operators": [
-            {"integration-name": "Cohere", "python-modules": 
["airflow.providers.cohere.operators.embedding"]}
+            {
+                "integration-name": "Cohere",
+                "python-modules": [
+                    "airflow.providers.cohere.operators.embedding",
+                    "airflow.providers.cohere.operators.rerank",
+                ],
+            }
         ],
         "connection-types": [
             {
diff --git a/providers/cohere/src/airflow/providers/cohere/hooks/cohere.py 
b/providers/cohere/src/airflow/providers/cohere/hooks/cohere.py
index 848a70edd74..b8c583519b1 100644
--- a/providers/cohere/src/airflow/providers/cohere/hooks/cohere.py
+++ b/providers/cohere/src/airflow/providers/cohere/hooks/cohere.py
@@ -110,6 +110,32 @@ class CohereHook(BaseHook):
             raise ValueError("Embeddings response is missing float_ field")
         return response.embeddings.float_
 
+    def rerank(
+        self,
+        *,
+        query: str,
+        documents: list[str],
+        model: str = "rerank-v3.5",
+        top_n: int | None = None,
+        max_tokens_per_doc: int | None = None,
+    ) -> dict[str, Any]:
+        """Rerank documents by their relevance to a query."""
+        rerank_kwargs: dict[str, Any] = {
+            "query": query,
+            "documents": documents,
+            "model": model,
+            "request_options": self.request_options,
+        }
+        if top_n is not None:
+            rerank_kwargs["top_n"] = top_n
+        if max_tokens_per_doc is not None:
+            rerank_kwargs["max_tokens_per_doc"] = max_tokens_per_doc
+
+        response = self.get_conn().rerank(
+            **rerank_kwargs,
+        )
+        return response.model_dump(mode="json")
+
     @classmethod
     def get_ui_field_behaviour(cls) -> dict[str, Any]:
         return {
diff --git a/providers/cohere/src/airflow/providers/cohere/operators/rerank.py 
b/providers/cohere/src/airflow/providers/cohere/operators/rerank.py
new file mode 100644
index 00000000000..a77358e3f61
--- /dev/null
+++ b/providers/cohere/src/airflow/providers/cohere/operators/rerank.py
@@ -0,0 +1,95 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+from functools import cached_property
+from typing import TYPE_CHECKING, Any
+
+from airflow.providers.cohere.hooks.cohere import CohereHook
+from airflow.providers.common.compat.sdk import BaseOperator
+
+if TYPE_CHECKING:
+    from cohere.core.request_options import RequestOptions
+
+    from airflow.providers.common.compat.sdk import Context
+
+
+class CohereRerankOperator(BaseOperator):
+    """
+    Rerank documents by their relevance to a query using Cohere's Rerank API.
+
+    .. seealso::
+        For more information on how to use this operator, take a look at the 
guide:
+        :ref:`howto/operator:CohereRerankOperator`
+
+    :param query: The search query used to rank the documents.
+    :param documents: Text documents to compare with the query.
+    :param model: Model to use for reranking. Uses the hook's default when 
omitted.
+    :param top_n: Maximum number of ranked documents to return. By default, 
all are returned.
+    :param max_tokens_per_doc: Maximum number of tokens retained from each 
document.
+    :param conn_id: Cohere connection id.
+    :param timeout: Request timeout in seconds.
+    :param request_options: Request-specific configuration passed to the 
Cohere client.
+    """
+
+    template_fields: Sequence[str] = ("query", "documents", "top_n", 
"max_tokens_per_doc")
+    template_fields_renderers = {"documents": "json"}
+
+    def __init__(
+        self,
+        *,
+        query: str,
+        documents: list[str],
+        model: str | None = None,
+        top_n: int | None = None,
+        max_tokens_per_doc: int | None = None,
+        conn_id: str = CohereHook.default_conn_name,
+        timeout: int | None = None,
+        request_options: RequestOptions | None = None,
+        **kwargs: Any,
+    ) -> None:
+        super().__init__(**kwargs)
+        self.query = query
+        self.documents = documents
+        self.model = model
+        self.top_n = top_n
+        self.max_tokens_per_doc = max_tokens_per_doc
+        self.conn_id = conn_id
+        self.timeout = timeout
+        self.request_options = request_options
+
+    @cached_property
+    def hook(self) -> CohereHook:
+        """Return a Cohere hook."""
+        return CohereHook(
+            conn_id=self.conn_id,
+            timeout=self.timeout,
+            request_options=self.request_options,
+        )
+
+    def execute(self, context: Context) -> dict[str, Any]:
+        """Rerank the documents and return an XCom-serializable response."""
+        rerank_kwargs: dict[str, Any] = {"query": self.query, "documents": 
self.documents}
+        if self.model is not None:
+            rerank_kwargs["model"] = self.model
+        if self.top_n is not None:
+            rerank_kwargs["top_n"] = int(self.top_n)
+        if self.max_tokens_per_doc is not None:
+            rerank_kwargs["max_tokens_per_doc"] = int(self.max_tokens_per_doc)
+        return self.hook.rerank(**rerank_kwargs)
diff --git a/providers/cohere/tests/unit/cohere/hooks/test_cohere.py 
b/providers/cohere/tests/system/cohere/example_cohere_rerank_operator.py
similarity index 51%
copy from providers/cohere/tests/unit/cohere/hooks/test_cohere.py
copy to providers/cohere/tests/system/cohere/example_cohere_rerank_operator.py
index f4e7a218b13..2be7067abe8 100644
--- a/providers/cohere/tests/unit/cohere/hooks/test_cohere.py
+++ b/providers/cohere/tests/system/cohere/example_cohere_rerank_operator.py
@@ -14,33 +14,29 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+
 from __future__ import annotations
 
-from unittest.mock import patch
+from datetime import datetime
+
+from airflow import DAG
+from airflow.providers.cohere.operators.rerank import CohereRerankOperator
 
-from airflow.models import Connection
-from airflow.providers.cohere.hooks.cohere import (
-    CohereHook,
-)
+with DAG("example_cohere_rerank", schedule=None, start_date=datetime(2023, 1, 
1), catchup=False) as dag:
+    # [START howto_operator_cohere_rerank]
+    CohereRerankOperator(
+        task_id="rerank_documents",
+        query="What is the capital of the United States?",
+        documents=[
+            "Carson City is the capital city of Nevada.",
+            "Washington, D.C. is the capital of the United States.",
+            "The capital city of France is Paris.",
+        ],
+        top_n=2,
+    )
+    # [END howto_operator_cohere_rerank]
 
 
-class TestCohereHook:
-    """
-    Test for CohereHook
-    """
+from tests_common.test_utils.system_tests import get_test_run
 
-    def test__get_api_key(self):
-        api_key = "test"
-        base_url = "http://some_host.com";
-        timeout = 150
-        with (
-            patch.object(
-                CohereHook,
-                "get_connection",
-                return_value=Connection(conn_type="cohere", password=api_key, 
host=base_url),
-            ),
-            patch("cohere.ClientV2") as client,
-        ):
-            hook = CohereHook(timeout=timeout)
-            _ = hook.get_conn()
-            client.assert_called_once_with(api_key=api_key, timeout=timeout, 
base_url=base_url)
+test_run = get_test_run(dag)
diff --git a/providers/cohere/tests/unit/cohere/hooks/test_cohere.py 
b/providers/cohere/tests/unit/cohere/hooks/test_cohere.py
index f4e7a218b13..4306113d448 100644
--- a/providers/cohere/tests/unit/cohere/hooks/test_cohere.py
+++ b/providers/cohere/tests/unit/cohere/hooks/test_cohere.py
@@ -16,7 +16,7 @@
 # under the License.
 from __future__ import annotations
 
-from unittest.mock import patch
+from unittest.mock import MagicMock, patch
 
 from airflow.models import Connection
 from airflow.providers.cohere.hooks.cohere import (
@@ -44,3 +44,50 @@ class TestCohereHook:
             hook = CohereHook(timeout=timeout)
             _ = hook.get_conn()
             client.assert_called_once_with(api_key=api_key, timeout=timeout, 
base_url=base_url)
+
+    @patch.object(CohereHook, "get_conn", autospec=True)
+    def test_rerank(self, mock_get_conn):
+        response = MagicMock(spec=["model_dump"])
+        response.model_dump.return_value = {
+            "id": "rerank-id",
+            "results": [{"index": 1, "relevance_score": 0.9}],
+        }
+        mock_get_conn.return_value.rerank.return_value = response
+        request_options = {"timeout_in_seconds": 10}
+        hook = CohereHook(request_options=request_options)
+
+        result = hook.rerank(
+            query="Where is the capital?",
+            documents=["first", "second"],
+            model="rerank-v3.5",
+            top_n=1,
+            max_tokens_per_doc=512,
+        )
+
+        mock_get_conn.return_value.rerank.assert_called_once_with(
+            query="Where is the capital?",
+            documents=["first", "second"],
+            model="rerank-v3.5",
+            top_n=1,
+            max_tokens_per_doc=512,
+            request_options=request_options,
+        )
+        response.model_dump.assert_called_once_with(mode="json")
+        assert result == {"id": "rerank-id", "results": [{"index": 1, 
"relevance_score": 0.9}]}
+
+    @patch.object(CohereHook, "get_conn", autospec=True)
+    def test_rerank_uses_default_model_and_omits_unset_limits(self, 
mock_get_conn):
+        response = MagicMock(spec=["model_dump"])
+        response.model_dump.return_value = {"results": []}
+        mock_get_conn.return_value.rerank.return_value = response
+        hook = CohereHook()
+
+        result = hook.rerank(query="Where is the capital?", 
documents=["first", "second"])
+
+        mock_get_conn.return_value.rerank.assert_called_once_with(
+            query="Where is the capital?",
+            documents=["first", "second"],
+            model="rerank-v3.5",
+            request_options=None,
+        )
+        assert result == {"results": []}
diff --git a/providers/cohere/tests/unit/cohere/operators/test_rerank.py 
b/providers/cohere/tests/unit/cohere/operators/test_rerank.py
new file mode 100644
index 00000000000..d57dba6c3de
--- /dev/null
+++ b/providers/cohere/tests/unit/cohere/operators/test_rerank.py
@@ -0,0 +1,86 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+from unittest import mock
+
+from airflow.providers.cohere.hooks.cohere import CohereHook
+from airflow.providers.cohere.operators.rerank import CohereRerankOperator
+
+
[email protected](CohereRerankOperator, "hook", 
new_callable=mock.PropertyMock)
+def 
test_execute_coerces_templated_limits_and_forwards_model(mock_hook_property):
+    expected = {"id": "rerank-id", "results": [{"index": 1, "relevance_score": 
0.9}]}
+    mock_hook = mock.create_autospec(CohereHook, instance=True)
+    mock_hook_property.return_value = mock_hook
+    mock_hook.rerank.return_value = expected
+    documents = ["first", "second"]
+    operator = CohereRerankOperator(
+        task_id="rerank",
+        query="Where is the capital?",
+        documents=documents,
+        model="rerank-v3.5",
+        top_n="1",
+        max_tokens_per_doc="512",
+    )
+
+    result = operator.execute(context={})
+
+    mock_hook.rerank.assert_called_once_with(
+        query="Where is the capital?",
+        documents=documents,
+        model="rerank-v3.5",
+        top_n=1,
+        max_tokens_per_doc=512,
+    )
+    assert result == expected
+
+
[email protected](CohereRerankOperator, "hook", 
new_callable=mock.PropertyMock)
+def test_execute_omits_unset_optional_arguments(mock_hook_property):
+    mock_hook = mock.create_autospec(CohereHook, instance=True)
+    mock_hook_property.return_value = mock_hook
+    operator = CohereRerankOperator(
+        task_id="rerank",
+        query="Where is the capital?",
+        documents=["first", "second"],
+    )
+
+    operator.execute(context={})
+
+    mock_hook.rerank.assert_called_once_with(query="Where is the capital?", 
documents=["first", "second"])
+
+
[email protected]("airflow.providers.cohere.operators.rerank.CohereHook", 
autospec=True)
+def test_hook_uses_operator_connection_options(mock_hook_class):
+    request_options = {"timeout_in_seconds": 10}
+    operator = CohereRerankOperator(
+        task_id="rerank",
+        query="Where is the capital?",
+        documents=["first", "second"],
+        conn_id="cohere_custom",
+        timeout=30,
+        request_options=request_options,
+    )
+
+    assert operator.hook == mock_hook_class.return_value
+    mock_hook_class.assert_called_once_with(
+        conn_id="cohere_custom",
+        timeout=30,
+        request_options=request_options,
+    )

Reply via email to