From 4de0730c40e8e5ed6b37ddcd1f88ef3e52dc5ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Boschi?= Date: Thu, 8 Jan 2026 11:41:15 +0100 Subject: [PATCH] feat: support cohere as embeddings and reranker (#122) --- .github/workflows/test.yml | 1 + hindsight-api/hindsight_api/config.py | 7 + .../hindsight_api/engine/cross_encoder.py | 101 +++++++++- .../hindsight_api/engine/embeddings.py | 128 ++++++++++++- hindsight-api/pyproject.toml | 1 + .../tests/test_custom_embedding_dimension.py | 178 +++++++++++++++++- .../docs/developer/configuration.md | 17 +- uv.lock | 74 ++++++++ 8 files changed, 501 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b4f2ea49..e084b006 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -325,6 +325,7 @@ jobs: GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }} HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/hindsight-api/hindsight_api/config.py b/hindsight-api/hindsight_api/config.py index c3a969f5..bdf5921c 100644 --- a/hindsight-api/hindsight_api/config.py +++ b/hindsight-api/hindsight_api/config.py @@ -31,6 +31,10 @@ ENV_EMBEDDINGS_TEI_URL = "HINDSIGHT_API_EMBEDDINGS_TEI_URL" ENV_EMBEDDINGS_OPENAI_API_KEY = "HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY" ENV_EMBEDDINGS_OPENAI_MODEL = "HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL" +ENV_COHERE_API_KEY = "HINDSIGHT_API_COHERE_API_KEY" +ENV_EMBEDDINGS_COHERE_MODEL = "HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL" +ENV_RERANKER_COHERE_MODEL = "HINDSIGHT_API_RERANKER_COHERE_MODEL" + ENV_RERANKER_PROVIDER = "HINDSIGHT_API_RERANKER_PROVIDER" ENV_RERANKER_LOCAL_MODEL = "HINDSIGHT_API_RERANKER_LOCAL_MODEL" ENV_RERANKER_TEI_URL = "HINDSIGHT_API_RERANKER_TEI_URL" @@ -72,6 +76,9 @@ DEFAULT_EMBEDDING_DIMENSION = 384 DEFAULT_RERANKER_PROVIDER = "local" DEFAULT_RERANKER_LOCAL_MODEL = "cross-encoder/ms-marco-MiniLM-L-6-v2" +DEFAULT_EMBEDDINGS_COHERE_MODEL = "embed-english-v3.0" +DEFAULT_RERANKER_COHERE_MODEL = "rerank-english-v3.0" + DEFAULT_HOST = "0.0.0.0" DEFAULT_PORT = 8888 DEFAULT_LOG_LEVEL = "info" diff --git a/hindsight-api/hindsight_api/engine/cross_encoder.py b/hindsight-api/hindsight_api/engine/cross_encoder.py index c91c2954..6248d886 100644 --- a/hindsight-api/hindsight_api/engine/cross_encoder.py +++ b/hindsight-api/hindsight_api/engine/cross_encoder.py @@ -13,8 +13,11 @@ from abc import ABC, abstractmethod import httpx from ..config import ( + DEFAULT_RERANKER_COHERE_MODEL, DEFAULT_RERANKER_LOCAL_MODEL, DEFAULT_RERANKER_PROVIDER, + ENV_COHERE_API_KEY, + ENV_RERANKER_COHERE_MODEL, ENV_RERANKER_LOCAL_MODEL, ENV_RERANKER_PROVIDER, ENV_RERANKER_TEI_URL, @@ -278,6 +281,96 @@ class RemoteTEICrossEncoder(CrossEncoderModel): return all_scores +class CohereCrossEncoder(CrossEncoderModel): + """ + Cohere cross-encoder implementation using the Cohere Rerank API. + + Supports rerank-english-v3.0 and rerank-multilingual-v3.0 models. + """ + + def __init__( + self, + api_key: str, + model: str = DEFAULT_RERANKER_COHERE_MODEL, + timeout: float = 60.0, + ): + """ + Initialize Cohere cross-encoder client. + + Args: + api_key: Cohere API key + model: Cohere rerank model name (default: rerank-english-v3.0) + timeout: Request timeout in seconds (default: 60.0) + """ + self.api_key = api_key + self.model = model + self.timeout = timeout + self._client = None + + @property + def provider_name(self) -> str: + return "cohere" + + async def initialize(self) -> None: + """Initialize the Cohere client.""" + if self._client is not None: + return + + try: + import cohere + except ImportError: + raise ImportError("cohere is required for CohereCrossEncoder. Install it with: pip install cohere") + + logger.info(f"Reranker: initializing Cohere provider with model {self.model}") + self._client = cohere.Client(api_key=self.api_key, timeout=self.timeout) + logger.info("Reranker: Cohere provider initialized") + + def predict(self, pairs: list[tuple[str, str]]) -> list[float]: + """ + Score query-document pairs using the Cohere Rerank API. + + Args: + pairs: List of (query, document) tuples to score + + Returns: + List of relevance scores + """ + if self._client is None: + raise RuntimeError("Reranker not initialized. Call initialize() first.") + + if not pairs: + return [] + + # Group pairs by query for efficient batching + # Cohere rerank expects one query with multiple documents + query_groups: dict[str, list[tuple[int, str]]] = {} + for idx, (query, text) in enumerate(pairs): + if query not in query_groups: + query_groups[query] = [] + query_groups[query].append((idx, text)) + + all_scores = [0.0] * len(pairs) + + for query, indexed_texts in query_groups.items(): + texts = [text for _, text in indexed_texts] + indices = [idx for idx, _ in indexed_texts] + + response = self._client.rerank( + query=query, + documents=texts, + model=self.model, + return_documents=False, + ) + + # Map scores back to original positions + for result in response.results: + original_idx = result.index + score = result.relevance_score + all_scores[indices[original_idx]] = score + + return all_scores + + def create_cross_encoder_from_env() -> CrossEncoderModel: """ Create a CrossEncoderModel instance based on environment variables. @@ -298,5 +391,11 @@ def create_cross_encoder_from_env() -> CrossEncoderModel: model = os.environ.get(ENV_RERANKER_LOCAL_MODEL) model_name = model or DEFAULT_RERANKER_LOCAL_MODEL return LocalSTCrossEncoder(model_name=model_name) + elif provider == "cohere": + api_key = os.environ.get(ENV_COHERE_API_KEY) + if not api_key: + raise ValueError(f"{ENV_COHERE_API_KEY} is required when {ENV_RERANKER_PROVIDER} is 'cohere'") + model = os.environ.get(ENV_RERANKER_COHERE_MODEL, DEFAULT_RERANKER_COHERE_MODEL) + return CohereCrossEncoder(api_key=api_key, model=model) else: - raise ValueError(f"Unknown reranker provider: {provider}. Supported: 'local', 'tei'") + raise ValueError(f"Unknown reranker provider: {provider}. Supported: 'local', 'tei', 'cohere'") diff --git a/hindsight-api/hindsight_api/engine/embeddings.py b/hindsight-api/hindsight_api/engine/embeddings.py index c11b9bdb..3a4c953b 100644 --- a/hindsight-api/hindsight_api/engine/embeddings.py +++ b/hindsight-api/hindsight_api/engine/embeddings.py @@ -16,9 +16,12 @@ from abc import ABC, abstractmethod import httpx from ..config import ( + DEFAULT_EMBEDDINGS_COHERE_MODEL, DEFAULT_EMBEDDINGS_LOCAL_MODEL, DEFAULT_EMBEDDINGS_OPENAI_MODEL, DEFAULT_EMBEDDINGS_PROVIDER, + ENV_COHERE_API_KEY, + ENV_EMBEDDINGS_COHERE_MODEL, ENV_EMBEDDINGS_LOCAL_MODEL, ENV_EMBEDDINGS_OPENAI_API_KEY, ENV_EMBEDDINGS_OPENAI_MODEL, @@ -409,6 +412,123 @@ class OpenAIEmbeddings(Embeddings): return all_embeddings +class CohereEmbeddings(Embeddings): + """ + Cohere embeddings implementation using the Cohere API. + + Supports embed-english-v3.0 (1024 dims) and embed-multilingual-v3.0 (1024 dims). + + The embedding dimension is auto-detected from the model at initialization. + """ + + # Known dimensions for Cohere embedding models + MODEL_DIMENSIONS = { + "embed-english-v3.0": 1024, + "embed-multilingual-v3.0": 1024, + "embed-english-light-v3.0": 384, + "embed-multilingual-light-v3.0": 384, + "embed-english-v2.0": 4096, + "embed-multilingual-v2.0": 768, + } + + def __init__( + self, + api_key: str, + model: str = DEFAULT_EMBEDDINGS_COHERE_MODEL, + batch_size: int = 96, + timeout: float = 60.0, + input_type: str = "search_document", + ): + """ + Initialize Cohere embeddings client. + + Args: + api_key: Cohere API key + model: Cohere embedding model name (default: embed-english-v3.0) + batch_size: Maximum batch size for embedding requests (default: 96, Cohere's limit) + timeout: Request timeout in seconds (default: 60.0) + input_type: Input type for embeddings (default: search_document). + Options: search_document, search_query, classification, clustering + """ + self.api_key = api_key + self.model = model + self.batch_size = batch_size + self.timeout = timeout + self.input_type = input_type + self._client = None + self._dimension: int | None = None + + @property + def provider_name(self) -> str: + return "cohere" + + @property + def dimension(self) -> int: + if self._dimension is None: + raise RuntimeError("Embeddings not initialized. Call initialize() first.") + return self._dimension + + async def initialize(self) -> None: + """Initialize the Cohere client and detect dimension.""" + if self._client is not None: + return + + try: + import cohere + except ImportError: + raise ImportError("cohere is required for CohereEmbeddings. Install it with: pip install cohere") + + logger.info(f"Embeddings: initializing Cohere provider with model {self.model}") + self._client = cohere.Client(api_key=self.api_key, timeout=self.timeout) + + # Try to get dimension from known models, otherwise do a test embedding + if self.model in self.MODEL_DIMENSIONS: + self._dimension = self.MODEL_DIMENSIONS[self.model] + else: + # Do a test embedding to detect dimension + response = self._client.embed( + texts=["test"], + model=self.model, + input_type=self.input_type, + ) + if response.embeddings: + self._dimension = len(response.embeddings[0]) + + logger.info(f"Embeddings: Cohere provider initialized (model: {self.model}, dim: {self._dimension})") + + def encode(self, texts: list[str]) -> list[list[float]]: + """ + Generate embeddings using the Cohere API. + + Args: + texts: List of text strings to encode + + Returns: + List of embedding vectors + """ + if self._client is None: + raise RuntimeError("Embeddings not initialized. Call initialize() first.") + + if not texts: + return [] + + all_embeddings = [] + + # Process in batches + for i in range(0, len(texts), self.batch_size): + batch = texts[i : i + self.batch_size] + + response = self._client.embed( + texts=batch, + model=self.model, + input_type=self.input_type, + ) + + all_embeddings.extend(response.embeddings) + + return all_embeddings + + def create_embeddings_from_env() -> Embeddings: """ Create an Embeddings instance based on environment variables. @@ -439,5 +559,11 @@ def create_embeddings_from_env() -> Embeddings: ) model = os.environ.get(ENV_EMBEDDINGS_OPENAI_MODEL, DEFAULT_EMBEDDINGS_OPENAI_MODEL) return OpenAIEmbeddings(api_key=api_key, model=model) + elif provider == "cohere": + api_key = os.environ.get(ENV_COHERE_API_KEY) + if not api_key: + raise ValueError(f"{ENV_COHERE_API_KEY} is required when {ENV_EMBEDDINGS_PROVIDER} is 'cohere'") + model = os.environ.get(ENV_EMBEDDINGS_COHERE_MODEL, DEFAULT_EMBEDDINGS_COHERE_MODEL) + return CohereEmbeddings(api_key=api_key, model=model) else: - raise ValueError(f"Unknown embeddings provider: {provider}. Supported: 'local', 'tei', 'openai'") + raise ValueError(f"Unknown embeddings provider: {provider}. Supported: 'local', 'tei', 'openai', 'cohere'") diff --git a/hindsight-api/pyproject.toml b/hindsight-api/pyproject.toml index 8d3c5d3e..03aa6272 100644 --- a/hindsight-api/pyproject.toml +++ b/hindsight-api/pyproject.toml @@ -39,6 +39,7 @@ dependencies = [ "google-genai>=1.0.0", "anthropic>=0.40.0", "typer>=0.9.0", + "cohere>=5.0.0", ] [project.optional-dependencies] diff --git a/hindsight-api/tests/test_custom_embedding_dimension.py b/hindsight-api/tests/test_custom_embedding_dimension.py index fbfcdeea..ba1c034e 100644 --- a/hindsight-api/tests/test_custom_embedding_dimension.py +++ b/hindsight-api/tests/test_custom_embedding_dimension.py @@ -14,8 +14,8 @@ from datetime import datetime from sqlalchemy import create_engine, text from hindsight_api import MemoryEngine, RequestContext -from hindsight_api.engine.embeddings import LocalSTEmbeddings, OpenAIEmbeddings -from hindsight_api.engine.cross_encoder import LocalSTCrossEncoder +from hindsight_api.engine.embeddings import LocalSTEmbeddings, OpenAIEmbeddings, CohereEmbeddings +from hindsight_api.engine.cross_encoder import LocalSTCrossEncoder, CohereCrossEncoder from hindsight_api.engine.query_analyzer import DateparserQueryAnalyzer from hindsight_api.extensions import TenantExtension, TenantContext from hindsight_api.migrations import run_migrations, ensure_embedding_dimension @@ -426,3 +426,177 @@ class TestOpenAIEmbeddings: await memory.close() except Exception: pass + + +# ============================================================================= +# Cohere Embeddings Tests +# ============================================================================= + + +def has_cohere_api_key() -> bool: + """Check if Cohere API key is available.""" + return bool(os.environ.get("COHERE_API_KEY")) + + +def get_cohere_api_key() -> str: + """Get Cohere API key from environment.""" + return os.environ.get("COHERE_API_KEY", "") + + +@pytest.fixture(scope="module") +def cohere_embeddings(): + """Create Cohere embeddings instance.""" + if not has_cohere_api_key(): + pytest.skip("Cohere API key not available (set COHERE_API_KEY)") + + embeddings = CohereEmbeddings( + api_key=get_cohere_api_key(), + model="embed-english-v3.0", + ) + loop = asyncio.new_event_loop() + try: + loop.run_until_complete(embeddings.initialize()) + finally: + loop.close() + return embeddings + + +@pytest.fixture(scope="module") +def cohere_cross_encoder(): + """Create Cohere cross-encoder instance.""" + if not has_cohere_api_key(): + pytest.skip("Cohere API key not available (set COHERE_API_KEY)") + + cross_encoder = CohereCrossEncoder( + api_key=get_cohere_api_key(), + model="rerank-english-v3.0", + ) + loop = asyncio.new_event_loop() + try: + loop.run_until_complete(cross_encoder.initialize()) + finally: + loop.close() + return cross_encoder + + +@pytest.fixture(scope="module") +def cohere_test_schema(pg0_db_url, worker_id, cohere_embeddings): + """Create an isolated schema for Cohere embedding tests.""" + schema_name = get_test_schema("test_cohere_embed", worker_id) + create_isolated_schema(pg0_db_url, schema_name, dimension=cohere_embeddings.dimension) + yield pg0_db_url, schema_name + drop_schema(pg0_db_url, schema_name) + + +class TestCohereEmbeddings: + """Tests for Cohere embeddings provider.""" + + def test_cohere_embeddings_initialization(self, cohere_embeddings): + """Test that Cohere embeddings initializes correctly.""" + assert cohere_embeddings.dimension == 1024 + assert cohere_embeddings.provider_name == "cohere" + + def test_cohere_embeddings_encode(self, cohere_embeddings): + """Test that Cohere embeddings can encode text.""" + texts = ["Hello, world!", "This is a test."] + embeddings = cohere_embeddings.encode(texts) + + assert len(embeddings) == 2 + assert len(embeddings[0]) == 1024 + assert len(embeddings[1]) == 1024 + assert all(isinstance(x, float) for x in embeddings[0]) + + +class TestCohereCrossEncoder: + """Tests for Cohere cross-encoder/reranker.""" + + def test_cohere_cross_encoder_initialization(self, cohere_cross_encoder): + """Test that Cohere cross-encoder initializes correctly.""" + assert cohere_cross_encoder.provider_name == "cohere" + + def test_cohere_cross_encoder_predict(self, cohere_cross_encoder): + """Test that Cohere cross-encoder can score pairs.""" + pairs = [ + ("What is the capital of France?", "Paris is the capital of France."), + ("What is the capital of France?", "The Eiffel Tower is in Paris."), + ("What is the capital of France?", "Python is a programming language."), + ] + scores = cohere_cross_encoder.predict(pairs) + + assert len(scores) == 3 + assert all(isinstance(s, float) for s in scores) + # The first result should be most relevant + assert scores[0] > scores[2], "Direct answer should score higher than unrelated text" + + +class TestCohereIntegration: + """Integration tests for Cohere embeddings with memory engine.""" + + @pytest.mark.asyncio + async def test_cohere_embeddings_retain_recall( + self, + cohere_test_schema, + cohere_embeddings, + cohere_cross_encoder, + query_analyzer, + request_context, + ): + """Test retain and recall operations with Cohere embeddings.""" + db_url, schema_name = cohere_test_schema + test_bank_id = f"cohere_test_{datetime.now().timestamp()}" + + memory = MemoryEngine( + db_url=db_url, + memory_llm_provider=os.getenv("HINDSIGHT_API_LLM_PROVIDER", "groq"), + memory_llm_api_key=os.getenv("HINDSIGHT_API_LLM_API_KEY"), + memory_llm_model=os.getenv("HINDSIGHT_API_LLM_MODEL", "openai/gpt-oss-120b"), + memory_llm_base_url=os.getenv("HINDSIGHT_API_LLM_BASE_URL") or None, + embeddings=cohere_embeddings, + cross_encoder=cohere_cross_encoder, + query_analyzer=query_analyzer, + pool_min_size=1, + pool_max_size=3, + run_migrations=False, + tenant_extension=SchemaTenantExtension(schema_name), + ) + + try: + await memory.initialize() + + # Store some memories + await memory.retain_async( + bank_id=test_bank_id, + content="Alice works as a software engineer at Google.", + context="career discussion", + request_context=request_context, + ) + + await memory.retain_async( + bank_id=test_bank_id, + content="Bob is a data scientist specializing in machine learning.", + context="team introductions", + request_context=request_context, + ) + + # Recall memories + result = await memory.recall_async( + bank_id=test_bank_id, + query="Who works in technology?", + request_context=request_context, + ) + + assert result is not None + assert len(result.results) > 0 + + memory_texts = [m.text for m in result.results] + assert any( + "Alice" in text or "Bob" in text or "software" in text or "data scientist" in text + for text in memory_texts + ), f"Expected to find relevant memories, got: {memory_texts}" + + finally: + try: + if memory._pool and not memory._pool._closing: + await memory.close() + except Exception: + pass diff --git a/hindsight-docs/docs/developer/configuration.md b/hindsight-docs/docs/developer/configuration.md index 43135449..a1e5d017 100644 --- a/hindsight-docs/docs/developer/configuration.md +++ b/hindsight-docs/docs/developer/configuration.md @@ -90,11 +90,13 @@ export HINDSIGHT_API_LLM_MODEL=your-model-name | Variable | Description | Default | |----------|-------------|---------| -| `HINDSIGHT_API_EMBEDDINGS_PROVIDER` | Provider: `local`, `tei`, or `openai` | `local` | +| `HINDSIGHT_API_EMBEDDINGS_PROVIDER` | Provider: `local`, `tei`, `openai`, or `cohere` | `local` | | `HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL` | Model for local provider | `BAAI/bge-small-en-v1.5` | | `HINDSIGHT_API_EMBEDDINGS_TEI_URL` | TEI server URL | - | | `HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY` | OpenAI API key (falls back to `HINDSIGHT_API_LLM_API_KEY`) | - | | `HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL` | OpenAI embedding model | `text-embedding-3-small` | +| `HINDSIGHT_API_COHERE_API_KEY` | Cohere API key (shared for embeddings and reranker) | - | +| `HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL` | Cohere embedding model | `embed-english-v3.0` | ```bash # Local (default) - uses SentenceTransformers @@ -109,6 +111,11 @@ export HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL=text-embedding-3-small # 1536 dime # TEI - HuggingFace Text Embeddings Inference (recommended for production) export HINDSIGHT_API_EMBEDDINGS_PROVIDER=tei export HINDSIGHT_API_EMBEDDINGS_TEI_URL=http://localhost:8080 + +# Cohere - cloud-based embeddings +export HINDSIGHT_API_EMBEDDINGS_PROVIDER=cohere +export HINDSIGHT_API_COHERE_API_KEY=your-api-key +export HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL=embed-english-v3.0 # 1024 dimensions ``` #### Embedding Dimensions @@ -131,9 +138,10 @@ Supported OpenAI embedding dimensions: | Variable | Description | Default | |----------|-------------|---------| -| `HINDSIGHT_API_RERANKER_PROVIDER` | Provider: `local` or `tei` | `local` | +| `HINDSIGHT_API_RERANKER_PROVIDER` | Provider: `local`, `tei`, or `cohere` | `local` | | `HINDSIGHT_API_RERANKER_LOCAL_MODEL` | Model for local provider | `cross-encoder/ms-marco-MiniLM-L-6-v2` | | `HINDSIGHT_API_RERANKER_TEI_URL` | TEI server URL | - | +| `HINDSIGHT_API_RERANKER_COHERE_MODEL` | Cohere rerank model | `rerank-english-v3.0` | ```bash # Local (default) - uses SentenceTransformers CrossEncoder @@ -143,6 +151,11 @@ export HINDSIGHT_API_RERANKER_LOCAL_MODEL=cross-encoder/ms-marco-MiniLM-L-6-v2 # TEI - for high-performance inference export HINDSIGHT_API_RERANKER_PROVIDER=tei export HINDSIGHT_API_RERANKER_TEI_URL=http://localhost:8081 + +# Cohere - cloud-based reranking +export HINDSIGHT_API_RERANKER_PROVIDER=cohere +export HINDSIGHT_API_COHERE_API_KEY=your-api-key # shared with embeddings +export HINDSIGHT_API_RERANKER_COHERE_MODEL=rerank-english-v3.0 ``` ### Server diff --git a/uv.lock b/uv.lock index b64b4072..d700d1d4 100644 --- a/uv.lock +++ b/uv.lock @@ -593,6 +593,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295 }, ] +[[package]] +name = "cohere" +version = "5.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastavro" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "pydantic-core" }, + { name = "requests" }, + { name = "tokenizers" }, + { name = "types-requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/ed/bb02083654bdc089ae4ef1cd7691fd2233f1fd9f32bcbfacc80ff57d9775/cohere-5.20.1.tar.gz", hash = "sha256:50973f63d2c6138ff52ce37d8d6f78ccc539af4e8c43865e960d68e0bf835b6f", size = 180820 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/e3/94eb11ac3ebaaa3a6afb5d2ff23db95d58bc468ae538c388edf49f2f20b5/cohere-5.20.1-py3-none-any.whl", hash = "sha256:d230fd13d95ba92ae927fce3dd497599b169883afc7954fe29b39fb8d5df5fc7", size = 318973 }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -836,6 +855,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/79/7f5a5e5513e6a737e5fb089d9c59c74d4d24dc24d581d3aa519b326bedda/fastapi_cloud_cli-0.3.1-py3-none-any.whl", hash = "sha256:7d1a98a77791a9d0757886b2ffbf11bcc6b3be93210dd15064be10b216bf7e00", size = 19711 }, ] +[[package]] +name = "fastavro" +version = "1.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/8b/fa2d3287fd2267be6261d0177c6809a7fa12c5600ddb33490c8dc29e77b2/fastavro-1.12.1.tar.gz", hash = "sha256:2f285be49e45bc047ab2f6bed040bb349da85db3f3c87880e4b92595ea093b2b", size = 1025661 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e9/31c64b47cefc0951099e7c0c8c8ea1c931edd1350f34d55c27cbfbb08df1/fastavro-1.12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b632b713bc5d03928a87d811fa4a11d5f25cd43e79c161e291c7d3f7aa740fd", size = 1016585 }, + { url = "https://files.pythonhosted.org/packages/10/76/111560775b548f5d8d828c1b5285ff90e2d2745643fb80ecbf115344eea4/fastavro-1.12.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa7ab3769beadcebb60f0539054c7755f63bd9cf7666e2c15e615ab605f89a8", size = 3404629 }, + { url = "https://files.pythonhosted.org/packages/b0/07/6bb93cb963932146c2b6c5c765903a0a547ad9f0f8b769a4a9aad8c06369/fastavro-1.12.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123fb221df3164abd93f2d042c82f538a1d5a43ce41375f12c91ce1355a9141e", size = 3428594 }, + { url = "https://files.pythonhosted.org/packages/d1/67/8115ec36b584197ea737ec79e3499e1f1b640b288d6c6ee295edd13b80f6/fastavro-1.12.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:632a4e3ff223f834ddb746baae0cc7cee1068eb12c32e4d982c2fee8a5b483d0", size = 3344145 }, + { url = "https://files.pythonhosted.org/packages/9e/9e/a7cebb3af967e62539539897c10138fa0821668ec92525d1be88a9cd3ee6/fastavro-1.12.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e6caf4e7a8717d932a3b1ff31595ad169289bbe1128a216be070d3a8391671", size = 3431942 }, + { url = "https://files.pythonhosted.org/packages/c0/d1/7774ddfb8781c5224294c01a593ebce2ad3289b948061c9701bd1903264d/fastavro-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:b91a0fe5a173679a6c02d53ca22dcaad0a2c726b74507e0c1c2e71a7c3f79ef9", size = 450542 }, + { url = "https://files.pythonhosted.org/packages/7c/f0/10bd1a3d08667fa0739e2b451fe90e06df575ec8b8ba5d3135c70555c9bd/fastavro-1.12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:509818cb24b98a804fc80be9c5fed90f660310ae3d59382fc811bfa187122167", size = 1009057 }, + { url = "https://files.pythonhosted.org/packages/78/ad/0d985bc99e1fa9e74c636658000ba38a5cd7f5ab2708e9c62eaf736ecf1a/fastavro-1.12.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089e155c0c76e0d418d7e79144ce000524dd345eab3bc1e9c5ae69d500f71b14", size = 3391866 }, + { url = "https://files.pythonhosted.org/packages/0d/9e/b4951dc84ebc34aac69afcbfbb22ea4a91080422ec2bfd2c06076ff1d419/fastavro-1.12.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44cbff7518901c91a82aab476fcab13d102e4999499df219d481b9e15f61af34", size = 3458005 }, + { url = "https://files.pythonhosted.org/packages/af/f8/5a8df450a9f55ca8441f22ea0351d8c77809fc121498b6970daaaf667a21/fastavro-1.12.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a275e48df0b1701bb764b18a8a21900b24cf882263cb03d35ecdba636bbc830b", size = 3295258 }, + { url = "https://files.pythonhosted.org/packages/99/b2/40f25299111d737e58b85696e91138a66c25b7334f5357e7ac2b0e8966f8/fastavro-1.12.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2de72d786eb38be6b16d556b27232b1bf1b2797ea09599507938cdb7a9fe3e7c", size = 3430328 }, + { url = "https://files.pythonhosted.org/packages/e0/07/85157a7c57c5f8b95507d7829b5946561e5ee656ff80e9dd9a757f53ddaf/fastavro-1.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:9090f0dee63fe022ee9cc5147483366cc4171c821644c22da020d6b48f576b4f", size = 444140 }, + { url = "https://files.pythonhosted.org/packages/bb/57/26d5efef9182392d5ac9f253953c856ccb66e4c549fd3176a1e94efb05c9/fastavro-1.12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:78df838351e4dff9edd10a1c41d1324131ffecbadefb9c297d612ef5363c049a", size = 1000599 }, + { url = "https://files.pythonhosted.org/packages/33/cb/8ab55b21d018178eb126007a56bde14fd01c0afc11d20b5f2624fe01e698/fastavro-1.12.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:780476c23175d2ae457c52f45b9ffa9d504593499a36cd3c1929662bf5b7b14b", size = 3335933 }, + { url = "https://files.pythonhosted.org/packages/fe/03/9c94ec9bf873eb1ffb0aa694f4e71940154e6e9728ddfdc46046d7e8ced4/fastavro-1.12.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0714b285160fcd515eb0455540f40dd6dac93bdeacdb03f24e8eac3d8aa51f8d", size = 3402066 }, + { url = "https://files.pythonhosted.org/packages/75/c8/cb472347c5a584ccb8777a649ebb28278fccea39d005fc7df19996f41df8/fastavro-1.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a8bc2dcec5843d499f2489bfe0747999108f78c5b29295d877379f1972a3d41a", size = 3240038 }, + { url = "https://files.pythonhosted.org/packages/e1/77/569ce9474c40304b3a09e109494e020462b83e405545b78069ddba5f614e/fastavro-1.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3b1921ac35f3d89090a5816b626cf46e67dbecf3f054131f84d56b4e70496f45", size = 3369398 }, + { url = "https://files.pythonhosted.org/packages/4a/1f/9589e35e9ea68035385db7bdbf500d36b8891db474063fb1ccc8215ee37c/fastavro-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:5aa777b8ee595b50aa084104cd70670bf25a7bbb9fd8bb5d07524b0785ee1699", size = 444220 }, + { url = "https://files.pythonhosted.org/packages/6c/d2/78435fe737df94bd8db2234b2100f5453737cffd29adee2504a2b013de84/fastavro-1.12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c3d67c47f177e486640404a56f2f50b165fe892cc343ac3a34673b80cc7f1dd6", size = 1086611 }, + { url = "https://files.pythonhosted.org/packages/b6/be/428f99b10157230ddac77ec8cc167005b29e2bd5cbe228345192bb645f30/fastavro-1.12.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5217f773492bac43dae15ff2931432bce2d7a80be7039685a78d3fab7df910bd", size = 3541001 }, + { url = "https://files.pythonhosted.org/packages/16/08/a2eea4f20b85897740efe44887e1ac08f30dfa4bfc3de8962bdcbb21a5a1/fastavro-1.12.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:469fecb25cba07f2e1bfa4c8d008477cd6b5b34a59d48715e1b1a73f6160097d", size = 3432217 }, + { url = "https://files.pythonhosted.org/packages/87/bb/b4c620b9eb6e9838c7f7e4b7be0762834443adf9daeb252a214e9ad3178c/fastavro-1.12.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d71c8aa841ef65cfab709a22bb887955f42934bced3ddb571e98fdbdade4c609", size = 3366742 }, + { url = "https://files.pythonhosted.org/packages/3d/d1/e69534ccdd5368350646fea7d93be39e5f77c614cca825c990bd9ca58f67/fastavro-1.12.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b81fc04e85dfccf7c028e0580c606e33aa8472370b767ef058aae2c674a90746", size = 3383743 }, + { url = "https://files.pythonhosted.org/packages/58/54/b7b4a0c3fb5fcba38128542da1b26c4e6d69933c923f493548bdfd63ab6a/fastavro-1.12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9445da127751ba65975d8e4bdabf36bfcfdad70fc35b2d988e3950cce0ec0e7c", size = 1001377 }, + { url = "https://files.pythonhosted.org/packages/1e/4f/0e589089c7df0d8f57d7e5293fdc34efec9a3b758a0d4d0c99a7937e2492/fastavro-1.12.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed924233272719b5d5a6a0b4d80ef3345fc7e84fc7a382b6232192a9112d38a6", size = 3320401 }, + { url = "https://files.pythonhosted.org/packages/f9/19/260110d56194ae29d7e423a336fccea8bcd103196d00f0b364b732bdb84e/fastavro-1.12.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3616e2f0e1c9265e92954fa099db79c6e7817356d3ff34f4bcc92699ae99697c", size = 3350894 }, + { url = "https://files.pythonhosted.org/packages/d0/96/58b0411e8be9694d5972bee3167d6c1fd1fdfdf7ce253c1a19a327208f4f/fastavro-1.12.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cb0337b42fd3c047fcf0e9b7597bd6ad25868de719f29da81eabb6343f08d399", size = 3229644 }, + { url = "https://files.pythonhosted.org/packages/5b/db/38660660eac82c30471d9101f45b3acfdcbadfe42d8f7cdb129459a45050/fastavro-1.12.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:64961ab15b74b7c168717bbece5660e0f3d457837c3cc9d9145181d011199fa7", size = 3329704 }, + { url = "https://files.pythonhosted.org/packages/9d/a9/1672910f458ecb30b596c9e59e41b7c00309b602a0494341451e92e62747/fastavro-1.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:792356d320f6e757e89f7ac9c22f481e546c886454a6709247f43c0dd7058004", size = 452911 }, + { url = "https://files.pythonhosted.org/packages/dc/8d/2e15d0938ded1891b33eff252e8500605508b799c2e57188a933f0bd744c/fastavro-1.12.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:120aaf82ac19d60a1016afe410935fe94728752d9c2d684e267e5b7f0e70f6d9", size = 3541999 }, + { url = "https://files.pythonhosted.org/packages/a7/1c/6dfd082a205be4510543221b734b1191299e6a1810c452b6bc76dfa6968e/fastavro-1.12.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6a3462934b20a74f9ece1daa49c2e4e749bd9a35fa2657b53bf62898fba80f5", size = 3433972 }, + { url = "https://files.pythonhosted.org/packages/24/90/9de694625a1a4b727b1ad0958d220cab25a9b6cf7f16a5c7faa9ea7b2261/fastavro-1.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1f81011d54dd47b12437b51dd93a70a9aa17b61307abf26542fc3c13efbc6c51", size = 3368752 }, + { url = "https://files.pythonhosted.org/packages/fa/93/b44f67589e4d439913dab6720f7e3507b0fa8b8e56d06f6fc875ced26afb/fastavro-1.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43ded16b3f4a9f1a42f5970c2aa618acb23ea59c4fcaa06680bdf470b255e5a8", size = 3386636 }, +] + [[package]] name = "fastcore" version = "1.8.16" @@ -1191,6 +1251,7 @@ dependencies = [ { name = "alembic" }, { name = "anthropic" }, { name = "asyncpg" }, + { name = "cohere" }, { name = "dateparser" }, { name = "fastapi", extra = ["standard"] }, { name = "fastmcp" }, @@ -1246,6 +1307,7 @@ requires-dist = [ { name = "alembic", specifier = ">=1.17.1" }, { name = "anthropic", specifier = ">=0.40.0" }, { name = "asyncpg", specifier = ">=0.29.0" }, + { name = "cohere", specifier = ">=5.0.0" }, { name = "dateparser", specifier = ">=1.2.2" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.120.3" }, { name = "fastmcp", specifier = ">=2.3.0" }, @@ -4367,6 +4429,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/64/7713ffe4b5983314e9d436a90d5bd4f63b6054e2aca783a3cfc44cb95bbf/typer-0.20.0-py3-none-any.whl", hash = "sha256:5b463df6793ec1dca6213a3cf4c0f03bc6e322ac5e16e13ddd622a889489784a", size = 47028 }, ] +[[package]] +name = "types-requests" +version = "2.32.4.20260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676 }, +] + [[package]] name = "typing-extensions" version = "4.15.0"