dex-connector-cohere 0.1.0__tar.gz
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.
- dex_connector_cohere-0.1.0/.gitignore +6 -0
- dex_connector_cohere-0.1.0/PKG-INFO +51 -0
- dex_connector_cohere-0.1.0/README.md +40 -0
- dex_connector_cohere-0.1.0/examples/cohere_search/__init__.py +3 -0
- dex_connector_cohere-0.1.0/examples/cohere_search/flow.py +129 -0
- dex_connector_cohere-0.1.0/examples/cohere_search/worker.py +51 -0
- dex_connector_cohere-0.1.0/pyproject.toml +37 -0
- dex_connector_cohere-0.1.0/src/dex_connectors/cohere/__init__.py +51 -0
- dex_connector_cohere-0.1.0/src/dex_connectors/cohere/_generated/__init__.py +2 -0
- dex_connector_cohere-0.1.0/src/dex_connectors/cohere/_generated/config.py +25 -0
- dex_connector_cohere-0.1.0/src/dex_connectors/cohere/_generated/connection.py +131 -0
- dex_connector_cohere-0.1.0/src/dex_connectors/cohere/_generated/definitions.py +55 -0
- dex_connector_cohere-0.1.0/src/dex_connectors/cohere/_generated/mock.py +119 -0
- dex_connector_cohere-0.1.0/src/dex_connectors/cohere/_generated/models.py +97 -0
- dex_connector_cohere-0.1.0/src/dex_connectors/cohere/_generated/steps.py +128 -0
- dex_connector_cohere-0.1.0/src/dex_connectors/cohere/client.py +382 -0
- dex_connector_cohere-0.1.0/src/dex_connectors/cohere/coheremock/__init__.py +43 -0
- dex_connector_cohere-0.1.0/src/dex_connectors/cohere/connection.py +46 -0
- dex_connector_cohere-0.1.0/src/dex_connectors/cohere/dex-connector.json +77 -0
- dex_connector_cohere-0.1.0/src/dex_connectors/cohere/models.py +22 -0
- dex_connector_cohere-0.1.0/src/dex_connectors/cohere/py.typed +0 -0
- dex_connector_cohere-0.1.0/src/dex_connectors/cohere/steps.py +33 -0
- dex_connector_cohere-0.1.0/tests/test_conformance.py +32 -0
- dex_connector_cohere-0.1.0/tests/test_manifest_contract.py +15 -0
- dex_connector_cohere-0.1.0/tests/test_operations.py +170 -0
- dex_connector_cohere-0.1.0/uv.lock +932 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: dex-connector-cohere
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cohere connector for Dex Flows
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: dex-connectors-sdk==0.1.0
|
|
8
|
+
Requires-Dist: dex-python-sdk>=1.28.0
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# Cohere connector for Python
|
|
13
|
+
|
|
14
|
+
Install with `pip install dex-connector-cohere==0.1.0`. The package requires
|
|
15
|
+
`dex-connectors-sdk==0.1.0` and `dex-python-sdk>=1.28.0`, and implements the
|
|
16
|
+
manifest Queries `embed` and `rerank` with the same branches, defaults, and
|
|
17
|
+
failure classification as the Go module.
|
|
18
|
+
|
|
19
|
+
## Operations
|
|
20
|
+
|
|
21
|
+
| Operation | Factory | Happy branch | Optional branches |
|
|
22
|
+
| --- | --- | --- | --- |
|
|
23
|
+
| `embed` | `new_embed_step` | `embedded` | `providerRejected`, `invalidResponse`, `defect` |
|
|
24
|
+
| `rerank` | `new_rerank_step` | `ranked` | `providerRejected`, `invalidResponse`, `defect` |
|
|
25
|
+
|
|
26
|
+
`embed` takes a model, 1 to 96 nonblank texts, and an input type
|
|
27
|
+
(`search_document`, `search_query`, `classification`, or `clustering`) and
|
|
28
|
+
returns one float vector per text. `rerank` takes a model, a query, 1 to 1000
|
|
29
|
+
nonblank documents, and an optional `top_n`, and returns each document's index
|
|
30
|
+
and relevance score in ranked order. Both calls may be billed, so the manifest
|
|
31
|
+
allows three attempts within five minutes. 408, 409, 429, 5xx, and transport
|
|
32
|
+
failures retry; 401 and 498 select `providerRejected` with an authentication
|
|
33
|
+
kind, and 403 with an authorization kind.
|
|
34
|
+
|
|
35
|
+
## Authorization
|
|
36
|
+
|
|
37
|
+
Create a Dex Web connection named `cohere` as described in the
|
|
38
|
+
[connector README](../README.md#connection). The API key travels only in the
|
|
39
|
+
`Authorization: Bearer` header and is resolved for every attempt.
|
|
40
|
+
|
|
41
|
+
## Example
|
|
42
|
+
|
|
43
|
+
[`examples/cohere_search`](examples/cohere_search/flow.py) embeds a query and
|
|
44
|
+
then reranks candidate documents. Run the Worker with
|
|
45
|
+
`uv run python -m examples.cohere_search.worker` after `dexcli dev` has loaded
|
|
46
|
+
a project containing the `cohere` connection.
|
|
47
|
+
|
|
48
|
+
## Tests
|
|
49
|
+
|
|
50
|
+
`coheremock.Mock` scripts both Queries and defaults to the manifest answers.
|
|
51
|
+
Run `uv run pytest tests` for provider parity and manifest contract checks.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Cohere connector for Python
|
|
2
|
+
|
|
3
|
+
Install with `pip install dex-connector-cohere==0.1.0`. The package requires
|
|
4
|
+
`dex-connectors-sdk==0.1.0` and `dex-python-sdk>=1.28.0`, and implements the
|
|
5
|
+
manifest Queries `embed` and `rerank` with the same branches, defaults, and
|
|
6
|
+
failure classification as the Go module.
|
|
7
|
+
|
|
8
|
+
## Operations
|
|
9
|
+
|
|
10
|
+
| Operation | Factory | Happy branch | Optional branches |
|
|
11
|
+
| --- | --- | --- | --- |
|
|
12
|
+
| `embed` | `new_embed_step` | `embedded` | `providerRejected`, `invalidResponse`, `defect` |
|
|
13
|
+
| `rerank` | `new_rerank_step` | `ranked` | `providerRejected`, `invalidResponse`, `defect` |
|
|
14
|
+
|
|
15
|
+
`embed` takes a model, 1 to 96 nonblank texts, and an input type
|
|
16
|
+
(`search_document`, `search_query`, `classification`, or `clustering`) and
|
|
17
|
+
returns one float vector per text. `rerank` takes a model, a query, 1 to 1000
|
|
18
|
+
nonblank documents, and an optional `top_n`, and returns each document's index
|
|
19
|
+
and relevance score in ranked order. Both calls may be billed, so the manifest
|
|
20
|
+
allows three attempts within five minutes. 408, 409, 429, 5xx, and transport
|
|
21
|
+
failures retry; 401 and 498 select `providerRejected` with an authentication
|
|
22
|
+
kind, and 403 with an authorization kind.
|
|
23
|
+
|
|
24
|
+
## Authorization
|
|
25
|
+
|
|
26
|
+
Create a Dex Web connection named `cohere` as described in the
|
|
27
|
+
[connector README](../README.md#connection). The API key travels only in the
|
|
28
|
+
`Authorization: Bearer` header and is resolved for every attempt.
|
|
29
|
+
|
|
30
|
+
## Example
|
|
31
|
+
|
|
32
|
+
[`examples/cohere_search`](examples/cohere_search/flow.py) embeds a query and
|
|
33
|
+
then reranks candidate documents. Run the Worker with
|
|
34
|
+
`uv run python -m examples.cohere_search.worker` after `dexcli dev` has loaded
|
|
35
|
+
a project containing the `cohere` connection.
|
|
36
|
+
|
|
37
|
+
## Tests
|
|
38
|
+
|
|
39
|
+
`coheremock.Mock` scripts both Queries and defaults to the manifest answers.
|
|
40
|
+
Run `uv run pytest tests` for provider parity and manifest contract checks.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Runnable Cohere search Flow: embed the query, then rerank candidate documents."""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import dex
|
|
8
|
+
from dex_connectors.sdk import QueryResult
|
|
9
|
+
|
|
10
|
+
from dex_connectors.cohere import (
|
|
11
|
+
Connection,
|
|
12
|
+
EmbedInput,
|
|
13
|
+
EmbedOutput,
|
|
14
|
+
RankedDocument,
|
|
15
|
+
RerankInput,
|
|
16
|
+
RerankOutput,
|
|
17
|
+
new_embed_step,
|
|
18
|
+
new_rerank_step,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
FLOW_TYPE = "CohereSearch"
|
|
22
|
+
CONNECTION_NAME = "cohere"
|
|
23
|
+
_EMBED_STEP_TYPE = "EmbedSearchQuery"
|
|
24
|
+
_RERANK_STEP_TYPE = "RerankSearchDocuments"
|
|
25
|
+
_ANNOTATION_GROUP = {"groupId": "cohere-search", "groupLabel": "Cohere search"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, slots=True)
|
|
29
|
+
class SearchRequest:
|
|
30
|
+
"""Search query and at least two candidate documents."""
|
|
31
|
+
|
|
32
|
+
query: str
|
|
33
|
+
documents: tuple[str, ...]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True, slots=True)
|
|
37
|
+
class SearchOutcome:
|
|
38
|
+
"""Candidate documents in Cohere's ranked order."""
|
|
39
|
+
|
|
40
|
+
results: tuple[RankedDocument, ...] = ()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_REQUEST = dex.Attribute("cohere-search-request", SearchRequest)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def map_to_embed_input(request: SearchRequest) -> EmbedInput:
|
|
47
|
+
"""Embed the query as a search query."""
|
|
48
|
+
return EmbedInput(model="embed-v4.0", texts=(request.query,), input_type="search_query")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def map_to_rerank_input(request: SearchRequest) -> RerankInput:
|
|
52
|
+
"""Rerank every candidate document against the query."""
|
|
53
|
+
return RerankInput(model="rerank-v5.0-pro", query=request.query, documents=request.documents)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# dex:group group-id:cohere-search group-label:"Cohere search"
|
|
57
|
+
# dex:explanation text:"Check and store the search request before calling Cohere."
|
|
58
|
+
class _StartRequest(dex.Step[SearchRequest]):
|
|
59
|
+
def get_step_type(self) -> str:
|
|
60
|
+
return "StartCohereSearch"
|
|
61
|
+
|
|
62
|
+
def execute(self, context: dex.Context, input: SearchRequest) -> dex.StepDecision:
|
|
63
|
+
if not input.query.strip() or len(input.documents) < 2:
|
|
64
|
+
return dex.force_fail("query and at least two documents are required")
|
|
65
|
+
_REQUEST.set(context, input)
|
|
66
|
+
return dex.go_to(dex.StepRef(_EMBED_STEP_TYPE), input)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# dex:group group-id:cohere-search group-label:"Cohere search"
|
|
70
|
+
# dex:explanation text:"Continue from the query embedding to the rerank call."
|
|
71
|
+
class _AfterEmbedding(dex.Step[QueryResult[EmbedOutput]]):
|
|
72
|
+
def get_step_type(self) -> str:
|
|
73
|
+
return "AfterCohereEmbedding"
|
|
74
|
+
|
|
75
|
+
def execute(self, context: dex.Context, input: QueryResult[EmbedOutput]) -> dex.StepDecision:
|
|
76
|
+
return dex.go_to(dex.StepRef(_RERANK_STEP_TYPE), _REQUEST.get(context))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# dex:group group-id:cohere-search group-label:"Cohere search"
|
|
80
|
+
# dex:explanation text:"Complete with the ranked documents."
|
|
81
|
+
class _CompleteSearch(dex.Step[QueryResult[RerankOutput]]):
|
|
82
|
+
def get_step_type(self) -> str:
|
|
83
|
+
return "CompleteCohereSearch"
|
|
84
|
+
|
|
85
|
+
def execute(self, context: dex.Context, input: QueryResult[RerankOutput]) -> dex.StepDecision:
|
|
86
|
+
return dex.graceful_complete(SearchOutcome(input.value.results))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class SearchFlow(dex.Flow[SearchRequest]):
|
|
90
|
+
"""Embed a query and rerank documents through the static cohere connection."""
|
|
91
|
+
|
|
92
|
+
def __init__(self, connection: Connection) -> None:
|
|
93
|
+
if connection.reference.name != CONNECTION_NAME:
|
|
94
|
+
raise ValueError("Cohere search requires the static cohere connection")
|
|
95
|
+
self._start = _StartRequest()
|
|
96
|
+
self._after_embedding = _AfterEmbedding()
|
|
97
|
+
self._complete = _CompleteSearch()
|
|
98
|
+
self._embed = new_embed_step(
|
|
99
|
+
step_type=_EMBED_STEP_TYPE,
|
|
100
|
+
step_input_type=SearchRequest,
|
|
101
|
+
annotations={**_ANNOTATION_GROUP, "explanation": "Embed the search query."},
|
|
102
|
+
connection=connection,
|
|
103
|
+
connection_name=CONNECTION_NAME,
|
|
104
|
+
map_to_operation_input=map_to_embed_input,
|
|
105
|
+
embedded=self._after_embedding,
|
|
106
|
+
)
|
|
107
|
+
self._rerank = new_rerank_step(
|
|
108
|
+
step_type=_RERANK_STEP_TYPE,
|
|
109
|
+
step_input_type=SearchRequest,
|
|
110
|
+
annotations={**_ANNOTATION_GROUP, "explanation": "Rank the candidate documents."},
|
|
111
|
+
connection=connection,
|
|
112
|
+
connection_name=CONNECTION_NAME,
|
|
113
|
+
map_to_operation_input=map_to_rerank_input,
|
|
114
|
+
ranked=self._complete,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def get_flow_type(self) -> str:
|
|
118
|
+
"""Return the stable Flow identity used in Dex Web."""
|
|
119
|
+
return FLOW_TYPE
|
|
120
|
+
|
|
121
|
+
def get_steps(self) -> dex.StepList[SearchRequest]:
|
|
122
|
+
"""Register the start, both Cohere calls, and their result Steps."""
|
|
123
|
+
return dex.StepList.start_step(self._start).other_steps(
|
|
124
|
+
self._embed, self._after_embedding, self._rerank, self._complete
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def get_persistence_schema(self) -> dex.PersistenceSchema:
|
|
128
|
+
"""Persist the search request between the two calls."""
|
|
129
|
+
return dex.PersistenceSchema.of(_REQUEST)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Worker entrypoint for the Cohere search Flow."""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import dex
|
|
11
|
+
from dex_connectors.sdk.projectconfig import load_from_environment
|
|
12
|
+
|
|
13
|
+
from dex_connectors.cohere import new_project_connection
|
|
14
|
+
|
|
15
|
+
from .flow import CONNECTION_NAME, SearchFlow
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def run_worker() -> None:
|
|
19
|
+
"""Load the named Dex Web connection and serve searches."""
|
|
20
|
+
project = await load_from_environment()
|
|
21
|
+
connection = None
|
|
22
|
+
cache = None
|
|
23
|
+
try:
|
|
24
|
+
connection = new_project_connection(project, CONNECTION_NAME)
|
|
25
|
+
registry = dex.Registry([SearchFlow(connection)], allow_async_handlers=True)
|
|
26
|
+
cache_directory = os.environ.get(
|
|
27
|
+
"DEX_BLOB_CACHE_DIR", str(Path(tempfile.gettempdir()) / "dex-cohere-search-blobs")
|
|
28
|
+
)
|
|
29
|
+
cache = dex.open_blob_cache(dex.BlobCacheConfig(cache_directory, 1 << 30))
|
|
30
|
+
worker = dex.AsyncWorker(
|
|
31
|
+
registry,
|
|
32
|
+
cache,
|
|
33
|
+
dex.WorkerOptions(server_address=os.environ.get("DEX_FLOW_SERVICE_ADDRESS", "localhost:8801")),
|
|
34
|
+
)
|
|
35
|
+
async with worker:
|
|
36
|
+
await worker.start()
|
|
37
|
+
finally:
|
|
38
|
+
if connection is not None:
|
|
39
|
+
await connection.aclose()
|
|
40
|
+
if cache is not None:
|
|
41
|
+
cache.close()
|
|
42
|
+
await project.close()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def main() -> None:
|
|
46
|
+
"""Run until the Worker stops or the process receives an interrupt."""
|
|
47
|
+
asyncio.run(run_worker())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
main()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dex-connector-cohere"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Cohere connector for Dex Flows"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
dependencies = ["dex-connectors-sdk==0.1.0", "dex-python-sdk>=1.28.0", "httpx>=0.27"]
|
|
13
|
+
|
|
14
|
+
[dependency-groups]
|
|
15
|
+
dev = ["pytest>=8", "pytest-asyncio>=0.24", "mypy>=1.13", "ruff>=0.7", "pyyaml>=6", "types-PyYAML>=6"]
|
|
16
|
+
|
|
17
|
+
[tool.hatch.build.targets.wheel]
|
|
18
|
+
packages = ["src/dex_connectors"]
|
|
19
|
+
|
|
20
|
+
[tool.pytest.ini_options]
|
|
21
|
+
testpaths = ["tests"]
|
|
22
|
+
pythonpath = [".", "src"]
|
|
23
|
+
asyncio_mode = "auto"
|
|
24
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
25
|
+
|
|
26
|
+
[tool.mypy]
|
|
27
|
+
python_version = "3.11"
|
|
28
|
+
strict = true
|
|
29
|
+
mypy_path = "src"
|
|
30
|
+
explicit_package_bases = true
|
|
31
|
+
|
|
32
|
+
[tool.ruff]
|
|
33
|
+
target-version = "py311"
|
|
34
|
+
line-length = 110
|
|
35
|
+
|
|
36
|
+
[tool.ruff.lint]
|
|
37
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Cohere embedding and rerank Queries for Dex connector Steps."""
|
|
4
|
+
|
|
5
|
+
from .client import (
|
|
6
|
+
CONNECTOR_ID,
|
|
7
|
+
DEFECT,
|
|
8
|
+
EMBED_DEFINITION,
|
|
9
|
+
EMBEDDED,
|
|
10
|
+
INPUT_TYPES,
|
|
11
|
+
INVALID_RESPONSE,
|
|
12
|
+
PROVIDER_REJECTED,
|
|
13
|
+
RANKED,
|
|
14
|
+
RERANK_DEFINITION,
|
|
15
|
+
Client,
|
|
16
|
+
Config,
|
|
17
|
+
Credentials,
|
|
18
|
+
EmbedOperation,
|
|
19
|
+
RerankOperation,
|
|
20
|
+
)
|
|
21
|
+
from .connection import Connection, new_connection, new_connection_with_operations, new_project_connection
|
|
22
|
+
from .models import EmbedInput, EmbedOutput, RankedDocument, RerankInput, RerankOutput
|
|
23
|
+
from .steps import new_embed_step, new_rerank_step
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"Client",
|
|
27
|
+
"Config",
|
|
28
|
+
"Connection",
|
|
29
|
+
"Credentials",
|
|
30
|
+
"EmbedInput",
|
|
31
|
+
"EmbedOperation",
|
|
32
|
+
"EmbedOutput",
|
|
33
|
+
"RankedDocument",
|
|
34
|
+
"RerankInput",
|
|
35
|
+
"RerankOperation",
|
|
36
|
+
"RerankOutput",
|
|
37
|
+
"new_connection",
|
|
38
|
+
"new_connection_with_operations",
|
|
39
|
+
"new_embed_step",
|
|
40
|
+
"new_project_connection",
|
|
41
|
+
"new_rerank_step",
|
|
42
|
+
"CONNECTOR_ID",
|
|
43
|
+
"DEFECT",
|
|
44
|
+
"EMBED_DEFINITION",
|
|
45
|
+
"EMBEDDED",
|
|
46
|
+
"INPUT_TYPES",
|
|
47
|
+
"INVALID_RESPONSE",
|
|
48
|
+
"PROVIDER_REJECTED",
|
|
49
|
+
"RANKED",
|
|
50
|
+
"RERANK_DEFINITION",
|
|
51
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# ruff: noqa
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from dex_connectors.sdk import SecretString
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class Config:
|
|
12
|
+
|
|
13
|
+
base_url: str = "https://api.cohere.com"
|
|
14
|
+
timeout_seconds: float = 30
|
|
15
|
+
max_response_bytes: int = 4194304
|
|
16
|
+
|
|
17
|
+
setattr(Config, '__module__', 'dex_connectors.cohere.client')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class Credentials:
|
|
22
|
+
|
|
23
|
+
api_key: SecretString
|
|
24
|
+
|
|
25
|
+
setattr(Credentials, '__module__', 'dex_connectors.cohere.client')
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# ruff: noqa
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from dex_connectors.sdk import ConnectionRef, Query, Call, SecretString
|
|
9
|
+
from dex_connectors.sdk.projectconfig import ConnectionKey, LoadedProject
|
|
10
|
+
|
|
11
|
+
from ..client import Client, Config, Credentials
|
|
12
|
+
from .definitions import CONNECTOR_ID, EMBED_DEFINITION, RERANK_DEFINITION
|
|
13
|
+
from ..models import EmbedInput, EmbedOutput, RerankInput, RerankOutput
|
|
14
|
+
|
|
15
|
+
class Connection:
|
|
16
|
+
|
|
17
|
+
__slots__ = ('_embed', '_rerank', '_client', 'reference')
|
|
18
|
+
|
|
19
|
+
def __init__(self,
|
|
20
|
+
reference: ConnectionRef,
|
|
21
|
+
embed: Query[EmbedInput, EmbedOutput],
|
|
22
|
+
rerank: Query[RerankInput, RerankOutput],
|
|
23
|
+
client: Client | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
reference.validate()
|
|
26
|
+
if reference.provider != CONNECTOR_ID or embed is None or rerank is None:
|
|
27
|
+
raise ValueError("Cohere connection provider and operations are required")
|
|
28
|
+
if embed.definition().operation != EMBED_DEFINITION.operation:
|
|
29
|
+
raise ValueError("Cohere connection operation identity differs")
|
|
30
|
+
if rerank.definition().operation != RERANK_DEFINITION.operation:
|
|
31
|
+
raise ValueError("Cohere connection operation identity differs")
|
|
32
|
+
self.reference = reference
|
|
33
|
+
self._embed = embed
|
|
34
|
+
self._rerank = rerank
|
|
35
|
+
self._client = client
|
|
36
|
+
|
|
37
|
+
def embed(self) -> Query[EmbedInput, EmbedOutput]:
|
|
38
|
+
return self._embed
|
|
39
|
+
|
|
40
|
+
def rerank(self) -> Query[RerankInput, RerankOutput]:
|
|
41
|
+
return self._rerank
|
|
42
|
+
|
|
43
|
+
async def aclose(self) -> None:
|
|
44
|
+
if self._client is not None:
|
|
45
|
+
await self._client.aclose()
|
|
46
|
+
|
|
47
|
+
def __repr__(self) -> str:
|
|
48
|
+
return "cohere.Connection([REDACTED])"
|
|
49
|
+
|
|
50
|
+
def __reduce__(self) -> tuple[object, ...]:
|
|
51
|
+
raise TypeError("Cohere connections cannot be serialized")
|
|
52
|
+
|
|
53
|
+
setattr(Connection, '__module__', 'dex_connectors.cohere.connection')
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def new_connection(client: Client, reference: ConnectionRef) -> Connection:
|
|
57
|
+
if client is None:
|
|
58
|
+
raise ValueError("Cohere client is required")
|
|
59
|
+
return Connection(reference, client.embed(), client.rerank(), client)
|
|
60
|
+
|
|
61
|
+
setattr(new_connection, '__module__', 'dex_connectors.cohere.connection')
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def new_connection_with_operations(
|
|
65
|
+
embed: Query[EmbedInput, EmbedOutput],
|
|
66
|
+
rerank: Query[RerankInput, RerankOutput],
|
|
67
|
+
reference: ConnectionRef,
|
|
68
|
+
) -> Connection:
|
|
69
|
+
return Connection(reference, embed, rerank)
|
|
70
|
+
|
|
71
|
+
setattr(new_connection_with_operations, '__module__', 'dex_connectors.cohere.connection')
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class _ProjectCredentialSource:
|
|
75
|
+
|
|
76
|
+
def __init__(self, project: LoadedProject, key: ConnectionKey) -> None:
|
|
77
|
+
self.project = project
|
|
78
|
+
self.key = key
|
|
79
|
+
|
|
80
|
+
async def resolve(self, call: Call) -> Credentials:
|
|
81
|
+
material = await self.project.resolve_credential(self.key, str(call.id))
|
|
82
|
+
api_key = material.get("api_key")
|
|
83
|
+
if not isinstance(api_key, str) or not api_key:
|
|
84
|
+
raise ValueError("Cohere credential api_key is required")
|
|
85
|
+
return Credentials(SecretString(api_key))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def new_project_connection(
|
|
89
|
+
project: LoadedProject, connection_name: str, http_client: httpx.AsyncClient | None = None
|
|
90
|
+
) -> Connection:
|
|
91
|
+
if project is None:
|
|
92
|
+
raise ValueError("Cohere connection requires loaded project configuration")
|
|
93
|
+
reference = ConnectionRef(CONNECTOR_ID, connection_name)
|
|
94
|
+
reference.validate()
|
|
95
|
+
key = ConnectionKey(CONNECTOR_ID, connection_name)
|
|
96
|
+
matching = [
|
|
97
|
+
entry
|
|
98
|
+
for entry in project.configuration.connections
|
|
99
|
+
if entry.connector_id == CONNECTOR_ID and entry.connection_name == connection_name
|
|
100
|
+
]
|
|
101
|
+
if len(matching) != 1:
|
|
102
|
+
raise ValueError("Cohere project connection is missing or duplicated")
|
|
103
|
+
if matching[0].provider != CONNECTOR_ID:
|
|
104
|
+
raise ValueError("Cohere project connection provider differs")
|
|
105
|
+
config = _decode_config(matching[0].configuration)
|
|
106
|
+
client = Client(config, _ProjectCredentialSource(project, key), http_client)
|
|
107
|
+
return new_connection(client, reference)
|
|
108
|
+
|
|
109
|
+
setattr(new_project_connection, '__module__', 'dex_connectors.cohere.connection')
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _decode_config(value: dict[str, Any]) -> Config:
|
|
113
|
+
if set(value) - {"baseUrl", "timeout", "maxResponseBytes"}:
|
|
114
|
+
raise ValueError("Cohere configuration has unknown fields")
|
|
115
|
+
config_arguments: dict[str, Any] = {}
|
|
116
|
+
if "baseUrl" in value:
|
|
117
|
+
base_url = value["baseUrl"]
|
|
118
|
+
if not (isinstance(base_url, str)):
|
|
119
|
+
raise ValueError("Cohere configuration baseUrl has an invalid type")
|
|
120
|
+
config_arguments['base_url'] = base_url
|
|
121
|
+
if "timeout" in value:
|
|
122
|
+
timeout = value["timeout"]
|
|
123
|
+
if not (type(timeout) is int):
|
|
124
|
+
raise ValueError("Cohere configuration timeout has an invalid type")
|
|
125
|
+
config_arguments['timeout_seconds'] = timeout / 1_000_000_000
|
|
126
|
+
if "maxResponseBytes" in value:
|
|
127
|
+
max_response_bytes = value["maxResponseBytes"]
|
|
128
|
+
if not (type(max_response_bytes) is int):
|
|
129
|
+
raise ValueError("Cohere configuration maxResponseBytes has an invalid type")
|
|
130
|
+
config_arguments['max_response_bytes'] = max_response_bytes
|
|
131
|
+
return Config(**config_arguments)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# ruff: noqa
|
|
4
|
+
|
|
5
|
+
from datetime import timedelta
|
|
6
|
+
|
|
7
|
+
from dex_connectors.sdk import (
|
|
8
|
+
BranchDefinition,
|
|
9
|
+
MutationDefinition,
|
|
10
|
+
OperationRef,
|
|
11
|
+
QueryDefinition,
|
|
12
|
+
RetryPolicy,
|
|
13
|
+
StepDefaults,
|
|
14
|
+
default_connector_step_execute_retry_policy,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
CONNECTOR_ID = "cohere"
|
|
18
|
+
PROVIDER = "cohere"
|
|
19
|
+
EMBED_OPERATION_ID = "embed"
|
|
20
|
+
RERANK_OPERATION_ID = "rerank"
|
|
21
|
+
DEFECT = "defect"
|
|
22
|
+
EMBEDDED = "embedded"
|
|
23
|
+
INVALID_RESPONSE = "invalidResponse"
|
|
24
|
+
PROVIDER_REJECTED = "providerRejected"
|
|
25
|
+
RANKED = "ranked"
|
|
26
|
+
|
|
27
|
+
EMBED_DEFINITION = QueryDefinition(
|
|
28
|
+
OperationRef(CONNECTOR_ID, EMBED_OPERATION_ID),
|
|
29
|
+
(
|
|
30
|
+
BranchDefinition(EMBEDDED, "Cohere returned one float vector per text.", False),
|
|
31
|
+
BranchDefinition(PROVIDER_REJECTED, "Cohere conclusively rejected the embedding request.", True),
|
|
32
|
+
BranchDefinition(INVALID_RESPONSE, "Cohere returned malformed or oversized embedding data.", True),
|
|
33
|
+
BranchDefinition(DEFECT, "Local input or connection credentials are invalid.", True),
|
|
34
|
+
),
|
|
35
|
+
StepDefaults(
|
|
36
|
+
timedelta(seconds=45),
|
|
37
|
+
2,
|
|
38
|
+
execute_retry=RetryPolicy(timedelta(seconds=2), 2, timedelta(seconds=30), 3, timedelta(seconds=300)),
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
RERANK_DEFINITION = QueryDefinition(
|
|
43
|
+
OperationRef(CONNECTOR_ID, RERANK_OPERATION_ID),
|
|
44
|
+
(
|
|
45
|
+
BranchDefinition(RANKED, "Cohere returned ranked document indices and scores.", False),
|
|
46
|
+
BranchDefinition(PROVIDER_REJECTED, "Cohere conclusively rejected the rerank request.", True),
|
|
47
|
+
BranchDefinition(INVALID_RESPONSE, "Cohere returned malformed or oversized rerank data.", True),
|
|
48
|
+
BranchDefinition(DEFECT, "Local input or connection credentials are invalid.", True),
|
|
49
|
+
),
|
|
50
|
+
StepDefaults(
|
|
51
|
+
timedelta(seconds=45),
|
|
52
|
+
2,
|
|
53
|
+
execute_retry=RetryPolicy(timedelta(seconds=2), 2, timedelta(seconds=30), 3, timedelta(seconds=300)),
|
|
54
|
+
),
|
|
55
|
+
)
|