diff --git a/CHANGELOG.md b/CHANGELOG.md index e2a1b774..46a627d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Tool selection remains when switching between pages. - Propagate temperature to the api call and set the seed. +### Removed +- Brain region and mtype resolving. + ## [v0.6.4] - 02.07.2025 ### Added diff --git a/README.md b/README.md index 32c04d12..dc85bd84 100644 --- a/README.md +++ b/README.md @@ -24,12 +24,6 @@ docker compose up docker exec -it neuroagent-minio-1 mc alias set myminio http://minio:9000 minioadmin minioadmin && docker exec -it neuroagent-minio-1 mc mb myminio/neuroagent ``` -To enable the brain region resolving tool, retrieve your bearer token and make sure to run the following script: -```bash -python backend/src/neuroagent/scripts/embed_hierarchies.py $token -e https://staging.openbraininstitute.org/api/entitycore/ -u http://localhost:9000 -b neuroagent -a minioadmin -s minioadmin -``` -which stores a json file in your minio/s3 instance. - 4. Access the application at `http://localhost:3000` Notes: diff --git a/backend/README.md b/backend/README.md index 74bcdef3..ac2e694d 100644 --- a/backend/README.md +++ b/backend/README.md @@ -37,12 +37,6 @@ You'll need to create the `neuroagent` bucket. You can either: docker exec mc mb /data/neuroagent ``` -You can compute and push the brain region hierarchy embedding to minio: -```bash -python src/neuroagent/scripts/embed_hierarchies.py $token -e https://staging.openbraininstitute.org/api/entitycore/ -u http://localhost:9000 -b neuroagent -a minioadmin -s minioadmin -``` -You need to insert your bearer token corresponding to the environment defined in the `-e` arg. - 4. (Optional) Set up Redis for rate limiting: ```bash docker run -d -p 6379:6379 redis diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 9c7306c5..32c447ba 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -27,7 +27,6 @@ dependencies = [ "pyyaml", "redis", "semantic-router", - "scikit-learn", "sqlalchemy[asyncio]", "tavily-python", "types-PyYAML", diff --git a/backend/src/neuroagent/app/app_utils.py b/backend/src/neuroagent/app/app_utils.py index 46360ec9..03873b00 100644 --- a/backend/src/neuroagent/app/app_utils.py +++ b/backend/src/neuroagent/app/app_utils.py @@ -2,7 +2,6 @@ import json import logging -import re import uuid from pathlib import Path from typing import Any, Literal, Sequence @@ -34,7 +33,6 @@ ToolCallPartVercel, ToolCallVercel, ) -from neuroagent.schemas import EmbeddedBrainRegions from neuroagent.tools.base_tool import BaseTool logger = logging.getLogger(__name__) @@ -226,24 +224,6 @@ async def commit_messages( await session.close() -def get_br_embeddings( - s3_client: Any, bucket_name: str, folder: str -) -> list[EmbeddedBrainRegions]: - """Retrieve brain regions embeddings from s3.""" - file_list = s3_client.list_objects_v2(Bucket=bucket_name, Prefix=folder) - pattern = re.compile(rf"^{folder}/.*_hierarchy_embeddings.json$") - output: list[EmbeddedBrainRegions] = [] - - if "Contents" in file_list: - for obj in file_list["Contents"]: - key = obj["Key"] - if pattern.match(key): - file_obj = s3_client.get_object(Bucket=bucket_name, Key=key) - content = json.loads(file_obj["Body"].read().decode("utf-8")) - output.append(EmbeddedBrainRegions(**content)) - return output - - def format_messages_output( db_messages: Sequence[Messages], tool_hil_mapping: dict[str, bool], diff --git a/backend/src/neuroagent/app/dependencies.py b/backend/src/neuroagent/app/dependencies.py index 3caa00da..5f46a6fa 100644 --- a/backend/src/neuroagent/app/dependencies.py +++ b/backend/src/neuroagent/app/dependencies.py @@ -71,8 +71,6 @@ PlotMorphologyGetOneTool, ReconstructionMorphologyGetAllTool, ReconstructionMorphologyGetOneTool, - ResolveBrainRegionTool, - ResolveMtypeTool, SCSGetAllTool, SCSGetOneTool, SCSPlotTool, @@ -354,8 +352,6 @@ def get_tool_list( LiteratureSearchTool, ReconstructionMorphologyGetAllTool, ReconstructionMorphologyGetOneTool, - ResolveBrainRegionTool, - ResolveMtypeTool, MorphometricsGetOneTool, EphysMetricsGetOneTool, OrganizationGetAllTool, @@ -578,7 +574,6 @@ def get_s3_client( def get_context_variables( - request: Request, settings: Annotated[Settings, Depends(get_settings)], httpx_client: Annotated[AsyncClient, Depends(get_httpx_client)], thread: Annotated[Threads, Depends(get_thread)], @@ -589,7 +584,6 @@ def get_context_variables( """Get the context variables to feed the tool's metadata.""" return { "bluenaas_url": settings.tools.bluenaas.url, - "brainregion_embeddings": request.app.state.br_embeddings, "bucket_name": settings.storage.bucket_name, "entitycore_url": settings.tools.entitycore.url, "httpx_client": httpx_client, diff --git a/backend/src/neuroagent/app/main.py b/backend/src/neuroagent/app/main.py index 17ce9ba5..e15f5c5d 100644 --- a/backend/src/neuroagent/app/main.py +++ b/backend/src/neuroagent/app/main.py @@ -24,7 +24,6 @@ from neuroagent import __version__ from neuroagent.app.app_utils import ( - get_br_embeddings, get_semantic_router, setup_engine, ) @@ -32,7 +31,6 @@ from neuroagent.app.dependencies import ( get_connection_string, get_mcp_tool_list, - get_s3_client, get_settings, get_tool_list, ) @@ -121,14 +119,6 @@ async def lifespan(fastapi_app: FastAPI) -> AsyncContextManager[None]: # type: semantic_router = get_semantic_router(settings=app_settings) fastapi_app.state.semantic_router = semantic_router - s3_client = get_s3_client(app_settings) - br_embeddings = get_br_embeddings( - s3_client=s3_client, - bucket_name=app_settings.storage.bucket_name, - folder="shared", - ) - fastapi_app.state.br_embeddings = br_embeddings - async with aclosing( AsyncAccountingSessionFactory( base_url=app_settings.accounting.base_url, diff --git a/backend/src/neuroagent/schemas.py b/backend/src/neuroagent/schemas.py index 46306a7e..609c28d0 100644 --- a/backend/src/neuroagent/schemas.py +++ b/backend/src/neuroagent/schemas.py @@ -163,19 +163,3 @@ class JSONMultiLinechart(BaseObject): default="solid", description="Line style (e.g., 'solid', 'dashed', 'dotted')" ) line_color: str | None = Field(None, description="Hex color code for the line") - - -class EmbeddedBrainRegion(BaseModel): - """Brain region schema.""" - - id: str - name: str - hierarchy_level: int - name_embedding: list[float] | None = None - - -class EmbeddedBrainRegions(BaseModel): - """Schema for dumping.""" - - regions: list[EmbeddedBrainRegion] - hierarchy_id: str diff --git a/backend/src/neuroagent/scripts/embed_hierarchies.py b/backend/src/neuroagent/scripts/embed_hierarchies.py deleted file mode 100644 index d137be69..00000000 --- a/backend/src/neuroagent/scripts/embed_hierarchies.py +++ /dev/null @@ -1,187 +0,0 @@ -"""Based on a brain region hierarchy, add embeddings in minio/s3 for every brain region.""" - -import argparse -import asyncio -import logging -import os -from typing import Any - -import boto3 -from dotenv import load_dotenv -from httpx import AsyncClient -from openai import AsyncOpenAI - -from neuroagent.schemas import EmbeddedBrainRegion, EmbeddedBrainRegions - -logging.basicConfig( - format="[%(levelname)s] %(asctime)s %(name)s %(message)s", level=logging.INFO -) - -logger = logging.getLogger(__name__) - - -def get_parser() -> argparse.ArgumentParser: - """Get parser for command line arguments.""" - parser = argparse.ArgumentParser( - formatter_class=argparse.ArgumentDefaultsHelpFormatter - ) - parser.add_argument( - "token", - type=str, - help="Bearer token for the entity core call.", - ) - parser.add_argument( - "--hierarchy-id", - "-i", - type=str, - default="e3e70682-c209-4cac-a29f-6fbed82c07cd", - help="Id of the brain region hierarchy.", - ) - parser.add_argument( - "--entity-core-url", - "-e", - required=False, - default=None, - help="URL of the entity core API. Read from env if not specified.", - ) - ( - parser.add_argument( - "--s3-url", - "-u", - type=str, - required=False, - default=None, - help="URL of the s3 bucket. Read from env if not specified.", - ), - ) - ( - parser.add_argument( - "--s3-bucket-name", - "-b", - type=str, - required=False, - default=None, - help="Name of the s3 bucket. Read from env if not specified.", - ), - ) - ( - parser.add_argument( - "--s3-access-key", - "-a", - type=str, - required=False, - default=None, - help="Access key of the s3 bucket. Read from env if not specified.", - ), - ) - ( - parser.add_argument( - "--s3-secret-key", - "-s", - type=str, - required=False, - default=None, - help="Secret key of the s3 bucket. Read from env if not specified.", - ), - ) - return parser - - -def flatten_hierarchy( - hierarchy: dict[str, Any], level: int = 0 -) -> list[EmbeddedBrainRegion]: - """Recursively walk the hierarchy and return a flat list of all brain regions for every node.""" - regions: list[EmbeddedBrainRegion] = [] - - # 1. Add this node - regions.append( - EmbeddedBrainRegion( - id=hierarchy["id"], - name=hierarchy["name"], - hierarchy_level=level, - ) - ) - - # 2. Recurse into any children - for child in hierarchy.get("children", []): - regions.extend(flatten_hierarchy(child, level=level + 1)) - - return regions - - -async def push_embeddings_to_s3( - hierarchy_id: str, - s3_url: str | None, - entity_core_url: str | None, - s3_access_key: str | None, - s3_secret_key: str | None, - s3_bucket_name: str, - token: str, -) -> None: - """Compute and push embeddings to s3.""" - httpx_client = AsyncClient(timeout=300.0) - logger.info(f"Getting brain hierarchy {hierarchy_id} from Entity-Core.") - - hierarchy = await httpx_client.get( - f"{(entity_core_url or os.getenv('NEUROAGENT_TOOLS__ENTITYCORE__URL')).rstrip('/')}/brain-region-hierarchy/{hierarchy_id}/hierarchy", # type: ignore - headers={"Authorization": f"Bearer {token}"}, - ) - if hierarchy.status_code != 200: - raise ValueError( - f"Entity core returned a non 200 status code. Could not update the brain region embeddings. Error: {hierarchy.text}" - ) - - logger.info("Flattening the hierarchy.") - flattened_hierarchy = flatten_hierarchy(hierarchy=hierarchy.json(), level=0) - brain_regions = EmbeddedBrainRegions( - regions=flattened_hierarchy, hierarchy_id=hierarchy_id - ) - - # Gather the names - names = [brain_region.name for brain_region in brain_regions.regions] - - # Embed them - logger.info("Embedding the names.") - openai_client = AsyncOpenAI(api_key=os.getenv("NEUROAGENT_LLM__OPENAI_TOKEN")) - name_embeddings = await openai_client.embeddings.create( - input=names, model="text-embedding-3-small" - ) - - # Set the embeddings in the original class - for brain_region, name_embedding in zip( - brain_regions.regions, name_embeddings.data - ): - brain_region.name_embedding = name_embedding.embedding - - # Put the result in the s3 bucket - logger.info( - f"Saving the results in s3 bucket: {s3_url or os.getenv('NEUROAGENT_STORAGE__ENDPOINT_URL')} at location: {f'shared/{hierarchy_id}_hierarchy.json'}" - ) - s3_client = boto3.client( - "s3", - endpoint_url=s3_url or os.getenv("NEUROAGENT_STORAGE__ENDPOINT_URL"), - aws_access_key_id=s3_access_key or os.getenv("NEUROAGENT_STORAGE__ACCESS_KEY"), - aws_secret_access_key=s3_secret_key - or os.getenv("NEUROAGENT_STORAGE__SECRET_KEY"), - aws_session_token=None, - config=boto3.session.Config(signature_version="s3v4"), - ) - - s3_client.put_object( - Bucket=s3_bucket_name or os.getenv("NEUROAGENT_STORAGE__BUCKET_NAME"), - Key=f"shared/{hierarchy_id}_hierarchy_embeddings.json", - Body=brain_regions.model_dump_json(), - ContentType="application/json", - ) - - -async def main() -> None: - """Run main logic.""" - parser = get_parser() - args = parser.parse_args() - await push_embeddings_to_s3(**vars(args)) - - -if __name__ == "__main__": - load_dotenv() - asyncio.run(main()) diff --git a/backend/src/neuroagent/tools/__init__.py b/backend/src/neuroagent/tools/__init__.py index cc491751..419055b4 100644 --- a/backend/src/neuroagent/tools/__init__.py +++ b/backend/src/neuroagent/tools/__init__.py @@ -130,53 +130,33 @@ LiteratureSearchTool, ParagraphMetadata, ) -from neuroagent.tools.now import NowTool from neuroagent.tools.obione_ephysmetrics_getone import EphysMetricsGetOneTool from neuroagent.tools.obione_morphometrics_getone import MorphometricsGetOneTool -from neuroagent.tools.resolve_brain_region_tool import ResolveBrainRegionTool -from neuroagent.tools.resolve_mtypes_tool import ResolveMtypeTool from neuroagent.tools.thumbnailgen_morphology_getone import PlotMorphologyGetOneTool from neuroagent.tools.weather import WeatherTool from neuroagent.tools.web_search import WebSearchTool __all__ = [ - "CircuitGetAllTool", - "CircuitGetOneTool", - "SCSGetAllTool", - "SCSGetOneTool", - "SCSPlotTool", - "SCSPostTool", - "LiteratureSearchTool", - "MEModelGetAllTool", - "MEModelGetOneTool", - "ReconstructionMorphologyGetAllTool", - "ReconstructionMorphologyGetOneTool", - "MorphometricsGetOneTool", - "EphysMetricsGetOneTool", - "NowTool", - "ParagraphMetadata", - "PlotGeneratorTool", - "ResolveBrainRegionTool", - "ResolveMtypeTool", - "WeatherTool", - "WebSearchTool", - "EtypeGetAllTool", - "EtypeGetOneTool", - "EModelGetAllTool", - "EModelGetOneTool", - "MtypeGetAllTool", - "MtypeGetOneTool", + "AssetDownloadOneTool", "AssetGetAllTool", "AssetGetOneTool", - "AssetDownloadOneTool", "BrainAtlasGetAllTool", "BrainAtlasGetOneTool", "BrainRegionGetAllTool", "BrainRegionGetOneTool", "BrainRegionHierarchyGetAllTool", "BrainRegionHierarchyGetOneTool", + "CircuitGetAllTool", + "CircuitGetOneTool", + "ContributionGetAllTool", + "ContributionGetOneTool", "ElectricalCellRecordingGetAllTool", "ElectricalCellRecordingGetOneTool", + "EModelGetAllTool", + "EModelGetOneTool", + "EphysMetricsGetOneTool", + "EtypeGetAllTool", + "EtypeGetOneTool", "ExperimentalBoutonDensityGetAllTool", "ExperimentalBoutonDensityGetOneTool", "ExperimentalNeuronDensityGetAllTool", @@ -185,9 +165,27 @@ "ExperimentalSynapsesPerConnectionGetOneTool", "IonChannelModelGetAllTool", "IonChannelModelGetOneTool", + "LiteratureSearchTool", "MeasurementAnnotationGetAllTool", "MeasurementAnnotationGetOneTool", + "MEModelGetAllTool", + "MEModelGetOneTool", + "MorphometricsGetOneTool", + "MtypeGetAllTool", + "MtypeGetOneTool", + "OrganizationGetAllTool", + "OrganizationGetOneTool", + "ParagraphMetadata", + "PersonGetAllTool", + "PersonGetOneTool", + "PlotGeneratorTool", "PlotMorphologyGetOneTool", + "ReconstructionMorphologyGetAllTool", + "ReconstructionMorphologyGetOneTool", + "SCSGetAllTool", + "SCSGetOneTool", + "SCSPlotTool", + "SCSPostTool", "SimulationCampaignGetAllTool", "SimulationCampaignGetOneTool", "SimulationExecutionGetAllTool", @@ -210,10 +208,6 @@ "StrainGetOneTool", "SubjectGetAllTool", "SubjectGetOneTool", - "ContributionGetAllTool", - "ContributionGetOneTool", - "OrganizationGetAllTool", - "OrganizationGetOneTool", - "PersonGetAllTool", - "PersonGetOneTool", + "WeatherTool", + "WebSearchTool", ] diff --git a/backend/src/neuroagent/tools/resolve_brain_region_tool.py b/backend/src/neuroagent/tools/resolve_brain_region_tool.py deleted file mode 100644 index eb91d6f9..00000000 --- a/backend/src/neuroagent/tools/resolve_brain_region_tool.py +++ /dev/null @@ -1,149 +0,0 @@ -"""Tool to resolve the brain region from natural english to its entitycore ID.""" - -import logging -from typing import ClassVar - -from openai import AsyncOpenAI -from pydantic import BaseModel, Field -from sklearn.metrics.pairwise import cosine_similarity - -from neuroagent.schemas import EmbeddedBrainRegions -from neuroagent.tools.base_tool import ( - BaseMetadata, - BaseTool, -) - -logger = logging.getLogger(__name__) - - -class ResolveBRInput(BaseModel): - """Defines the input structure for the Resolve Brain Region tool.""" - - brain_region_name: str = Field( - description="Specifies the target brain region NAME provided by the user in natural language.", - ) - hierarchy_id: str = Field( - default="e3e70682-c209-4cac-a29f-6fbed82c07cd", - description="Id of the brain region hierarchy from which we resolve.", - ) - number_of_candidates: int = Field( - default=10, description="Number of candidate brain regions to return." - ) - - -class ResolveBRMetadata(BaseMetadata): - """Metadata for ResolveEntitiesTool.""" - - brainregion_embeddings: list[EmbeddedBrainRegions] - openai_client: AsyncOpenAI | None - - -class BrainRegion(BaseModel): - """Output schema for the Brain region resolver.""" - - id: str - name: str - score: float - - -class ResolveBrainRegionToolOutput(BaseModel): - """Output schema for the Resolve Entities tool.""" - - brain_regions: list[BrainRegion] - - -class ResolveBrainRegionTool(BaseTool): - """Class defining the Brain Region Resolving logic.""" - - name: ClassVar[str] = "resolve-brain-region-tool" - name_frontend: ClassVar[str] = "Resolve Brain Region" - description: ClassVar[ - str - ] = """Resolve a brain region's name to its UUID using semantic search. - Accepts natural language inputs containing the full or partial name. - Use this tool as soon as you need the ID of a brain region for subsequent tasks.""" - description_frontend: ClassVar[str] = ( - """Converts natural language brain region to its ID.""" - ) - metadata: ResolveBRMetadata - input_schema: ResolveBRInput - - async def arun( - self, - ) -> ResolveBrainRegionToolOutput: - """Given a brain region's name in natural language, resolve its ID.""" - logger.info( - f"Entering Brain Region resolver tool. Inputs: {self.input_schema.model_dump()}" - ) - - # First we select the correct hierarchy with pre-computed embeddings - try: - hierarchy = next( - ( - region - for region in self.metadata.brainregion_embeddings - if region.hierarchy_id == self.input_schema.hierarchy_id - ) - ) - except StopIteration: - raise ValueError("Hierarchy ID not found in existing embeddings.") - - # Try exact match before anything - try: - return next( - ResolveBrainRegionToolOutput( - brain_regions=[ - BrainRegion( - id=region.id, - name=region.name, - score=1, - ) - ] - ) - for region in hierarchy.regions - if region.name.lower() == self.input_schema.brain_region_name.lower() - ) - except StopIteration: - if not self.metadata.openai_client: - raise ValueError( - "Could not exact match the requested brain region. Please provide the OpenAI client to perform semantic search." - ) - pass - - # If exact match didn't work we perform semantic search - response = await self.metadata.openai_client.embeddings.create( - input=self.input_schema.brain_region_name, - model="text-embedding-3-small", - ) - name_embedding = response.data[0].embedding - - # Gather pre-computed name embeddings - br_name_embeddings = [ - brain_region.name_embedding for brain_region in hierarchy.regions - ] - - # Compute cosine similarity - input_name_region_name_similarity = cosine_similarity( - [name_embedding], br_name_embeddings - ).squeeze(axis=0) - - # Assign score to each brain region and prepare for output. - scored_regions = [ - BrainRegion(id=brain_region.id, name=brain_region.name, score=score) - for brain_region, score in zip( - hierarchy.regions, - input_name_region_name_similarity, - ) - ] - - # Sort brain regions by their score - top_brain_regions = sorted(scored_regions, key=lambda x: x.score, reverse=True) - - return ResolveBrainRegionToolOutput( - brain_regions=top_brain_regions[: self.input_schema.number_of_candidates] - ) - - @classmethod - async def is_online(cls) -> bool: - """Check if the tool is online.""" - return True diff --git a/backend/src/neuroagent/tools/resolve_mtypes_tool.py b/backend/src/neuroagent/tools/resolve_mtypes_tool.py deleted file mode 100644 index 92f3c7f3..00000000 --- a/backend/src/neuroagent/tools/resolve_mtypes_tool.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Tool to resolve m-types to entitycore ID.""" - -import logging -from typing import ClassVar - -from httpx import AsyncClient -from pydantic import BaseModel, Field - -from neuroagent.tools.autogenerated_types.entitycore import ( - ReadManyMtypeGetParametersQuery, -) -from neuroagent.tools.base_tool import ( - BaseMetadata, - BaseTool, -) - -logger = logging.getLogger(__name__) - - -class ResolveMtypeInput(BaseModel): - """Defines the input structure for the m-types resolving tool.""" - - mtype: str = Field( - description="Specifies the target M-type (Morphological type) as provided by the user in natural language. The value must match exactly, without case insensitivity.", - ) - - -class ResolveMtypeMetadata(BaseMetadata): - """Metadata for ResolveEntitiesTool.""" - - httpx_client: AsyncClient - entitycore_url: str - - -class MType(BaseModel): - """Output schema for the Mtype resolver.""" - - mtype_name: str - mtype_id: str - - -class ResolveMtypeOutput(BaseModel): - """Output schema for the Resolve Entities tool.""" - - mtypes: list[MType] - - -class ResolveMtypeTool(BaseTool): - """Class defining the m-types Resolving logic.""" - - name: ClassVar[str] = "resolve-mtype-tool" - name_frontend: ClassVar[str] = "Resolve m-types" - description: ClassVar[str] = ( - """Resolve the mtype name from natural english to its corresponding ID too (formated ad UUID).""" - ) - description_frontend: ClassVar[str] = ( - """Convert natural language m-type to its ID.""" - ) - metadata: ResolveMtypeMetadata - input_schema: ResolveMtypeInput - - async def arun( - self, - ) -> ResolveMtypeOutput: - """Given a brain region in natural language, resolve its ID.""" - logger.info( - f"Entering Brain Region resolver tool. Inputs: {self.input_schema.mtype=}" - ) - params = ReadManyMtypeGetParametersQuery(pref_label=self.input_schema.mtype) - mtype_response = await self.metadata.httpx_client.get( - url=self.metadata.entitycore_url + "/mtype", - params=params.model_dump(exclude_defaults=True), - ) - - if mtype_response.status_code != 200: - raise ValueError( - f"The m-type endpoint returned a non 200 response code. Error: {mtype_response.text}" - ) - - mtypes = [ - MType(mtype_name=mtype["pref_label"], mtype_id=mtype["id"]) - for mtype in mtype_response.json()["data"] - ] - - return ResolveMtypeOutput(mtypes=mtypes) - - @classmethod - async def is_online(cls, *, httpx_client: AsyncClient, entitycore_url: str) -> bool: - """Check if the tool is online.""" - response = await httpx_client.get( - f"{entitycore_url.rstrip('/')}/health", - ) - return response.status_code == 200 diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 01cb7c2d..668b5328 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -2,7 +2,7 @@ import json from typing import ClassVar -from unittest.mock import Mock, mock_open, patch +from unittest.mock import mock_open, patch from uuid import UUID import pytest @@ -17,7 +17,6 @@ from neuroagent.app.dependencies import Agent, get_openrouter_models, get_settings from neuroagent.app.main import app from neuroagent.app.schemas import OpenRouterModelResponse -from neuroagent.schemas import EmbeddedBrainRegion, EmbeddedBrainRegions from neuroagent.tools.base_tool import BaseTool from tests.mock_client import MockOpenAIClient, create_mock_response @@ -314,23 +313,6 @@ def settings(): ) -@pytest.fixture(autouse=True) -def mock_br_embeddings(monkeypatch): - """Automatically mock br_embeddings for all tests""" - mock_embeddings = [ - EmbeddedBrainRegions( - regions=[EmbeddedBrainRegion(id="1234", name="test", hierarchy_level=0)], - hierarchy_id="4567", - ) - ] # or whatever mock data you need - - def mock_get_br_embeddings(*args, **kwargs): - return mock_embeddings - - monkeypatch.setattr("neuroagent.app.main.get_br_embeddings", mock_get_br_embeddings) - monkeypatch.setattr("neuroagent.app.main.get_s3_client", lambda *params: Mock()) - - # Prevent tests from connecting to actual MCP servers @pytest.fixture(autouse=True, scope="module") def patch_mcp_servers(): diff --git a/backend/tests/scripts/test_embed_hierarchies.py b/backend/tests/scripts/test_embed_hierarchies.py deleted file mode 100644 index d1c28653..00000000 --- a/backend/tests/scripts/test_embed_hierarchies.py +++ /dev/null @@ -1,425 +0,0 @@ -"""Unit tests for the brain region embeddings script.""" - -import json -from unittest.mock import AsyncMock, Mock, patch - -import pytest - -from neuroagent.scripts.embed_hierarchies import ( - flatten_hierarchy, - push_embeddings_to_s3, -) - - -class TestFlattenHierarchy: - """Tests for the flatten_hierarchy function with comprehensive fake data.""" - - def test_flatten_single_node_minimal(self): - """Test flattening a hierarchy with a single node.""" - hierarchy = { - "id": "brain_001", - "name": "Whole Brain", - "acronym": "WB", - } - - result = flatten_hierarchy(hierarchy) - - assert len(result) == 1 - assert result[0].id == "brain_001" - assert result[0].name == "Whole Brain" - assert result[0].hierarchy_level == 0 - - def test_flatten_simple_parent_child(self): - """Test flattening with one parent and multiple children.""" - hierarchy = { - "id": "ctx_001", - "name": "Cerebral Cortex", - "acronym": "CTX", - "children": [ - { - "id": "ctx_motor_001", - "name": "Primary Motor Cortex", - "acronym": "M1", - }, - { - "id": "ctx_visual_001", - "name": "Primary Visual Cortex", - "acronym": "V1", - }, - { - "id": "ctx_auditory_001", - "name": "Primary Auditory Cortex", - "acronym": "A1", - }, - ], - } - - result = flatten_hierarchy(hierarchy) - - assert len(result) == 4 # 1 parent + 3 children - - # Check parent - parent = result[0] - assert parent.id == "ctx_001" - assert parent.name == "Cerebral Cortex" - assert parent.hierarchy_level == 0 - - # Check children - children = [r for r in result if r.hierarchy_level == 1] - assert len(children) == 3 - - child_ids = {child.id for child in children} - expected_ids = {"ctx_motor_001", "ctx_visual_001", "ctx_auditory_001"} - assert child_ids == expected_ids - - def test_flatten_deep_nested_hierarchy(self): - """Test flattening a deeply nested brain region hierarchy.""" - hierarchy = { - "id": "brain_root", - "name": "Brain", - "acronym": "BR", - "children": [ - { - "id": "forebrain_001", - "name": "Forebrain", - "acronym": "FB", - "children": [ - { - "id": "telencephalon_001", - "name": "Telencephalon", - "acronym": "TEL", - "children": [ - { - "id": "hippocampus_001", - "name": "Hippocampus", - "acronym": "HIP", - "children": [ - { - "id": "ca1_001", - "name": "Cornu Ammonis 1", - "acronym": "CA1", - }, - { - "id": "ca3_001", - "name": "Cornu Ammonis 3", - "acronym": "CA3", - }, - ], - }, - { - "id": "amygdala_001", - "name": "Amygdala", - "acronym": "AMY", - }, - ], - } - ], - }, - { - "id": "brainstem_001", - "name": "Brainstem", - "acronym": "BS", - "children": [ - { - "id": "midbrain_001", - "name": "Midbrain", - "acronym": "MB", - } - ], - }, - ], - } - - result = flatten_hierarchy(hierarchy) - - assert len(result) == 9 # Total nodes in the hierarchy - - # Check hierarchy levels - level_0 = [r for r in result if r.hierarchy_level == 0] - level_1 = [r for r in result if r.hierarchy_level == 1] - level_2 = [r for r in result if r.hierarchy_level == 2] - level_3 = [r for r in result if r.hierarchy_level == 3] - level_4 = [r for r in result if r.hierarchy_level == 4] - - assert len(level_0) == 1 # Brain - assert len(level_1) == 2 # Forebrain, Brainstem - assert len(level_2) == 2 # Telencephalon, Midbrain - assert len(level_3) == 2 # Hippocampus, Amygdala - assert len(level_4) == 2 # CA1, CA3 - - # Verify specific regions at correct levels - assert level_0[0].name == "Brain" - assert any(r.name == "Hippocampus" for r in level_3) - - def test_flatten_with_custom_starting_level(self): - """Test flattening with a custom starting hierarchy level.""" - hierarchy = { - "id": "thalamus_001", - "name": "Thalamus", - "acronym": "TH", - "children": [ - { - "id": "lgn_001", - "name": "Lateral Geniculate Nucleus", - "acronym": "LGN", - } - ], - } - - result = flatten_hierarchy(hierarchy, level=3) - - assert len(result) == 2 - assert result[0].hierarchy_level == 3 # Thalamus at level 3 - assert result[1].hierarchy_level == 4 # LGN at level 4 - - def test_flatten_empty_children_list(self): - """Test flattening with explicitly empty children list.""" - hierarchy = { - "id": "cerebellum_001", - "name": "Cerebellum", - "acronym": "CB", - "children": [], - } - - result = flatten_hierarchy(hierarchy) - - assert len(result) == 1 - assert result[0].id == "cerebellum_001" - assert result[0].name == "Cerebellum" - - def test_flatten_complex_realistic_hierarchy(self): - """Test with a complex, realistic brain region hierarchy.""" - hierarchy = { - "id": "ctx_frontal", - "name": "Frontal Cortex", - "acronym": "FC", - "children": [ - { - "id": "ctx_prefrontal", - "name": "Prefrontal Cortex", - "acronym": "PFC", - "children": [ - { - "id": "ctx_dlpfc", - "name": "Dorsolateral Prefrontal Cortex", - "acronym": "dlPFC", - }, - { - "id": "ctx_vmpfc", - "name": "Ventromedial Prefrontal Cortex", - "acronym": "vmPFC", - }, - { - "id": "ctx_ofc", - "name": "Orbitofrontal Cortex", - "acronym": "OFC", - "children": [ - { - "id": "ctx_ofc_lateral", - "name": "Lateral Orbitofrontal Cortex", - "acronym": "lOFC", - }, - { - "id": "ctx_ofc_medial", - "name": "Medial Orbitofrontal Cortex", - "acronym": "mOFC", - }, - ], - }, - ], - }, - { - "id": "ctx_motor_primary", - "name": "Primary Motor Cortex", - "acronym": "M1", - }, - { - "id": "ctx_motor_supplementary", - "name": "Supplementary Motor Area", - "acronym": "SMA", - }, - ], - } - - result = flatten_hierarchy(hierarchy) - - # Verify total count - assert len(result) == 9 - - # Verify all expected regions are present - region_names = {r.name for r in result} - expected_names = { - "Frontal Cortex", - "Prefrontal Cortex", - "Dorsolateral Prefrontal Cortex", - "Ventromedial Prefrontal Cortex", - "Orbitofrontal Cortex", - "Lateral Orbitofrontal Cortex", - "Medial Orbitofrontal Cortex", - "Primary Motor Cortex", - "Supplementary Motor Area", - } - assert len(region_names.intersection(expected_names)) == 9 - - # Verify hierarchy levels are correct - ofc_regions = [ - r - for r in result - if "Orbitofrontal" in r.name and "Lateral" in r.name or "Medial" in r.name - ] - assert all(r.hierarchy_level == 3 for r in ofc_regions) - - -class TestPushEmbeddingsToS3: - """Tests for the push_embeddings_to_s3 function with mocked dependencies.""" - - @pytest.fixture - def sample_hierarchy_response(self): - """Sample hierarchy response from entity core API.""" - return { - "id": "test_hierarchy_root", - "name": "Test Brain Region", - "acronym": "TBR", - "children": [ - { - "id": "test_child_1", - "name": "Test Child Region 1", - "acronym": "TCR1", - }, - { - "id": "test_child_2", - "name": "Test Child Region 2", - "acronym": "TCR2", - "children": [ - { - "id": "test_grandchild_1", - "name": "Test Grandchild Region 1", - "acronym": "TGCR1", - } - ], - }, - ], - } - - @pytest.fixture - def mock_embedding_data(self): - """Mock embedding response data from OpenAI.""" - return [ - Mock(embedding=[0.1, 0.2, 0.3, 0.4, 0.5]), - Mock(embedding=[0.2, 0.3, 0.4, 0.5, 0.6]), - Mock(embedding=[0.3, 0.4, 0.5, 0.6, 0.7]), - Mock(embedding=[0.4, 0.5, 0.6, 0.7, 0.8]), - ] - - @patch("neuroagent.scripts.embed_hierarchies.AsyncOpenAI") - @patch("neuroagent.scripts.embed_hierarchies.boto3.client") - # @patch.dict(os.environ, {"NEUROAGENT_OPENAI__TOKEN": "test-openai-token"}) - @pytest.mark.asyncio - async def test_successful_embedding_pipeline( - self, - mock_boto_client, - mock_openai_client, - httpx_mock, - sample_hierarchy_response, - mock_embedding_data, - ): - """Test successful execution of the embedding pipeline.""" - # Setup HTTP client mock for entity core API - httpx_mock.add_response( - method="GET", - url="http://test-entity-core.com/brain-region-hierarchy/test-hierarchy-123/hierarchy", - json=sample_hierarchy_response, - status_code=200, - ) - - # Setup OpenAI client mock - mock_openai_instance = AsyncMock() - mock_openai_client.return_value = mock_openai_instance - - mock_embedding_response = Mock(data=mock_embedding_data) - mock_embeddings_api = AsyncMock() - mock_embeddings_api.create.return_value = mock_embedding_response - mock_openai_instance.embeddings = mock_embeddings_api - - # Setup S3 client mock - mock_s3_instance = Mock() - mock_boto_client.return_value = mock_s3_instance - - # Execute function - await push_embeddings_to_s3( - hierarchy_id="test-hierarchy-123", - s3_url="http://test-s3.com", - entity_core_url="http://test-entity-core.com", - s3_access_key="test-access-key", - s3_secret_key="test-secret-key", - s3_bucket_name="test-bucket", - token="test-bearer-token", - ) - - # Verify entity core API call was made - assert len(httpx_mock.get_requests()) == 1 - request = httpx_mock.get_requests()[0] - assert "test-hierarchy-123" in str(request.url) - assert request.headers["Authorization"] == "Bearer test-bearer-token" - - # Verify OpenAI embedding calls - assert mock_embeddings_api.create.call_count == 1 - - # Check the embedding calls - embedding_calls = mock_embeddings_api.create.call_args_list - names_call = embedding_calls[0] - - # Verify names were embedded - expected_names = [ - "Test Brain Region", - "Test Child Region 1", - "Test Child Region 2", - "Test Grandchild Region 1", - ] - assert names_call.kwargs["input"] == expected_names - assert names_call.kwargs["model"] == "text-embedding-3-small" - - # Verify S3 upload - mock_s3_instance.put_object.assert_called_once() - s3_call_args = mock_s3_instance.put_object.call_args.kwargs - - assert s3_call_args["Bucket"] == "test-bucket" - assert ( - s3_call_args["Key"] == "shared/test-hierarchy-123_hierarchy_embeddings.json" - ) - assert s3_call_args["ContentType"] == "application/json" - - # Verify the uploaded data structure - uploaded_data = json.loads(s3_call_args["Body"]) - assert uploaded_data["hierarchy_id"] == "test-hierarchy-123" - assert len(uploaded_data["regions"]) == 4 - - # Check that embeddings were properly assigned - for region_data in uploaded_data["regions"]: - assert region_data["name_embedding"] is not None - assert len(region_data["name_embedding"]) == 5 # Embedding dimension - - @pytest.mark.asyncio - async def test_entity_core_api_error_handling(self, httpx_mock): - """Test proper error handling when entity core API fails.""" - httpx_mock.add_response( - method="GET", - url="http://test-entity-core.com/brain-region-hierarchy/nonexistent-hierarchy/hierarchy", - text="Hierarchy not found", - status_code=404, - ) - - # Verify exception is raised - with pytest.raises(ValueError) as exc_info: - await push_embeddings_to_s3( - hierarchy_id="nonexistent-hierarchy", - s3_url="http://test-s3.com", - entity_core_url="http://test-entity-core.com", - s3_access_key="test-access-key", - s3_secret_key="test-secret-key", - s3_bucket_name="test-bucket", - token="test-token", - ) - - assert "Entity core returned a non 200 status code" in str(exc_info.value) - assert "Hierarchy not found" in str(exc_info.value) diff --git a/backend/tests/tools/test_resolve_brain_region_tool.py b/backend/tests/tools/test_resolve_brain_region_tool.py deleted file mode 100644 index c032c85a..00000000 --- a/backend/tests/tools/test_resolve_brain_region_tool.py +++ /dev/null @@ -1,295 +0,0 @@ -"""Test the resolve_brain_region_tool.""" - -from unittest.mock import AsyncMock, Mock, patch - -import numpy as np -import pytest -from httpx import AsyncClient -from openai import AsyncOpenAI - -from neuroagent.schemas import EmbeddedBrainRegion, EmbeddedBrainRegions -from neuroagent.tools import ResolveBrainRegionTool -from neuroagent.tools.resolve_brain_region_tool import ( - ResolveBrainRegionToolOutput, - ResolveBRInput, - ResolveBRMetadata, -) - - -@pytest.mark.asyncio -class TestResolveBrainRegionTool: - """Test suite for the ResolveBrainRegionTool.""" - - @pytest.fixture - def sample_brain_regions(self): - """Create sample brain regions for testing.""" - return EmbeddedBrainRegions( - hierarchy_id="e3e70682-c209-4cac-a29f-6fbed82c07cd", - regions=[ - EmbeddedBrainRegion( - id="thalamus_001", - name="Thalamus", - hierarchy_level=0, - name_embedding=[0.8, 0.6, 0.7, 0.9, 0.5], - ), - EmbeddedBrainRegion( - id="epithalamus_001", - name="Epithalamus", - hierarchy_level=1, - name_embedding=[0.7, 0.8, 0.6, 0.8, 0.4], - ), - EmbeddedBrainRegion( - id="hypothalamus_001", - name="Hypothalamus", - hierarchy_level=1, - name_embedding=[0.6, 0.7, 0.8, 0.7, 0.6], - ), - EmbeddedBrainRegion( - id="visual_cortex_001", - name="Primary Visual Cortex", - hierarchy_level=2, - name_embedding=[0.2, 0.3, 0.1, 0.4, 0.2], - ), - EmbeddedBrainRegion( - id="motor_cortex_001", - name="Primary Motor Cortex", - hierarchy_level=2, - name_embedding=[0.3, 0.2, 0.4, 0.1, 0.3], - ), - ], - ) - - @pytest.fixture - def mock_openai_client(self): - """Mock OpenAI client for embedding generation.""" - client = AsyncMock(spec=AsyncOpenAI) - client.embeddings = Mock() # or AsyncMock, if `create` is async - client.embeddings.create = AsyncMock() - - # Mock embedding response for "thalamus" query - mock_response = Mock() - mock_response.data = [ - Mock(embedding=[0.85, 0.65, 0.75, 0.95, 0.55]) - ] # Close to Thalamus embedding - client.embeddings.create.return_value = mock_response - return client - - async def test_exact_match_found(self, sample_brain_regions, mock_openai_client): - """Test when an exact name match is found.""" - tool = ResolveBrainRegionTool( - metadata=ResolveBRMetadata( - brainregion_embeddings=[sample_brain_regions], - openai_client=mock_openai_client, - httpx_client=Mock(spec=AsyncClient), - ), - input_schema=ResolveBRInput( - brain_region_name="Thalamus", # Exact match - hierarchy_id="e3e70682-c209-4cac-a29f-6fbed82c07cd", - number_of_candidates=5, - ), - ) - - response = await tool.arun() - - # Should return exact match with score 1.0 - assert isinstance(response, ResolveBrainRegionToolOutput) - assert len(response.brain_regions) == 1 - assert response.brain_regions[0].id == "thalamus_001" - assert response.brain_regions[0].name == "Thalamus" - assert response.brain_regions[0].score == 1.0 - - # OpenAI client should not be called for exact matches - mock_openai_client.embeddings.create.assert_not_called() - - async def test_case_insensitive_exact_match( - self, sample_brain_regions, mock_openai_client - ): - """Test case-insensitive exact matching.""" - tool = ResolveBrainRegionTool( - metadata=ResolveBRMetadata( - brainregion_embeddings=[sample_brain_regions], - openai_client=mock_openai_client, - httpx_client=Mock(spec=AsyncClient), - ), - input_schema=ResolveBRInput( - brain_region_name="THALAMUS", # Different case - hierarchy_id="e3e70682-c209-4cac-a29f-6fbed82c07cd", - number_of_candidates=5, - ), - ) - - response = await tool.arun() - - assert isinstance(response, ResolveBrainRegionToolOutput) - assert len(response.brain_regions) == 1 - assert response.brain_regions[0].id == "thalamus_001" - assert response.brain_regions[0].name == "Thalamus" - assert response.brain_regions[0].score == 1.0 - - @patch("neuroagent.tools.resolve_brain_region_tool.cosine_similarity") - async def test_semantic_search_fallback( - self, mock_cosine_similarity, sample_brain_regions, mock_openai_client - ): - """Test semantic search when no exact match is found.""" - # Mock cosine similarity to return high similarity for Thalamus - mock_cosine_similarity.return_value = np.array( - [ - [ - 0.95, - 0.80, - 0.75, - 0.30, - 0.25, - ] - ] - ) # Scores for each region - - tool = ResolveBrainRegionTool( - metadata=ResolveBRMetadata( - brainregion_embeddings=[sample_brain_regions], - openai_client=mock_openai_client, - httpx_client=Mock(spec=AsyncClient), - ), - input_schema=ResolveBRInput( - brain_region_name="brain stem region", # No exact match - hierarchy_id="e3e70682-c209-4cac-a29f-6fbed82c07cd", - number_of_candidates=3, - ), - ) - - response = await tool.arun() - - # Verify OpenAI embedding was called - mock_openai_client.embeddings.create.assert_called_once_with( - input="brain stem region", model="text-embedding-3-small" - ) - - # Verify cosine similarity was computed - mock_cosine_similarity.assert_called_once() - - # Check response structure and ordering (should be sorted by score descending) - assert isinstance(response, ResolveBrainRegionToolOutput) - assert len(response.brain_regions) == 3 # Limited by number_of_candidates - - # First result should be highest scoring (Thalamus with 0.95) - assert response.brain_regions[0].id == "thalamus_001" - assert response.brain_regions[0].score == 0.95 - - # Second result should be Epithalamus with 0.80 - assert response.brain_regions[1].id == "epithalamus_001" - assert response.brain_regions[1].score == 0.80 - - # Third result should be Hypothalamus with 0.75 - assert response.brain_regions[2].id == "hypothalamus_001" - assert response.brain_regions[2].score == 0.75 - - async def test_hierarchy_not_found_error( - self, sample_brain_regions, mock_openai_client - ): - """Test error handling when hierarchy ID is not found.""" - tool = ResolveBrainRegionTool( - metadata=ResolveBRMetadata( - brainregion_embeddings=[sample_brain_regions], - openai_client=mock_openai_client, - httpx_client=Mock(spec=AsyncClient), - ), - input_schema=ResolveBRInput( - brain_region_name="Thalamus", - hierarchy_id="nonexistent-hierarchy-id", # Invalid hierarchy ID - number_of_candidates=5, - ), - ) - - with pytest.raises( - ValueError, match="Hierarchy ID not found in existing embeddings" - ): - await tool.arun() - - @patch("neuroagent.tools.resolve_brain_region_tool.cosine_similarity") - async def test_number_of_candidates_limiting( - self, mock_cosine_similarity, sample_brain_regions, mock_openai_client - ): - """Test that the number of returned candidates is properly limited.""" - # Mock similarity scores for all 5 regions - mock_cosine_similarity.return_value = np.array( - [ - [ - 0.95, - 0.80, - 0.75, - 0.30, - 0.25, - ] - ] - ) # Scores for each region - - tool = ResolveBrainRegionTool( - metadata=ResolveBRMetadata( - brainregion_embeddings=[sample_brain_regions], - openai_client=mock_openai_client, - httpx_client=Mock(spec=AsyncClient), - ), - input_schema=ResolveBRInput( - brain_region_name="cortex region", - hierarchy_id="e3e70682-c209-4cac-a29f-6fbed82c07cd", - number_of_candidates=2, # Limit to 2 results - ), - ) - - response = await tool.arun() - - assert isinstance(response, ResolveBrainRegionToolOutput) - assert len(response.brain_regions) == 2 # Should be limited to 2 - - # Should return the top 2 scoring regions - assert response.brain_regions[0].score == 0.95 - assert response.brain_regions[1].score == 0.80 - - async def test_multiple_hierarchies_selection(self, mock_openai_client): - """Test that the correct hierarchy is selected when multiple are available.""" - # Create two different hierarchies - hierarchy1 = EmbeddedBrainRegions( - hierarchy_id="hierarchy-1", - regions=[ - EmbeddedBrainRegion( - id="region_h1_1", - name="Region H1-1", - hierarchy_level=0, - name_embedding=[0.1, 0.2, 0.3, 0.4, 0.5], - ), - ], - ) - - hierarchy2 = EmbeddedBrainRegions( - hierarchy_id="hierarchy-2", - regions=[ - EmbeddedBrainRegion( - id="region_h2_1", - name="Region H2-1", - hierarchy_level=0, - name_embedding=[0.6, 0.7, 0.8, 0.9, 1.0], - ), - ], - ) - - tool = ResolveBrainRegionTool( - metadata=ResolveBRMetadata( - brainregion_embeddings=[hierarchy1, hierarchy2], - openai_client=mock_openai_client, - httpx_client=Mock(spec=AsyncClient), - ), - input_schema=ResolveBRInput( - brain_region_name="Region H2-1", # Exact match in hierarchy-2 - hierarchy_id="hierarchy-2", - number_of_candidates=5, - ), - ) - - response = await tool.arun() - - # Should find the exact match in hierarchy-2 - assert isinstance(response, ResolveBrainRegionToolOutput) - assert len(response.brain_regions) == 1 - assert response.brain_regions[0].id == "region_h2_1" - assert response.brain_regions[0].name == "Region H2-1" - assert response.brain_regions[0].score == 1.0 diff --git a/backend/tests/tools/test_resolve_mtype_tool.py b/backend/tests/tools/test_resolve_mtype_tool.py deleted file mode 100644 index 9370d3cd..00000000 --- a/backend/tests/tools/test_resolve_mtype_tool.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Test the revole_brain_region_tool.""" - -import json - -import pytest -from httpx import AsyncClient - -from neuroagent.tools import ResolveMtypeTool -from neuroagent.tools.resolve_mtypes_tool import ( - MType, - ResolveMtypeInput, - ResolveMtypeMetadata, - ResolveMtypeOutput, -) - - -@pytest.mark.asyncio -async def test_arun(httpx_mock): - with open("tests/data/entitycore_mtype.json") as f: - mtypes_region = json.load(f) - - httpx_mock.add_response( - url="http://fake_entitycore_url.com/78/mtype?pref_label=Interneu", - json=mtypes_region, - ) - - tool = ResolveMtypeTool( - metadata=ResolveMtypeMetadata( - token="greattokenpleasedontexpire", - httpx_client=AsyncClient(timeout=None), - entitycore_url="http://fake_entitycore_url.com/78", - ), - input_schema=ResolveMtypeInput(mtype="Interneu"), - ) - - response = await tool.arun() - assert isinstance(response, ResolveMtypeOutput) - assert response == ResolveMtypeOutput( - mtypes=[ - MType( - mtype_name="Pyramidal Neuron", - mtype_id="1a2b8f1f-f1fd-42a2-9755-d4c13a902931", - ), - ], - ) diff --git a/backend/uv.lock b/backend/uv.lock index e70a7c77..eca63ed8 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -762,15 +762,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, ] -[[package]] -name = "joblib" -version = "1.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/fe/0f5a938c54105553436dbff7a61dc4fed4b1b2c98852f8833beaf4d5968f/joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444", size = 330475, upload-time = "2025-05-23T12:04:37.097Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746, upload-time = "2025-05-23T12:04:35.124Z" }, -] - [[package]] name = "jsonschema" version = "4.24.0" @@ -1057,7 +1048,6 @@ dependencies = [ { name = "python-dotenv" }, { name = "pyyaml" }, { name = "redis" }, - { name = "scikit-learn" }, { name = "semantic-router" }, { name = "sqlalchemy", extra = ["asyncio"] }, { name = "tavily-python" }, @@ -1103,7 +1093,6 @@ requires-dist = [ { name = "redis" }, { name = "responses", marker = "extra == 'dev'" }, { name = "ruff", marker = "extra == 'dev'" }, - { name = "scikit-learn" }, { name = "semantic-router" }, { name = "sqlalchemy", extras = ["asyncio"] }, { name = "tavily-python" }, @@ -1785,86 +1774,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/17/22bf8155aa0ea2305eefa3a6402e040df7ebe512d1310165eda1e233c3f8/s3transfer-0.13.0-py3-none-any.whl", hash = "sha256:0148ef34d6dd964d0d8cf4311b2b21c474693e57c2e069ec708ce043d2b527be", size = 85152, upload-time = "2025-05-22T19:24:48.703Z" }, ] -[[package]] -name = "scikit-learn" -version = "1.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "joblib" }, - { name = "numpy" }, - { name = "scipy" }, - { name = "threadpoolctl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/3b/29fa87e76b1d7b3b77cc1fcbe82e6e6b8cd704410705b008822de530277c/scikit_learn-1.7.0.tar.gz", hash = "sha256:c01e869b15aec88e2cdb73d27f15bdbe03bce8e2fb43afbe77c45d399e73a5a3", size = 7178217, upload-time = "2025-06-05T22:02:46.703Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/42/c6b41711c2bee01c4800ad8da2862c0b6d2956a399d23ce4d77f2ca7f0c7/scikit_learn-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ef09b1615e1ad04dc0d0054ad50634514818a8eb3ee3dee99af3bffc0ef5007", size = 11719657, upload-time = "2025-06-05T22:01:56.345Z" }, - { url = "https://files.pythonhosted.org/packages/a3/24/44acca76449e391b6b2522e67a63c0454b7c1f060531bdc6d0118fb40851/scikit_learn-1.7.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:7d7240c7b19edf6ed93403f43b0fcb0fe95b53bc0b17821f8fb88edab97085ef", size = 10712636, upload-time = "2025-06-05T22:01:59.093Z" }, - { url = "https://files.pythonhosted.org/packages/9f/1b/fcad1ccb29bdc9b96bcaa2ed8345d56afb77b16c0c47bafe392cc5d1d213/scikit_learn-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80bd3bd4e95381efc47073a720d4cbab485fc483966f1709f1fd559afac57ab8", size = 12242817, upload-time = "2025-06-05T22:02:01.43Z" }, - { url = "https://files.pythonhosted.org/packages/c6/38/48b75c3d8d268a3f19837cb8a89155ead6e97c6892bb64837183ea41db2b/scikit_learn-1.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dbe48d69aa38ecfc5a6cda6c5df5abef0c0ebdb2468e92437e2053f84abb8bc", size = 12873961, upload-time = "2025-06-05T22:02:03.951Z" }, - { url = "https://files.pythonhosted.org/packages/f4/5a/ba91b8c57aa37dbd80d5ff958576a9a8c14317b04b671ae7f0d09b00993a/scikit_learn-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:8fa979313b2ffdfa049ed07252dc94038def3ecd49ea2a814db5401c07f1ecfa", size = 10717277, upload-time = "2025-06-05T22:02:06.77Z" }, - { url = "https://files.pythonhosted.org/packages/70/3a/bffab14e974a665a3ee2d79766e7389572ffcaad941a246931c824afcdb2/scikit_learn-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c2c7243d34aaede0efca7a5a96d67fddaebb4ad7e14a70991b9abee9dc5c0379", size = 11646758, upload-time = "2025-06-05T22:02:09.51Z" }, - { url = "https://files.pythonhosted.org/packages/58/d8/f3249232fa79a70cb40595282813e61453c1e76da3e1a44b77a63dd8d0cb/scikit_learn-1.7.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:9f39f6a811bf3f15177b66c82cbe0d7b1ebad9f190737dcdef77cfca1ea3c19c", size = 10673971, upload-time = "2025-06-05T22:02:12.217Z" }, - { url = "https://files.pythonhosted.org/packages/67/93/eb14c50533bea2f77758abe7d60a10057e5f2e2cdcf0a75a14c6bc19c734/scikit_learn-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63017a5f9a74963d24aac7590287149a8d0f1a0799bbe7173c0d8ba1523293c0", size = 11818428, upload-time = "2025-06-05T22:02:14.947Z" }, - { url = "https://files.pythonhosted.org/packages/08/17/804cc13b22a8663564bb0b55fb89e661a577e4e88a61a39740d58b909efe/scikit_learn-1.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b2f8a0b1e73e9a08b7cc498bb2aeab36cdc1f571f8ab2b35c6e5d1c7115d97d", size = 12505887, upload-time = "2025-06-05T22:02:17.824Z" }, - { url = "https://files.pythonhosted.org/packages/68/c7/4e956281a077f4835458c3f9656c666300282d5199039f26d9de1dabd9be/scikit_learn-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:34cc8d9d010d29fb2b7cbcd5ccc24ffdd80515f65fe9f1e4894ace36b267ce19", size = 10668129, upload-time = "2025-06-05T22:02:20.536Z" }, - { url = "https://files.pythonhosted.org/packages/9a/c3/a85dcccdaf1e807e6f067fa95788a6485b0491d9ea44fd4c812050d04f45/scikit_learn-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5b7974f1f32bc586c90145df51130e02267e4b7e77cab76165c76cf43faca0d9", size = 11559841, upload-time = "2025-06-05T22:02:23.308Z" }, - { url = "https://files.pythonhosted.org/packages/d8/57/eea0de1562cc52d3196eae51a68c5736a31949a465f0b6bb3579b2d80282/scikit_learn-1.7.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:014e07a23fe02e65f9392898143c542a50b6001dbe89cb867e19688e468d049b", size = 10616463, upload-time = "2025-06-05T22:02:26.068Z" }, - { url = "https://files.pythonhosted.org/packages/10/a4/39717ca669296dfc3a62928393168da88ac9d8cbec88b6321ffa62c6776f/scikit_learn-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7e7ced20582d3a5516fb6f405fd1d254e1f5ce712bfef2589f51326af6346e8", size = 11766512, upload-time = "2025-06-05T22:02:28.689Z" }, - { url = "https://files.pythonhosted.org/packages/d5/cd/a19722241d5f7b51e08351e1e82453e0057aeb7621b17805f31fcb57bb6c/scikit_learn-1.7.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1babf2511e6ffd695da7a983b4e4d6de45dce39577b26b721610711081850906", size = 12461075, upload-time = "2025-06-05T22:02:31.233Z" }, - { url = "https://files.pythonhosted.org/packages/f3/bc/282514272815c827a9acacbe5b99f4f1a4bc5961053719d319480aee0812/scikit_learn-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:5abd2acff939d5bd4701283f009b01496832d50ddafa83c90125a4e41c33e314", size = 10652517, upload-time = "2025-06-05T22:02:34.139Z" }, - { url = "https://files.pythonhosted.org/packages/ea/78/7357d12b2e4c6674175f9a09a3ba10498cde8340e622715bcc71e532981d/scikit_learn-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e39d95a929b112047c25b775035c8c234c5ca67e681ce60d12413afb501129f7", size = 12111822, upload-time = "2025-06-05T22:02:36.904Z" }, - { url = "https://files.pythonhosted.org/packages/d0/0c/9c3715393343f04232f9d81fe540eb3831d0b4ec351135a145855295110f/scikit_learn-1.7.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:0521cb460426c56fee7e07f9365b0f45ec8ca7b2d696534ac98bfb85e7ae4775", size = 11325286, upload-time = "2025-06-05T22:02:39.739Z" }, - { url = "https://files.pythonhosted.org/packages/64/e0/42282ad3dd70b7c1a5f65c412ac3841f6543502a8d6263cae7b466612dc9/scikit_learn-1.7.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:317ca9f83acbde2883bd6bb27116a741bfcb371369706b4f9973cf30e9a03b0d", size = 12380865, upload-time = "2025-06-05T22:02:42.137Z" }, - { url = "https://files.pythonhosted.org/packages/4e/d0/3ef4ab2c6be4aa910445cd09c5ef0b44512e3de2cfb2112a88bb647d2cf7/scikit_learn-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:126c09740a6f016e815ab985b21e3a0656835414521c81fc1a8da78b679bdb75", size = 11549609, upload-time = "2025-06-05T22:02:44.483Z" }, -] - -[[package]] -name = "scipy" -version = "1.15.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, - { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, - { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, - { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, - { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, - { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, - { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, - { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, - { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, - { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, - { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, - { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, - { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, - { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, - { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, - { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, - { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, - { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, - { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, - { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, - { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, - { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, - { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, - { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, - { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, - { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, - { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, - { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, - { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, - { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, - { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, - { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, - { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, -] - [[package]] name = "semantic-router" version = "0.1.8" @@ -2008,15 +1917,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/9d/1ec73120c3b8fc52a84bcd51d0028ba9dfa729938abb41b41e4fcb58f263/tavily_python-0.7.5-py3-none-any.whl", hash = "sha256:e64660977c1b96df8c3327e8ff8ed626d5ca3178c52ee167ffa543731642f221", size = 15379, upload-time = "2025-06-05T23:13:05.906Z" }, ] -[[package]] -name = "threadpoolctl" -version = "3.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, -] - [[package]] name = "tiktoken" version = "0.9.0"